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 +20 -2
- package/dist/definitions/MongoQueryCollection.d.ts +14 -0
- package/dist/definitions/MongoQueryCollection.d.ts.map +1 -1
- package/dist/definitions/MongoQuerySingleItem.d.ts +4 -0
- package/dist/definitions/MongoQuerySingleItem.d.ts.map +1 -1
- package/dist/definitions/MongoQuerySubscription.test.d.ts +2 -0
- package/dist/definitions/MongoQuerySubscription.test.d.ts.map +1 -0
- package/dist/definitions/createActionQueue.d.ts +9 -0
- package/dist/definitions/createActionQueue.d.ts.map +1 -0
- package/dist/definitions/createActionQueue.test.d.ts +2 -0
- package/dist/definitions/createActionQueue.test.d.ts.map +1 -0
- package/dist/index-node.mjs +245 -102
- package/dist/index-node.mjs.map +1 -1
- package/package.json +2 -2
- package/src/MongoQueryCollection.ts +195 -97
- package/src/MongoQuerySingleItem.test.ts +65 -26
- package/src/MongoQuerySingleItem.ts +103 -41
- package/src/MongoQuerySubscription.test.ts +552 -0
- package/src/SubscribeStore.integration.test.ts +1 -1
- package/src/createActionQueue.test.ts +85 -0
- package/src/createActionQueue.ts +44 -0
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
|
|
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,
|
|
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,
|
|
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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"createActionQueue.test.d.ts","sourceRoot":"","sources":["../../src/createActionQueue.test.ts"],"names":[],"mappings":""}
|
package/dist/index-node.mjs
CHANGED
|
@@ -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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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 (
|
|
156
|
-
|
|
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
|
|
160
|
-
cancel:
|
|
161
|
-
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
|
-
|
|
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
|
|
226
|
-
if (
|
|
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
|
-
([
|
|
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
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
|
286
|
-
cancel:
|
|
287
|
-
then: (onFulfilled, onRejected) =>
|
|
428
|
+
stop,
|
|
429
|
+
cancel: stop,
|
|
430
|
+
then: (onFulfilled, onRejected) => initialPromise.then(onFulfilled, onRejected)
|
|
288
431
|
};
|
|
289
432
|
}
|
|
290
433
|
async createMongoCursor() {
|