entity-repository 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.d.ts CHANGED
@@ -11,6 +11,7 @@ export declare function createRepositoryContext<Definitions extends EntityDefini
11
11
  useRepository: () => Repository<Definitions, Config>;
12
12
  useRepositoryQuery: <Table extends keyof Definitions>(table: Table, id: EntityIdTuple<Definitions, Config, Table>, fetcher: (id: EntityIdTuple<Definitions, Config, Table>) => Promise<Definitions[Table] | null>) => RepositoryQuery<Definitions[Table]>;
13
13
  useRepositoryListQuery: <Table extends keyof Definitions, Param>(table: Table, param: Param, options: ListQueryOptions<Definitions[Table]>, fetcher: (param: Param) => Promise<Definitions[Table][]>) => ListQueryState<Definitions[Table]>;
14
+ useObservableList: <Table extends keyof Definitions, Key>(table: Table, key: Key, options: ListQueryOptions<Definitions[Table]>) => Definitions[Table][];
14
15
  useSubscribedState: <Value>(observable: Observable<Value>, getSnapshot: () => Value) => Value;
15
16
  };
16
17
  //# sourceMappingURL=react.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,SAAS,EAA6E,MAAM,OAAO,CAAC;AAE5H,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/F,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,wBAAgB,uBAAuB,CACrC,WAAW,SAAS,iBAAiB,EACrC,MAAM,SAAS,YAAY,CAAC,WAAW,CAAC,GAAG,YAAY,CAAC,WAAW,CAAC;oDAOjE;QACD,UAAU,EAAE,UAAU,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC5C,QAAQ,EAAE,SAAS,CAAC;KACrB;;yBAyC2B,KAAK,SAAS,MAAM,WAAW,SAClD,KAAK,MACR,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,WACpC,CAAC,EAAE,EAAE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,KAC7F,eAAe,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;6BAiBN,KAAK,SAAS,MAAM,WAAW,EAAE,KAAK,SAC7D,KAAK,SACL,KAAK,WACH,gBAAgB,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,WACpC,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,KACvD,cAAc,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;yBAnDT,KAAK,cAAc,UAAU,CAAC,KAAK,CAAC,eAAe,MAAM,KAAK;EAgF3F"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,SAAS,EAA6E,MAAM,OAAO,CAAC;AAE5H,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/F,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,wBAAgB,uBAAuB,CACrC,WAAW,SAAS,iBAAiB,EACrC,MAAM,SAAS,YAAY,CAAC,WAAW,CAAC,GAAG,YAAY,CAAC,WAAW,CAAC;oDAOjE;QACD,UAAU,EAAE,UAAU,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC5C,QAAQ,EAAE,SAAS,CAAC;KACrB;;yBAyC2B,KAAK,SAAS,MAAM,WAAW,SAClD,KAAK,MACR,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,WACpC,CAAC,EAAE,EAAE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,KAC7F,eAAe,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;6BAmDN,KAAK,SAAS,MAAM,WAAW,EAAE,KAAK,SAC7D,KAAK,SACL,KAAK,WACH,gBAAgB,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,WACpC,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,KACvD,cAAc,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;wBAlCV,KAAK,SAAS,MAAM,WAAW,EAAE,GAAG,SACtD,KAAK,OACP,GAAG,WACC,gBAAgB,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,KAC5C,WAAW,CAAC,KAAK,CAAC,EAAE;yBAvDK,KAAK,cAAc,UAAU,CAAC,KAAK,CAAC,eAAe,MAAM,KAAK;EAmH3F"}
package/dist/react.js CHANGED
@@ -37,6 +37,32 @@ export function createRepositoryContext() {
37
37
  const recordQuery = useMemo(() => repository.recordQuery(table, id, fetcher), [repository, JSON.stringify(id)]);
38
38
  return useSubscribedState(recordQuery.$state, () => recordQuery.$state.value);
39
39
  }
40
+ /**
41
+ * Subscribes to a filtered/ordered observable view of the cache. Reads the
42
+ * current matching snapshot on mount, then re-emits whenever an
43
+ * insert/update/delete on the table changes the matching set. Unlike
44
+ * useRepositoryListQuery, there's no fetcher — this hook is the
45
+ * cold-start-safe read for data that's seeded elsewhere (typically by a
46
+ * sibling listQuery).
47
+ *
48
+ * `key` keys the memoization (use any value derived from the filter
49
+ * inputs, e.g. a task id).
50
+ */
51
+ function useObservableList(table, key, options) {
52
+ const repository = useRepository();
53
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- table/options intentionally ignored (stable for a given key)
54
+ const observable = useMemo(() => repository.observableList(table, options), [repository, JSON.stringify(key)]);
55
+ return useSubscribedState(observable, () => {
56
+ // Pull the current snapshot via a synchronous one-shot subscribe.
57
+ // observableList emits its first value synchronously.
58
+ let snapshot = [];
59
+ const sub = observable.subscribe((value) => {
60
+ snapshot = value;
61
+ });
62
+ sub.unsubscribe();
63
+ return snapshot;
64
+ });
65
+ }
40
66
  /**
41
67
  * Subscribes to a list query keyed only by `param`.
42
68
  *
@@ -58,6 +84,7 @@ export function createRepositoryContext() {
58
84
  useRepository,
59
85
  useRepositoryQuery,
60
86
  useRepositoryListQuery,
87
+ useObservableList,
61
88
  useSubscribedState,
62
89
  };
63
90
  }
@@ -1,4 +1,4 @@
1
- import { BehaviorSubject, Subject } from "rxjs";
1
+ import { BehaviorSubject, Observable, Subject } from "rxjs";
2
2
  import type { EntityConfig, EntityDefinitions, EntityEvent, EntityIdTuple, RepositoryConfig } from "./types";
3
3
  import { ListQuery, type ListQueryOptions } from "./list-query";
4
4
  import { RecordQuery } from "./record-query";
@@ -6,12 +6,35 @@ export declare class Repository<Definitions extends EntityDefinitions, Config ex
6
6
  private stores;
7
7
  private config;
8
8
  constructor(config: RepositoryConfig<Definitions, Config>);
9
+ /**
10
+ * Bulk upsert. Equivalent to calling `set` for every entity, but cheaper
11
+ * than a `for` loop at the callsite when seeding many records at once
12
+ * (e.g. when one fetch returns hundreds of related rows). Each entity
13
+ * still fires its own insert/update event so subscribers see the
14
+ * progression — the savings are in callsite ergonomics, not event volume.
15
+ */
16
+ multiSet<Table extends keyof Definitions>(table: Table, entities: readonly Definitions[Table][]): void;
9
17
  set<Table extends keyof Definitions>(table: Table, entity: Definitions[Table]): void;
10
18
  del<Table extends keyof Definitions>(table: Table, id: EntityIdTuple<Definitions, Config, Table>): void;
11
19
  get<Table extends keyof Definitions>(table: Table, id: EntityIdTuple<Definitions, Config, Table>): Definitions[Table] | null;
12
20
  fetch<Table extends keyof Definitions>(table: Table, id: EntityIdTuple<Definitions, Config, Table>, fetcher: (id: EntityIdTuple<Definitions, Config, Table>) => Promise<Definitions[Table] | null>): Promise<Definitions[Table] | null>;
13
21
  getObservable<Table extends keyof Definitions>(table: Table, id: EntityIdTuple<Definitions, Config, Table>): BehaviorSubject<Definitions[Table] | null>;
14
22
  getEvents<Table extends keyof Definitions>(table: Table): Subject<EntityEvent<Definitions[Table]>>;
23
+ /**
24
+ * Observable view of the cache for `table`, optionally filtered and
25
+ * ordered. On subscribe, emits the current matching snapshot
26
+ * synchronously (no cold-start race against a non-replaying event
27
+ * Subject), then emits a new array whenever an insert/update/delete on
28
+ * the table changes the matching set.
29
+ *
30
+ * Use this when a subscriber needs to mount AFTER the data may already
31
+ * have been seeded — e.g. a per-row tag query mounted when the parent
32
+ * task row renders, where the parent's fetch already filled the cache.
33
+ *
34
+ * Returned arrays are immutable snapshots — each emission is a fresh
35
+ * array, so React subscribers can compare by reference.
36
+ */
37
+ observableList<Table extends keyof Definitions>(table: Table, options?: ListQueryOptions<Definitions[Table]>): Observable<Definitions[Table][]>;
15
38
  recordQuery<Table extends keyof Definitions>(table: Table, id: EntityIdTuple<Definitions, Config, Table>, fetcher: (id: EntityIdTuple<Definitions, Config, Table>) => Promise<Definitions[Table] | null>): RecordQuery<Definitions, Config, Table>;
16
39
  getCacheKey<Table extends keyof Definitions>(table: Table, id: EntityIdTuple<Definitions, Config, Table>): string;
17
40
  getEntityKey<Table extends keyof Definitions>(table: Table, entity: Definitions[Table]): string;
@@ -1 +1 @@
1
- {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../src/repository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEhD,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,gBAAgB,EACjB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAS7C,qBAAa,UAAU,CACrB,WAAW,SAAS,iBAAiB,EACrC,MAAM,SAAS,YAAY,CAAC,WAAW,CAAC,GAAG,YAAY,CAAC,WAAW,CAAC;IAEpE,OAAO,CAAC,MAAM,CAAqD;IACnE,OAAO,CAAC,MAAM,CAAwC;gBAE1C,MAAM,EAAE,gBAAgB,CAAC,WAAW,EAAE,MAAM,CAAC;IAIzD,GAAG,CAAC,KAAK,SAAS,MAAM,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,KAAK,CAAC;IA4B7E,GAAG,CAAC,KAAK,SAAS,MAAM,WAAW,EACjC,KAAK,EAAE,KAAK,EACZ,EAAE,EAAE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC;IAsB/C,GAAG,CAAC,KAAK,SAAS,MAAM,WAAW,EACjC,KAAK,EAAE,KAAK,EACZ,EAAE,EAAE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,GAC5C,WAAW,CAAC,KAAK,CAAC,GAAG,IAAI;IAQtB,KAAK,CAAC,KAAK,SAAS,MAAM,WAAW,EACzC,KAAK,EAAE,KAAK,EACZ,EAAE,EAAE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAC7C,OAAO,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,GAC7F,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IA+BrC,aAAa,CAAC,KAAK,SAAS,MAAM,WAAW,EAC3C,KAAK,EAAE,KAAK,EACZ,EAAE,EAAE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,GAC5C,eAAe,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAgB7C,SAAS,CAAC,KAAK,SAAS,MAAM,WAAW,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAKlG,WAAW,CAAC,KAAK,SAAS,MAAM,WAAW,EACzC,KAAK,EAAE,KAAK,EACZ,EAAE,EAAE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAC7C,OAAO,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,GAC7F,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC;IAI1C,WAAW,CAAC,KAAK,SAAS,MAAM,WAAW,EACzC,KAAK,EAAE,KAAK,EACZ,EAAE,EAAE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,GAC5C,MAAM;IAIT,YAAY,CAAC,KAAK,SAAS,MAAM,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,KAAK,CAAC,GAAG,MAAM;IAI/F,SAAS,CAAC,KAAK,SAAS,MAAM,WAAW,EACvC,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,gBAAgB,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAC7C,OAAO,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,GAC3C,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC;IAIxC,OAAO,CAAC,QAAQ;IAiBhB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,qBAAqB;CAa9B"}
1
+ {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../src/repository.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { BehaviorSubject, Subject } from "rxjs";
1
+ import { BehaviorSubject, Observable, Subject } from "rxjs";
2
2
  import { ListQuery } from "./list-query";
3
3
  import { RecordQuery } from "./record-query";
4
4
  export class Repository {
@@ -7,6 +7,17 @@ export class Repository {
7
7
  constructor(config) {
8
8
  this.config = config;
9
9
  }
10
+ /**
11
+ * Bulk upsert. Equivalent to calling `set` for every entity, but cheaper
12
+ * than a `for` loop at the callsite when seeding many records at once
13
+ * (e.g. when one fetch returns hundreds of related rows). Each entity
14
+ * still fires its own insert/update event so subscribers see the
15
+ * progression — the savings are in callsite ergonomics, not event volume.
16
+ */
17
+ multiSet(table, entities) {
18
+ for (const entity of entities)
19
+ this.set(table, entity);
20
+ }
10
21
  set(table, entity) {
11
22
  const store = this.getStore(table);
12
23
  const cacheKey = this.getCacheKeyFromEntity(table, entity);
@@ -95,6 +106,69 @@ export class Repository {
95
106
  const store = this.getStore(table);
96
107
  return store.events$;
97
108
  }
109
+ /**
110
+ * Observable view of the cache for `table`, optionally filtered and
111
+ * ordered. On subscribe, emits the current matching snapshot
112
+ * synchronously (no cold-start race against a non-replaying event
113
+ * Subject), then emits a new array whenever an insert/update/delete on
114
+ * the table changes the matching set.
115
+ *
116
+ * Use this when a subscriber needs to mount AFTER the data may already
117
+ * have been seeded — e.g. a per-row tag query mounted when the parent
118
+ * task row renders, where the parent's fetch already filled the cache.
119
+ *
120
+ * Returned arrays are immutable snapshots — each emission is a fresh
121
+ * array, so React subscribers can compare by reference.
122
+ */
123
+ observableList(table, options = {}) {
124
+ const store = this.getStore(table);
125
+ const filterFn = options.filter ?? (() => true);
126
+ const orderFn = options.order ?? null;
127
+ const applyOrder = (records) => orderFn ? [...records].sort(orderFn) : records;
128
+ return new Observable((subscriber) => {
129
+ let current = applyOrder(Array.from(store.records.values()).filter(filterFn));
130
+ subscriber.next(current);
131
+ const eventsSub = store.events$.subscribe((event) => {
132
+ switch (event.type) {
133
+ case "insert":
134
+ case "update": {
135
+ const passes = filterFn(event.new);
136
+ const key = this.getEntityKey(table, event.new);
137
+ const index = current.findIndex((record) => this.getEntityKey(table, record) === key);
138
+ if (!passes) {
139
+ if (index === -1)
140
+ return;
141
+ const next = current.slice();
142
+ next.splice(index, 1);
143
+ current = applyOrder(next);
144
+ subscriber.next(current);
145
+ return;
146
+ }
147
+ const next = current.slice();
148
+ if (index === -1)
149
+ next.push(event.new);
150
+ else
151
+ next[index] = event.new;
152
+ current = applyOrder(next);
153
+ subscriber.next(current);
154
+ return;
155
+ }
156
+ case "delete": {
157
+ const key = this.getEntityKey(table, event.old);
158
+ const index = current.findIndex((record) => this.getEntityKey(table, record) === key);
159
+ if (index === -1)
160
+ return;
161
+ const next = current.slice();
162
+ next.splice(index, 1);
163
+ current = applyOrder(next);
164
+ subscriber.next(current);
165
+ return;
166
+ }
167
+ }
168
+ });
169
+ return () => eventsSub.unsubscribe();
170
+ });
171
+ }
98
172
  recordQuery(table, id, fetcher) {
99
173
  return new RecordQuery(this, table, id, fetcher);
100
174
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entity-repository",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Type-safe entity caching and state management with RxJS and React",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,6 +17,8 @@
17
17
  "scripts": {
18
18
  "build": "tsc",
19
19
  "clean": "rm -rf dist",
20
+ "test": "node --import tsx --test tests/*.test.ts",
21
+ "prepare": "npm run build",
20
22
  "prepublishOnly": "npm run clean && npm run build"
21
23
  },
22
24
  "keywords": [
@@ -50,7 +52,9 @@
50
52
  }
51
53
  },
52
54
  "devDependencies": {
55
+ "@types/node": "^22.0.0",
53
56
  "@types/react": "^19.2.8",
57
+ "tsx": "^4.19.0",
54
58
  "typescript": "^5.9.3"
55
59
  }
56
60
  }