liwi-mongo 14.1.0 → 15.0.0

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/README.md CHANGED
@@ -290,13 +290,31 @@ const subscription = query.fetchAndSubscribe((error, changes) => {
290
290
  subscription.stop();
291
291
  ```
292
292
 
293
- `createQuerySingleItem` behaves the same but resolves a single item. Without `sort`, its criteria must match at most one document: when one update moves several matching documents, the subscription reports the error `should not match more than 1, use sort if you can have multiple match` through its callback. With `sort`, it refetches on every relevant update.
293
+ `createQuerySingleItem` behaves the same but resolves a single item. It keeps the key of the item its client holds: a write concerns it when it is about that key or the written document matches. Without `sort`, its criteria must match at most one document: when one update moves several matching documents, the subscription reports the error `should not match more than 1, use sort if you can have multiple match` through its callback. With `sort`, it refetches on every relevant update, and when its item is deleted.
294
294
 
295
295
  Subscriptions are driven in-process: writes going through the subscribe store are matched against the query criteria with [mingo](https://github.com/kofrasa/mingo).
296
296
 
297
+ Each collection subscription keeps the keys its client holds, and decides from them and the stored document, not from the write's `prev`:
298
+
299
+ | Held by the client | Document matches the criteria | Sent |
300
+ | ------------------ | ----------------------------- | ---------- |
301
+ | yes | yes | `updated` |
302
+ | yes | no, or deleted | `deleted` |
303
+ | no | yes | `inserted` |
304
+ | no | no, or deleted | nothing |
305
+
306
+ A `changed` notification ([writes from another process](../liwi-subscribe-store#writes-from-another-process)) is re-read by each concerned subscription, in its order, so a repeated or late one is harmless.
307
+
308
+ With `limit` or `skip`, a document entering or leaving the result refetches it (`find` and `count`) and sends `initial` instead: only the server knows which document fills or leaves the window. With `limit`, when more documents match than the query shows (a 101st matching document exists for `limit: 100`), and with `skip`, a write to any document the client does not hold refetches, matching or not: a matching one may enter the window, and one that left the criteria may have shifted a skipped page or changed `meta.total`, which neither its key nor its new state shows. When a limited result holds every match, only a write making a document match refetches. Each such write costs one refetch per open subscription of that query.
309
+
310
+ `resync()` on the store refetches every subscription and sends `initial`, also to one opened with `subscribe`. Such a subscription (no initial result) still fetches once, to know the keys: its thenable resolves after that fetch, and rejects when it fails. When a write happens during that fetch, it sends the fetch result as `initial` anyway: the client's own earlier result may hold a document the fetch no longer sees, or miss one it does. Until a fetch succeeded, writes are ignored; a `resync` retries.
311
+
312
+ Changes are computed one action at a time per subscription, so a refetch is never overtaken by an older change. While nothing is pending they reach the callback during the write call, as before; behind a refetch (and the initial fetch) they wait for it.
313
+
297
314
  Caveats:
298
315
 
299
- - Writes performed outside this process (other services, mongo shell, migrations) are not detected.
316
+ - Writes performed outside a subscribe store (mongo shell, migrations) are not detected. Another process writing through its own subscribe store can relay its keys.
317
+ - Updated rows of a sorted query are sent as `updated`: the client keeps their position.
300
318
  - `$text` criteria cannot be matched in memory: such queries fetch correctly but never emit updates.
301
319
  - Queries returned by the subscribe store are fixed — `changeParams` throws; build a new query when parameters change.
302
320
 
@@ -11,7 +11,21 @@ export default class MongoQueryCollection<Model extends MongoBaseModel<KeyValue>
11
11
  constructor(store: MongoStore<Model, KeyValue>, options: QueryOptions<Model>, transformer?: Transformer<Model, Item>);
12
12
  createTestCriteria(): TestCriteria;
13
13
  fetch<T>(onFulfilled: (result: QueryResult<Item[]>) => T): Promise<T>;
14
+ /**
15
+ * Each subscription keeps the keys its client holds, and decides from them and
16
+ * the stored document alone, never from the emitted `prev`. A `changed`
17
+ * notification (written by another process) is re-read here, in this
18
+ * subscription's order, if it concerns it: a repeated, late or crossing
19
+ * notification leaves the client equal to a fresh fetch. A subscription opened
20
+ * without its initial result still fetches it, to know those keys, and sends it
21
+ * when a write raced with that fetch. With `limit` or `skip`, a document
22
+ * entering or leaving refetches instead: only the server knows what fills or
23
+ * leaves the window.
24
+ */
14
25
  _subscribe(callback: SubscribeCallback<KeyValue, Item[]>, _includeInitial: boolean): QuerySubscription;
26
+ private canTestCriteria;
27
+ private isWindowed;
28
+ private computeMemberships;
15
29
  private createMongoCursor;
16
30
  }
17
31
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"MongoQueryCollection.d.ts","sourceRoot":"","sources":["../../src/MongoQueryCollection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EAEf,YAAY,EACZ,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,8BAA8B,EAAE,MAAM,sBAAsB,CAAC;AAEtE,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,YAAY,EACb,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,UAAU,MAAM,cAAc,CAAC;AAS3C,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;AAE1C,MAAM,CAAC,OAAO,OAAO,oBAAoB,CACvC,KAAK,SAAS,cAAc,CAAC,QAAQ,CAAC,EACtC,MAAM,SAAS,WAAW,CAAC,MAAM,CAAC,GAAG,KAAK,EAC1C,QAAQ,SAAS,eAAe,GAAG,KAAK,CAAC,KAAK,CAAC,EAC/C,IAAI,SAAS,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,GAAG,KAAK,CACnD,SAAQ,8BAA8B,CACtC,YAAY,EACZ,QAAQ,EACR,KAAK,EACL,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,EAChC,MAAM,EACN,IAAI,EAAE,CACP;IACC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IAEpD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAE9C,OAAO,CAAC,YAAY,CAAC,CAAe;IAEpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA2B;IAEvD,YACE,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,EAClC,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC,EAC5B,WAAW,GAAE,WAAW,CAAC,KAAK,EAAE,IAAI,CAAuB,EAM5D;IAED,kBAAkB,IAAI,YAAY,CAejC;IAEK,KAAK,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAe1E;IAED,UAAU,CACR,QAAQ,EAAE,iBAAiB,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,EAC7C,eAAe,EAAE,OAAO,GACvB,iBAAiB,CA+GnB;YAEa,iBAAiB;CAmBhC"}
1
+ {"version":3,"file":"MongoQueryCollection.d.ts","sourceRoot":"","sources":["../../src/MongoQueryCollection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EAGf,YAAY,EACZ,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,8BAA8B,EAAE,MAAM,sBAAsB,CAAC;AAEtE,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,YAAY,EACb,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,UAAU,MAAM,cAAc,CAAC;AAU3C,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;AAkD1C,MAAM,CAAC,OAAO,OAAO,oBAAoB,CACvC,KAAK,SAAS,cAAc,CAAC,QAAQ,CAAC,EACtC,MAAM,SAAS,WAAW,CAAC,MAAM,CAAC,GAAG,KAAK,EAC1C,QAAQ,SAAS,eAAe,GAAG,KAAK,CAAC,KAAK,CAAC,EAC/C,IAAI,SAAS,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,GAAG,KAAK,CACnD,SAAQ,8BAA8B,CACtC,YAAY,EACZ,QAAQ,EACR,KAAK,EACL,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,EAChC,MAAM,EACN,IAAI,EAAE,CACP;IACC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IAEpD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAE9C,OAAO,CAAC,YAAY,CAAC,CAAe;IAEpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA2B;IAEvD,YACE,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,EAClC,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC,EAC5B,WAAW,GAAE,WAAW,CAAC,KAAK,EAAE,IAAI,CAAuB,EAM5D;IAED,kBAAkB,IAAI,YAAY,CAejC;IAEK,KAAK,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAe1E;IAED;;;;;;;;;;OAUG;IACH,UAAU,CACR,QAAQ,EAAE,iBAAiB,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,EAC7C,eAAe,EAAE,OAAO,GACvB,iBAAiB,CAkHnB;IAED,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,kBAAkB;YA0BZ,iBAAiB;CAmBhC"}
@@ -11,6 +11,10 @@ export default class MongoQuerySingleItem<Model extends MongoBaseModel<KeyValue>
11
11
  constructor(store: MongoStore<Model, KeyValue>, options: QueryOptions<Model>, transformer?: Transformer<Model, Result>);
12
12
  createMingoTestCriteria(): TestCriteria;
13
13
  fetch<T>(onFulfilled: (result: QueryResult<Result>) => T): Promise<T>;
14
+ /**
15
+ * From the key the client holds and the written documents, not from `prev`.
16
+ * `refetch` when the result can only be known by querying again.
17
+ */
14
18
  private computeChanges;
15
19
  _subscribe(callback: SubscribeCallback<KeyValue, Result>, _includeInitial: boolean): QuerySubscription;
16
20
  private createMongoCursor;
@@ -1 +1 @@
1
- {"version":3,"file":"MongoQuerySingleItem.d.ts","sourceRoot":"","sources":["../../src/MongoQuerySingleItem.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EAEf,YAAY,EACZ,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,8BAA8B,EAAE,MAAM,sBAAsB,CAAC;AAEtE,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,YAAY,EACb,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,UAAU,MAAM,cAAc,CAAC;AAS3C,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;AAE1C,MAAM,CAAC,OAAO,OAAO,oBAAoB,CACvC,KAAK,SAAS,cAAc,CAAC,QAAQ,CAAC,EACtC,MAAM,SAAS,WAAW,CAAC,MAAM,CAAC,GAAG,KAAK,EAC1C,MAAM,SAAS,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,EACnE,QAAQ,SAAS,eAAe,GAAG,KAAK,CAAC,KAAK,CAAC,CAC/C,SAAQ,8BAA8B,CACtC,YAAY,EACZ,QAAQ,EACR,KAAK,EACL,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,EAChC,MAAM,EACN,MAAM,CACP;IACC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IAEpD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAE9C,OAAO,CAAC,YAAY,CAAC,CAAe;IAEpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IAEzD,YACE,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,EAClC,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC,EAC5B,WAAW,GAAE,WAAW,CAAC,KAAK,EAAE,MAAM,CAAuB,EAM9D;IAED,uBAAuB,IAAI,YAAY,CAWtC;IAEK,KAAK,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAe1E;YAEa,cAAc;IAkD5B,UAAU,CACR,QAAQ,EAAE,iBAAiB,CAAC,QAAQ,EAAE,MAAM,CAAC,EAC7C,eAAe,EAAE,OAAO,GACvB,iBAAiB,CAuCnB;YAEa,iBAAiB;CAehC"}
1
+ {"version":3,"file":"MongoQuerySingleItem.d.ts","sourceRoot":"","sources":["../../src/MongoQuerySingleItem.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EAGf,YAAY,EACZ,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,8BAA8B,EAAE,MAAM,sBAAsB,CAAC;AAEtE,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,YAAY,EACb,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,UAAU,MAAM,cAAc,CAAC;AAU3C,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;AAE1C,MAAM,CAAC,OAAO,OAAO,oBAAoB,CACvC,KAAK,SAAS,cAAc,CAAC,QAAQ,CAAC,EACtC,MAAM,SAAS,WAAW,CAAC,MAAM,CAAC,GAAG,KAAK,EAC1C,MAAM,SAAS,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,EACnE,QAAQ,SAAS,eAAe,GAAG,KAAK,CAAC,KAAK,CAAC,CAC/C,SAAQ,8BAA8B,CACtC,YAAY,EACZ,QAAQ,EACR,KAAK,EACL,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,EAChC,MAAM,EACN,MAAM,CACP;IACC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IAEpD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAE9C,OAAO,CAAC,YAAY,CAAC,CAAe;IAEpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IAEzD,YACE,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,EAClC,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC,EAC5B,WAAW,GAAE,WAAW,CAAC,KAAK,EAAE,MAAM,CAAuB,EAM9D;IAED,uBAAuB,IAAI,YAAY,CAWtC;IAEK,KAAK,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAe1E;IAED;;;OAGG;IACH,OAAO,CAAC,cAAc;IAgDtB,UAAU,CACR,QAAQ,EAAE,iBAAiB,CAAC,QAAQ,EAAE,MAAM,CAAC,EAC7C,eAAe,EAAE,OAAO,GACvB,iBAAiB,CAiGnB;YAEa,iBAAiB;CAehC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=MongoQuerySubscription.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MongoQuerySubscription.test.d.ts","sourceRoot":"","sources":["../../src/MongoQuerySubscription.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,9 @@
1
+ export type ActionRunner = () => Promise<void> | undefined;
2
+ /**
3
+ * Runs a subscription's actions in order: synchronously while nothing
4
+ * asynchronous is pending, so a change reaches the subscriber during the write
5
+ * call; behind the pending work otherwise (the initial fetch, a refetch), so a
6
+ * refetch is never overtaken by an older change.
7
+ */
8
+ export declare const createActionQueue: (initial: Promise<unknown>, onError: (error: unknown) => void) => ((run: ActionRunner) => void);
9
+ //# sourceMappingURL=createActionQueue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createActionQueue.d.ts","sourceRoot":"","sources":["../../src/createActionQueue.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,YACnB,OAAO,CAAC,OAAO,CAAC,WAChB,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,KAChC,CAAC,CAAC,GAAG,EAAE,YAAY,KAAK,IAAI,CAgC9B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=createActionQueue.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createActionQueue.test.d.ts","sourceRoot":"","sources":["../../src/createActionQueue.test.ts"],"names":[],"mappings":""}
@@ -45,7 +45,64 @@ class MongoCursor extends AbstractStoreCursor {
45
45
  }
46
46
  }
47
47
 
48
+ const createActionQueue = (initial, onError) => {
49
+ let tail;
50
+ const track = (promise, isReported) => {
51
+ const current = promise.then(
52
+ () => {
53
+ },
54
+ (error) => {
55
+ if (isReported) onError(error);
56
+ }
57
+ ).then(() => {
58
+ if (tail === current) tail = void 0;
59
+ });
60
+ tail = current;
61
+ };
62
+ track(initial, false);
63
+ return (run) => {
64
+ if (tail) {
65
+ track(tail.then(run), true);
66
+ return;
67
+ }
68
+ try {
69
+ const pending = run();
70
+ if (pending) track(pending, true);
71
+ } catch (error) {
72
+ onError(error);
73
+ }
74
+ };
75
+ };
76
+
48
77
  const identityTransformer$1 = (model) => model;
78
+ const getWrittenStates = (action, keyPath) => {
79
+ const states = /* @__PURE__ */ new Map();
80
+ switch (action.type) {
81
+ case "inserted":
82
+ action.next.forEach((next) => states.set(next[keyPath], next));
83
+ break;
84
+ case "deleted":
85
+ action.prev.forEach((prev) => states.set(prev[keyPath], void 0));
86
+ break;
87
+ case "updated":
88
+ action.changes.forEach(([, next]) => states.set(next[keyPath], next));
89
+ break;
90
+ default:
91
+ throw new Error("Unsupported type");
92
+ }
93
+ return states;
94
+ };
95
+ const toCollectionChanges = ({
96
+ deleted,
97
+ updated,
98
+ inserted
99
+ }) => {
100
+ const changes = [];
101
+ if (deleted.length > 0) changes.push({ type: "deleted", keys: deleted });
102
+ if (updated.length > 0) changes.push({ type: "updated", result: updated });
103
+ if (inserted.length > 0) changes.push({ type: "inserted", result: inserted });
104
+ return changes;
105
+ };
49
106
  class MongoQueryCollection extends AbstractSubscribableStoreQuery {
50
107
  store;
51
108
  options;
@@ -85,81 +142,122 @@ class MongoQueryCollection extends AbstractSubscribableStoreQuery {
85
142
  }
86
143
  });
87
144
  }
145
+ /**
146
+ * Each subscription keeps the keys its client holds, and decides from them and
147
+ * the stored document alone, never from the emitted `prev`. A `changed`
148
+ * notification (written by another process) is re-read here, in this
149
+ * subscription's order, if it concerns it: a repeated, late or crossing
150
+ * notification leaves the client equal to a fresh fetch. A subscription opened
151
+ * without its initial result still fetches it, to know those keys, and sends it
152
+ * when a write raced with that fetch. With `limit` or `skip`, a document
153
+ * entering or leaving refetches instead: only the server knows what fills or
154
+ * leaves the window.
155
+ */
88
156
  _subscribe(callback, _includeInitial) {
89
157
  const store = super.getSubscribeStore();
158
+ const keyPath = this.store.keyPath;
90
159
  const testCriteria = this.createTestCriteria();
91
- const promise = _includeInitial ? this.fetch(({ result, meta, info }) => {
92
- callback(null, [
93
- {
94
- type: "initial",
95
- initial: result,
96
- queryInfo: info,
97
- meta
98
- }
99
- ]);
100
- }) : Promise.resolve();
101
- const unsubscribe = store.subscribe((action) => {
102
- const changes = [];
103
- switch (action.type) {
104
- case "inserted": {
105
- const filtered = action.next.filter(testCriteria);
106
- if (filtered.length > 0) {
107
- changes.push({
108
- type: "inserted",
109
- result: filtered.map(this.transformer)
110
- });
111
- }
112
- break;
113
- }
114
- case "deleted": {
115
- const filtered = action.prev.filter(testCriteria);
116
- if (filtered.length > 0) {
117
- changes.push({
118
- type: "deleted",
119
- keys: filtered.map((object) => object[this.store.keyPath])
120
- });
121
- }
122
- break;
123
- }
124
- case "updated": {
125
- const {
126
- deleted,
127
- updated,
128
- inserted
129
- } = { deleted: [], updated: [], inserted: [] };
130
- action.changes.forEach(([prevObject, nextObject]) => {
131
- if (testCriteria(prevObject)) {
132
- if (!testCriteria(nextObject)) {
133
- deleted.push(prevObject[this.store.keyPath]);
134
- } else {
135
- updated.push(this.transformer(nextObject));
136
- }
137
- } else if (testCriteria(nextObject)) {
138
- inserted.push(this.transformer(nextObject));
139
- }
140
- });
141
- if (deleted.length > 0) {
142
- changes.push({ type: "deleted", keys: deleted });
143
- }
144
- if (updated.length > 0) {
145
- changes.push({ type: "updated", result: updated });
146
- }
147
- if (inserted.length > 0) {
148
- changes.push({ type: "inserted", result: inserted });
149
- }
150
- break;
160
+ let sentKeys = /* @__PURE__ */ new Set();
161
+ let total = 0;
162
+ let isReady = false;
163
+ let isInitializing = true;
164
+ let hasActionDuringInit = false;
165
+ let isStopped = false;
166
+ const send = (changes) => {
167
+ if (!isStopped && changes.length > 0) callback(null, changes);
168
+ };
169
+ const refetch = () => this.fetch(({ result, meta, info }) => {
170
+ sentKeys = new Set(result.map((item) => item[keyPath]));
171
+ total = meta.total;
172
+ isReady = true;
173
+ return [{ type: "initial", initial: result, queryInfo: info, meta }];
174
+ });
175
+ const initialPromise = refetch().then((changes) => {
176
+ isInitializing = false;
177
+ if (_includeInitial || hasActionDuringInit) send(changes);
178
+ });
179
+ const applyStates = (states) => {
180
+ const memberships = this.computeMemberships(states, sentKeys);
181
+ if (this.isWindowed() && (memberships.inserted.length > 0 || memberships.deleted.length > 0)) {
182
+ return refetch().then(send);
183
+ }
184
+ memberships.deleted.forEach((key) => sentKeys.delete(key));
185
+ memberships.inserted.forEach((item) => sentKeys.add(item[keyPath]));
186
+ send(toCollectionChanges(memberships));
187
+ return void 0;
188
+ };
189
+ const rereadAndApply = async (keys) => {
190
+ const states = new Map(
191
+ keys.map((key) => [key, void 0])
192
+ );
193
+ const byKeys = { [keyPath]: { $in: keys } };
194
+ const stored = await this.store.findAll(byKeys);
195
+ stored.forEach((model) => states.set(model[keyPath], model));
196
+ if (!isStopped) await applyStates(states);
197
+ };
198
+ const handleAction = (action) => {
199
+ if (isStopped) return void 0;
200
+ if (action.type === "resync") return refetch().then(send);
201
+ if (!isReady || !this.canTestCriteria()) return void 0;
202
+ if (this.options.skip || this.options.limit && total > sentKeys.size) {
203
+ const keys = action.type === "changed" ? action.keys : [...getWrittenStates(action, keyPath).keys()];
204
+ if (keys.some((key) => !sentKeys.has(key))) {
205
+ return refetch().then(send);
151
206
  }
152
- default:
153
- throw new Error("Unsupported type");
154
207
  }
155
- if (changes.length === 0) return;
156
- callback(null, changes);
208
+ if (action.type === "changed") {
209
+ const matchingKeys = new Set(
210
+ action.next.filter(testCriteria).map((model) => model[keyPath])
211
+ );
212
+ const concernedKeys = action.keys.filter(
213
+ (key) => sentKeys.has(key) || matchingKeys.has(key)
214
+ );
215
+ return concernedKeys.length > 0 ? rereadAndApply(concernedKeys) : void 0;
216
+ }
217
+ return applyStates(getWrittenStates(action, keyPath));
218
+ };
219
+ const enqueue = createActionQueue(initialPromise, (error) => {
220
+ if (!isStopped) callback(error, []);
157
221
  });
222
+ const unsubscribe = store.subscribe((action) => {
223
+ if (isInitializing) hasActionDuringInit = true;
224
+ enqueue(() => handleAction(action));
225
+ });
226
+ const stop = () => {
227
+ isStopped = true;
228
+ unsubscribe();
229
+ };
158
230
  return {
159
- stop: unsubscribe,
160
- cancel: unsubscribe,
161
- then: (onFulfilled, onRejected) => promise.then(onFulfilled, onRejected)
231
+ stop,
232
+ cancel: stop,
233
+ then: (onFulfilled, onRejected) => initialPromise.then(onFulfilled, onRejected)
234
+ };
235
+ }
236
+ canTestCriteria() {
237
+ return !this.options.criteria || !("$text" in this.options.criteria);
238
+ }
239
+ isWindowed() {
240
+ return !!this.options.limit || !!this.options.skip;
241
+ }
242
+ computeMemberships(states, sentKeys) {
243
+ const testCriteria = this.createTestCriteria();
244
+ const memberships = {
245
+ deleted: [],
246
+ updated: [],
247
+ inserted: []
162
248
  };
249
+ states.forEach((next, key) => {
250
+ const isSent = sentKeys.has(key);
251
+ const isMatching = next !== void 0 && testCriteria(next);
252
+ if (isSent && isMatching) {
253
+ memberships.updated.push(this.transformer(next));
254
+ } else if (isSent) {
255
+ memberships.deleted.push(key);
256
+ } else if (isMatching) {
257
+ memberships.inserted.push(this.transformer(next));
258
+ }
259
+ });
260
+ return memberships;
163
261
  }
164
262
  async createMongoCursor() {
165
263
  const cursor = await this.store.cursor(
@@ -214,7 +312,12 @@ class MongoQuerySingleItem extends AbstractSubscribableStoreQuery {
214
312
  });
215
313
  });
216
314
  }
217
- async computeChanges(action, testCriteria) {
315
+ /**
316
+ * From the key the client holds and the written documents, not from `prev`.
317
+ * `refetch` when the result can only be known by querying again.
318
+ */
319
+ computeChanges(action, testCriteria, currentKey) {
320
+ const keyPath = this.store.keyPath;
218
321
  switch (action.type) {
219
322
  case "inserted": {
220
323
  const filtered = action.next.filter(testCriteria);
@@ -222,24 +325,17 @@ class MongoQuerySingleItem extends AbstractSubscribableStoreQuery {
222
325
  return [{ type: "updated", result: this.transformer(filtered[0]) }];
223
326
  }
224
327
  case "deleted": {
225
- const filtered = action.prev.filter(testCriteria);
226
- if (filtered.length === 0) return [];
227
- return [
228
- {
229
- type: "deleted",
230
- keys: filtered.map((object) => object[this.store.keyPath])
231
- }
232
- ];
328
+ const isCurrentDeleted = currentKey !== null && action.prev.some((prev) => prev[keyPath] === currentKey);
329
+ if (!isCurrentDeleted) return [];
330
+ if (this.options.sort) return "refetch";
331
+ return [{ type: "deleted", keys: [currentKey] }];
233
332
  }
234
333
  case "updated": {
235
334
  const filtered = action.changes.filter(
236
- ([prev, next2]) => testCriteria(prev) || testCriteria(next2)
335
+ ([, next2]) => next2[keyPath] === currentKey || testCriteria(next2)
237
336
  );
238
337
  if (filtered.length === 0) return [];
239
- if (this.options.sort) {
240
- const { result } = await this.fetch((res) => res);
241
- return [{ type: "updated", result }];
242
- }
338
+ if (this.options.sort) return "refetch";
243
339
  if (filtered.length > 1) {
244
340
  throw new Error(
245
341
  "should not match more than 1, use sort if you can have multiple match"
@@ -260,31 +356,78 @@ class MongoQuerySingleItem extends AbstractSubscribableStoreQuery {
260
356
  _subscribe(callback, _includeInitial) {
261
357
  const store = super.getSubscribeStore();
262
358
  const testCriteria = this.createMingoTestCriteria();
263
- const promise = _includeInitial ? this.fetch(({ result, meta, info }) => {
264
- callback(null, [
265
- {
266
- type: "initial",
267
- initial: result,
268
- queryInfo: info,
269
- meta
270
- }
271
- ]);
272
- }) : Promise.resolve();
273
- const unsubscribe = store.subscribe(async (action) => {
274
- let changes;
275
- try {
276
- changes = await this.computeChanges(action, testCriteria);
277
- } catch (error) {
278
- callback(error, []);
279
- return;
359
+ const keyPath = this.store.keyPath;
360
+ let currentKey = null;
361
+ let isReady = false;
362
+ let isStopped = false;
363
+ const getKeyAfter = (change) => {
364
+ switch (change.type) {
365
+ case "initial":
366
+ return change.initial?.[keyPath] ?? null;
367
+ case "inserted":
368
+ case "updated":
369
+ return change.result?.[keyPath] ?? null;
370
+ case "deleted":
371
+ return null;
372
+ default:
373
+ throw new Error("Unsupported type");
280
374
  }
281
- if (changes.length === 0) return;
375
+ };
376
+ const send = (changes) => {
377
+ if (isStopped || changes.length === 0) return;
378
+ currentKey = getKeyAfter(changes.at(-1));
282
379
  callback(null, changes);
380
+ };
381
+ const refetch = () => this.fetch((result) => {
382
+ isReady = true;
383
+ return result;
384
+ });
385
+ let isInitializing = true;
386
+ let hasActionDuringInit = false;
387
+ const initialPromise = refetch().then(
388
+ ({ result, meta, info }) => {
389
+ isInitializing = false;
390
+ currentKey = result?.[keyPath] ?? null;
391
+ if (_includeInitial || hasActionDuringInit) {
392
+ send([{ type: "initial", initial: result, queryInfo: info, meta }]);
393
+ }
394
+ }
395
+ );
396
+ const refetchAsUpdated = () => refetch().then(({ result }) => {
397
+ send([{ type: "updated", result }]);
398
+ });
399
+ const handleAction = (action) => {
400
+ if (isStopped) return void 0;
401
+ if (action.type === "resync") {
402
+ return refetch().then(({ result, meta, info }) => {
403
+ send([{ type: "initial", initial: result, queryInfo: info, meta }]);
404
+ });
405
+ }
406
+ if (!isReady) return void 0;
407
+ if (action.type === "changed") {
408
+ const isConcerned = currentKey !== null && action.keys.includes(currentKey) || action.next.some(testCriteria);
409
+ return isConcerned ? refetchAsUpdated() : void 0;
410
+ }
411
+ const changes = this.computeChanges(action, testCriteria, currentKey);
412
+ if (changes === "refetch") return refetchAsUpdated();
413
+ send(changes);
414
+ return void 0;
415
+ };
416
+ const enqueue = createActionQueue(initialPromise, (error) => {
417
+ if (!isStopped) callback(error, []);
283
418
  });
419
+ const unsubscribe = store.subscribe((action) => {
420
+ if (isInitializing) hasActionDuringInit = true;
421
+ enqueue(() => handleAction(action));
422
+ });
423
+ const stop = () => {
424
+ isStopped = true;
425
+ unsubscribe();
426
+ };
284
427
  return {
285
- stop: unsubscribe,
286
- cancel: unsubscribe,
287
- then: (onFulfilled, onRejected) => promise.then(onFulfilled, onRejected)
428
+ stop,
429
+ cancel: stop,
430
+ then: (onFulfilled, onRejected) => initialPromise.then(onFulfilled, onRejected)
288
431
  };
289
432
  }
290
433
  async createMongoCursor() {