npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

entity-repository

v0.1.3

Published

Type-safe entity caching and state management with RxJS and React

Readme

entity-repository

Type-safe entity caching and state management with RxJS observables and React integration.

Features

  • Type-safe entity management - Full TypeScript generics for entity definitions and ID types
  • In-memory caching - Fast Map-based cache by table and entity ID
  • RxJS-based reactivity - Observable streams for real-time updates
  • Query layer - RecordQuery for single entities, ListQuery for filtered/sorted lists
  • React integration - Context provider and hooks for seamless React usage
  • Request deduplication - Prevents duplicate fetch requests for the same entity
  • Real-time event system - Insert/update/delete events for reactive list updates

Installation

npm install entity-repository rxjs

Quick Start

1. Define your entity types

import type { EntityConfig } from "entity-repository";

// Define the shape of your entities
type Entities = {
  users: { id: string; name: string; email: string };
  posts: { id: string; title: string; authorId: string; createdAt: string };
};

// Configure which field is the ID for each entity
const entityConfig = {
  users: { id: "id" },
  posts: { id: "id" },
} as const satisfies EntityConfig<Entities>;

type MyEntityConfig = typeof entityConfig;

2. Create the repository

import { Repository } from "entity-repository";

const repository = new Repository<Entities, MyEntityConfig>({
  entities: entityConfig,
});

3. Basic operations

// Store an entity
repository.set("users", { id: "1", name: "Alice", email: "[email protected]" });

// Get an entity (returns null if not cached)
const user = repository.get("users", { id: "1" });

// Fetch with caching (fetches only if not cached, deduplicates concurrent requests)
const user = await repository.fetch("users", { id: "1" }, async (id) => {
  const response = await fetch(`/api/users/${id.id}`);
  return response.json();
});

// Delete an entity
repository.del("users", { id: "1" });

4. Reactive subscriptions

// Subscribe to entity changes
const observable = repository.getObservable("users", { id: "1" });
observable.subscribe((user) => {
  console.log("User changed:", user);
});

// Subscribe to all events for a table (insert/update/delete)
repository.getEvents("users").subscribe((event) => {
  if (event.type === "insert") console.log("New user:", event.new);
  if (event.type === "update") console.log("Updated:", event.old, "->", event.new);
  if (event.type === "delete") console.log("Deleted:", event.old);
});

React Integration

Setup

import { useMemo } from "react";
import { createRepositoryContext, Repository } from "entity-repository";

// Create typed context and hooks
export const {
  RepositoryProvider,
  useRepository,
  useRepositoryQuery,
  useRepositoryListQuery,
  useSubscribedState,
} = createRepositoryContext<Entities, MyEntityConfig>();

// In your app root
function App() {
  const repository = useMemo(
    () => new Repository<Entities, MyEntityConfig>({ entities: entityConfig }),
    []
  );

  return (
    <RepositoryProvider repository={repository}>
      <YourApp />
    </RepositoryProvider>
  );
}

useRepositoryQuery - Single entity

function UserProfile({ userId }: { userId: string }) {
  const { entity: user, status } = useRepositoryQuery(
    "users",
    { id: userId },
    async (id) => {
      const response = await fetch(`/api/users/${id.id}`);
      return response.json();
    }
  );

  if (status === "fetching") return <div>Loading...</div>;
  if (status === "error") return <div>Error loading user</div>;
  if (!user) return <div>User not found</div>;

  return <div>{user.name}</div>;
}

useRepositoryListQuery - Entity lists with filter/sort

function UserPosts({ authorId }: { authorId: string }) {
  const { records: posts, status } = useRepositoryListQuery(
    "posts",
    { authorId },  // param - query re-runs when this changes
    {
      filter: (post) => post.authorId === authorId,
      order: (a, b) => b.createdAt.localeCompare(a.createdAt),
    },
    async (param) => {
      const response = await fetch(`/api/posts?author=${param.authorId}`);
      return response.json();
    }
  );

  if (status === "fetching" && posts.length === 0) return <div>Loading...</div>;

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

useSubscribedState - Subscribe to any RxJS observable

import { interval } from "rxjs";

function Timer() {
  const seconds = useSubscribedState(interval(1000), 0);
  return <div>Seconds: {seconds}</div>;
}

Real-time Updates Integration

The repository emits events when entities change. Connect to your real-time backend:

// Example with Supabase Realtime
useEffect(() => {
  const channel = supabase
    .channel("changes")
    .on("postgres_changes", { event: "*", schema: "public" }, (payload) => {
      const table = payload.table as keyof Entities;

      if (payload.eventType === "DELETE") {
        repository.del(table, { id: payload.old.id });
      } else {
        repository.set(table, payload.new);
      }
    })
    .subscribe();

  return () => supabase.removeChannel(channel);
}, [repository, supabase]);

Advanced Usage

RecordQuery - Standalone single-entity query

const query = repository.recordQuery("users", { id: "1" }, async (id) => {
  const response = await fetch(`/api/users/${id.id}`);
  return response.json();
});

// Subscribe to state changes
query.$state.subscribe((state) => {
  console.log("Status:", state.status);
  console.log("Entity:", state.entity);
});

// Manually trigger a fetch
await query.fetch();

// Cleanup when done
query.dispose();

ListQuery - Standalone list query

const query = repository.listQuery(
  "posts",
  {
    filter: (post) => post.authorId === "1",
    order: (a, b) => b.createdAt.localeCompare(a.createdAt),
  },
  async () => {
    const response = await fetch("/api/posts?author=1");
    return response.json();
  }
);

// Subscribe to records and status separately
query.$records.subscribe((posts) => {
  console.log("Posts:", posts);
});

query.$status.subscribe((status) => {
  console.log("Status:", status.status);
});

// Manually trigger a refetch
await query.refetch();

// Cleanup when done
query.dispose();

Cache key utilities

// Get the cache key for an entity ID
const key = repository.getCacheKey("users", { id: "1" }); // "1"

// Get the cache key from an entity object
const key = repository.getEntityKey("users", { id: "1", name: "Alice", email: "..." }); // "1"

API Reference

Repository

| Method | Description | |--------|-------------| | set(table, entity) | Store entity in cache, emits insert/update event | | get(table, id) | Get cached entity or null | | del(table, id) | Remove entity from cache, emits delete event | | fetch(table, id, fetcher) | Get cached or fetch, deduplicates concurrent requests | | getObservable(table, id) | BehaviorSubject for entity changes | | getEvents(table) | Subject emitting insert/update/delete events | | recordQuery(table, id, fetcher) | Create RecordQuery instance | | listQuery(table, options, fetcher) | Create ListQuery instance | | getCacheKey(table, id) | Get cache key string from entity ID | | getEntityKey(table, entity) | Get cache key string from entity object |

RecordQuery

Manages single-entity queries with status tracking.

| Property/Method | Description | |-----------------|-------------| | $state | BehaviorSubject<{ entity, status }> - subscribe to state changes | | fetch() | Manually trigger fetch, returns entity | | dispose() | Cleanup subscriptions |

ListQuery

Manages list queries with filter/sort and real-time updates.

| Property/Method | Description | |-----------------|-------------| | $records | BehaviorSubject<Entity[]> - subscribe to record list | | $status | BehaviorSubject<{ status, error? }> - subscribe to status | | refetch() | Manually trigger refetch, returns records | | dispose() | Cleanup subscriptions |

React Hooks

| Hook | Description | |------|-------------| | useRepository() | Access repository instance from context | | useRepositoryQuery(table, id, fetcher) | Subscribe to single entity query | | useRepositoryListQuery(table, param, options, fetcher) | Subscribe to filtered/sorted list query | | useSubscribedState(observable, initial) | Subscribe to any RxJS observable |

Types

// Entity configuration - maps tables to their ID field
type EntityConfig<Definitions> = {
  [Table in keyof Definitions]: { id: keyof Definitions[Table] & string };
};

// Entity ID tuple - picks only the ID field from entity
type EntityIdTuple<Definitions, Config, Table> =
  Pick<Definitions[Table], Config[Table]["id"]>;

// Entity event types - emitted on insert/update/delete
type EntityEvent<Entity> = {
  timestamp: Date;
} & (
  | { type: "insert"; new: Entity }
  | { type: "update"; old: Entity; new: Entity }
  | { type: "delete"; old: Entity }
);

// Single-entity query state
type RepositoryQuery<Entity> = {
  entity: Entity | null;
} & (
  | { status: "fetching" }
  | { status: "idle" }
  | { status: "error"; error: Error }
);

// List query options
type ListQueryOptions<Entity> = {
  filter?: (entity: Entity) => boolean;
  order?: (left: Entity, right: Entity) => number;
};

// List query status
type ListQueryStatus =
  | { status: "idle" }
  | { status: "fetching" }
  | { status: "error"; error: Error };

// List query state (returned by useRepositoryListQuery)
type ListQueryState<Entity> = {
  records: Entity[];
} & ListQueryStatus;

How It Works

Caching Strategy

The repository uses a simple Map-based cache keyed by table name and entity ID:

  1. Set: Stores entity and emits insert (new) or update (existing) event
  2. Get: Returns cached entity or null (synchronous)
  3. Fetch: Returns cached entity, or fetches and caches if not present
  4. Request deduplication: Concurrent fetches for the same entity share a single request

Reactivity

  • getObservable() returns a BehaviorSubject that emits when an entity changes
  • getEvents() returns a Subject that emits insert/update/delete events for a table
  • ListQuery subscribes to table events and automatically updates its filtered list

React Integration

  • useRepositoryQuery creates a RecordQuery and subscribes to its state
  • useRepositoryListQuery creates a ListQuery and subscribes to records + status
  • Both hooks use JSON.stringify(id/param) for stable memoization keys

License

MIT