liwi-subscribe-store 13.3.3 → 14.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.
@@ -1,5 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
+ import { DeletedAfterUpdateError, NotFoundError } from "liwi-store";
3
4
  import type {
4
5
  AbstractConnection,
5
6
  SubscribableStore,
@@ -36,18 +37,15 @@ const created = new Date("2020-01-01");
36
37
  const updated = new Date("2021-01-01");
37
38
 
38
39
  const createSubscribeStore = (
39
- upsertResult: UpsertResult<TestModel>,
40
+ store: Partial<TestStore>,
40
41
  ): {
41
42
  subscribeStore: TestSubscribeStore;
42
43
  actions: Actions<TestModel>[];
43
44
  } => {
44
- const store: Pick<TestStore, "keyPath" | "upsertOneWithInfo"> = {
45
+ const subscribeStore: TestSubscribeStore = new SubscribeStore({
45
46
  keyPath: "_id",
46
- upsertOneWithInfo: () => Promise.resolve(upsertResult),
47
- };
48
- const subscribeStore: TestSubscribeStore = new SubscribeStore(
49
- store as TestStore,
50
- );
47
+ ...store,
48
+ } as TestStore);
51
49
  const actions: Actions<TestModel>[] = [];
52
50
  subscribeStore.subscribe((action) => actions.push(action));
53
51
  return { subscribeStore, actions };
@@ -61,7 +59,9 @@ describe("SubscribeStore upsertOneWithInfo", () => {
61
59
  inserted: true,
62
60
  object,
63
61
  };
64
- const { subscribeStore, actions } = createSubscribeStore(upsertResult);
62
+ const { subscribeStore, actions } = createSubscribeStore({
63
+ upsertOneWithInfo: () => Promise.resolve(upsertResult),
64
+ });
65
65
 
66
66
  const result = await subscribeStore.upsertOneWithInfo(object);
67
67
 
@@ -83,7 +83,9 @@ describe("SubscribeStore upsertOneWithInfo", () => {
83
83
  object,
84
84
  prev,
85
85
  };
86
- const { subscribeStore, actions } = createSubscribeStore(upsertResult);
86
+ const { subscribeStore, actions } = createSubscribeStore({
87
+ upsertOneWithInfo: () => Promise.resolve(upsertResult),
88
+ });
87
89
 
88
90
  const result = await subscribeStore.upsertOneWithInfo({
89
91
  _id: "1",
@@ -95,3 +97,514 @@ describe("SubscribeStore upsertOneWithInfo", () => {
95
97
  assert.deepEqual(actions, [{ type: "updated", changes: [[prev, object]] }]);
96
98
  });
97
99
  });
100
+
101
+ const stored: TestModel = { _id: "1", label: "stored", created, updated };
102
+ const staleCopy: TestModel = { _id: "1", label: "stale", created, updated };
103
+ const next: TestModel = { _id: "1", label: "next", created, updated };
104
+
105
+ const createCursorStore = (models: TestModel[]): Pick<TestStore, "cursor"> => ({
106
+ cursor: (() =>
107
+ Promise.resolve({
108
+ forEach: async (callback: (model: TestModel) => Promise<void>) => {
109
+ for (const model of models) await callback(model);
110
+ },
111
+ })) as unknown as TestStore["cursor"],
112
+ });
113
+
114
+ describe("SubscribeStore over a store with …WithInfo methods", () => {
115
+ it("emits the stored prev, not the caller's copy, on partialUpdateOne", async () => {
116
+ const calls: unknown[][] = [];
117
+ const { subscribeStore, actions } = createSubscribeStore({
118
+ partialUpdateByKeyWithInfo: (...args) => {
119
+ calls.push(args);
120
+ return Promise.resolve({ prev: stored, next });
121
+ },
122
+ });
123
+
124
+ const result = await subscribeStore.partialUpdateOne(staleCopy, {
125
+ $set: { label: "next" },
126
+ });
127
+
128
+ assert.equal(result, next);
129
+ assert.deepEqual(calls, [
130
+ ["1", { $set: { label: "next" } }, undefined, undefined],
131
+ ]);
132
+ assert.deepEqual(actions, [{ type: "updated", changes: [[stored, next]] }]);
133
+ });
134
+
135
+ it("throws NotFoundError and emits nothing on partialUpdateByKey of a missing key", async () => {
136
+ const { subscribeStore, actions } = createSubscribeStore({
137
+ partialUpdateByKeyWithInfo: () => Promise.resolve(undefined),
138
+ });
139
+
140
+ await assert.rejects(
141
+ subscribeStore.partialUpdateByKey("1", { $set: { label: "next" } }),
142
+ NotFoundError,
143
+ );
144
+ assert.deepEqual(actions, []);
145
+ });
146
+
147
+ it("throws DeletedAfterUpdateError and emits nothing when the document was deleted after the update", async () => {
148
+ const { subscribeStore, actions } = createSubscribeStore({
149
+ partialUpdateByKeyWithInfo: () =>
150
+ Promise.resolve({ prev: stored, next: undefined }),
151
+ });
152
+
153
+ const error: unknown = await subscribeStore
154
+ .partialUpdateByKey("1", { $set: { label: "next" } })
155
+ .catch((error_: unknown) => error_);
156
+
157
+ assert.ok(error instanceof DeletedAfterUpdateError);
158
+ assert.ok(error instanceof NotFoundError);
159
+ assert.deepEqual(actions, []);
160
+ });
161
+
162
+ it("skips in partialUpdateMany a document deleted after its update", async () => {
163
+ const { subscribeStore, actions } = createSubscribeStore({
164
+ ...createCursorStore([staleCopy, { ...staleCopy, _id: "2" }]),
165
+ partialUpdateByKeyWithInfo: (key) =>
166
+ Promise.resolve(
167
+ key === "1"
168
+ ? { prev: stored, next }
169
+ : { prev: { ...stored, _id: "2" }, next: undefined },
170
+ ),
171
+ });
172
+
173
+ await subscribeStore.partialUpdateMany(
174
+ { label: "stale" },
175
+ { $set: { label: "next" } },
176
+ );
177
+
178
+ assert.deepEqual(actions, [{ type: "updated", changes: [[stored, next]] }]);
179
+ });
180
+
181
+ it("emits the stored prev on replaceOne", async () => {
182
+ const { subscribeStore, actions } = createSubscribeStore({
183
+ replaceOneWithInfo: () => Promise.resolve({ prev: stored, next }),
184
+ });
185
+
186
+ const result = await subscribeStore.replaceOne(staleCopy);
187
+
188
+ assert.equal(result, next);
189
+ assert.deepEqual(actions, [{ type: "updated", changes: [[stored, next]] }]);
190
+ });
191
+
192
+ it("throws NotFoundError and emits nothing on replaceOne of a missing key", async () => {
193
+ const { subscribeStore, actions } = createSubscribeStore({
194
+ replaceOneWithInfo: () => Promise.resolve(undefined),
195
+ });
196
+
197
+ await assert.rejects(subscribeStore.replaceOne(staleCopy), NotFoundError);
198
+ assert.deepEqual(actions, []);
199
+ });
200
+
201
+ it("emits one change per object on replaceSeveral", async () => {
202
+ const stored2: TestModel = { ...stored, _id: "2" };
203
+ const next2: TestModel = { ...next, _id: "2" };
204
+ const { subscribeStore, actions } = createSubscribeStore({
205
+ replaceOneWithInfo: (object) =>
206
+ Promise.resolve(
207
+ object._id === "1"
208
+ ? { prev: stored, next }
209
+ : { prev: stored2, next: next2 },
210
+ ),
211
+ });
212
+
213
+ const result = await subscribeStore.replaceSeveral([
214
+ staleCopy,
215
+ { ...staleCopy, _id: "2" },
216
+ ]);
217
+
218
+ assert.deepEqual(result, [next, next2]);
219
+ assert.deepEqual(actions, [
220
+ {
221
+ type: "updated",
222
+ changes: [
223
+ [stored, next],
224
+ [stored2, next2],
225
+ ],
226
+ },
227
+ ]);
228
+ });
229
+
230
+ it("emits the replaces that succeeded before rethrowing on replaceSeveral", async () => {
231
+ const { subscribeStore, actions } = createSubscribeStore({
232
+ replaceOneWithInfo: (object) =>
233
+ Promise.resolve(
234
+ object._id === "1" ? { prev: stored, next } : undefined,
235
+ ),
236
+ });
237
+
238
+ await assert.rejects(
239
+ subscribeStore.replaceSeveral([staleCopy, { ...staleCopy, _id: "2" }]),
240
+ NotFoundError,
241
+ );
242
+ assert.deepEqual(actions, [{ type: "updated", changes: [[stored, next]] }]);
243
+ });
244
+
245
+ it("emits the updates done before a failure on partialUpdateMany", async () => {
246
+ const failure = new Error("write failed");
247
+ const { subscribeStore, actions } = createSubscribeStore({
248
+ ...createCursorStore([staleCopy, { ...staleCopy, _id: "2" }]),
249
+ partialUpdateByKeyWithInfo: (key) =>
250
+ key === "1"
251
+ ? Promise.resolve({ prev: stored, next })
252
+ : Promise.reject(failure),
253
+ });
254
+
255
+ await assert.rejects(
256
+ subscribeStore.partialUpdateMany(
257
+ { label: "stale" },
258
+ { $set: { label: "next" } },
259
+ ),
260
+ failure,
261
+ );
262
+ assert.deepEqual(actions, [{ type: "updated", changes: [[stored, next]] }]);
263
+ });
264
+
265
+ it("emits the stored prev on deleteOne", async () => {
266
+ const { subscribeStore, actions } = createSubscribeStore({
267
+ deleteByKeyWithInfo: () => Promise.resolve({ prev: stored }),
268
+ });
269
+
270
+ await subscribeStore.deleteOne(staleCopy);
271
+
272
+ assert.deepEqual(actions, [{ type: "deleted", prev: [stored] }]);
273
+ });
274
+
275
+ it("resolves and emits nothing on deleteByKey of a missing key", async () => {
276
+ const { subscribeStore, actions } = createSubscribeStore({
277
+ deleteByKeyWithInfo: () => Promise.resolve(undefined),
278
+ });
279
+
280
+ await subscribeStore.deleteByKey("1");
281
+
282
+ assert.deepEqual(actions, []);
283
+ });
284
+
285
+ it("passes the partial update options to every document of partialUpdateMany", async () => {
286
+ const options: unknown[] = [];
287
+ const { subscribeStore } = createSubscribeStore({
288
+ ...createCursorStore([staleCopy, { ...staleCopy, _id: "2" }]),
289
+ partialUpdateByKeyWithInfo: (key, update, criteria, option) => {
290
+ options.push(option);
291
+ return Promise.resolve({ prev: stored, next });
292
+ },
293
+ });
294
+
295
+ await subscribeStore.partialUpdateMany(
296
+ { label: "stale" },
297
+ { $set: { label: "next" } },
298
+ { touchUpdated: false },
299
+ );
300
+
301
+ assert.deepEqual(options, [
302
+ { touchUpdated: false },
303
+ { touchUpdated: false },
304
+ ]);
305
+ });
306
+
307
+ it("emits only the documents partialUpdateMany actually modified", async () => {
308
+ const { subscribeStore, actions } = createSubscribeStore({
309
+ ...createCursorStore([staleCopy, { ...staleCopy, _id: "2" }]),
310
+ partialUpdateByKeyWithInfo: (key) =>
311
+ Promise.resolve(key === "1" ? { prev: stored, next } : undefined),
312
+ });
313
+
314
+ await subscribeStore.partialUpdateMany(
315
+ { label: "stale" },
316
+ { $set: { label: "next" } },
317
+ );
318
+
319
+ assert.deepEqual(actions, [{ type: "updated", changes: [[stored, next]] }]);
320
+ });
321
+
322
+ it("emits nothing when partialUpdateMany modifies no document", async () => {
323
+ const { subscribeStore, actions } = createSubscribeStore({
324
+ ...createCursorStore([staleCopy]),
325
+ partialUpdateByKeyWithInfo: () => Promise.resolve(undefined),
326
+ });
327
+
328
+ await subscribeStore.partialUpdateMany(
329
+ { label: "stale" },
330
+ { $set: { label: "next" } },
331
+ );
332
+
333
+ assert.deepEqual(actions, []);
334
+ });
335
+
336
+ it("emits nothing when every replace of replaceSeveral fails", async () => {
337
+ const { subscribeStore, actions } = createSubscribeStore({
338
+ replaceOneWithInfo: () => Promise.resolve(undefined),
339
+ });
340
+
341
+ await assert.rejects(
342
+ subscribeStore.replaceSeveral([staleCopy]),
343
+ NotFoundError,
344
+ );
345
+ assert.deepEqual(actions, []);
346
+ });
347
+ });
348
+
349
+ describe("SubscribeStore over a store without …WithInfo methods", () => {
350
+ it("reads prev before a partial update", async () => {
351
+ const { subscribeStore, actions } = createSubscribeStore({
352
+ findOne: () => Promise.resolve(stored),
353
+ partialUpdateByKey: () => Promise.resolve(next),
354
+ });
355
+
356
+ const result = await subscribeStore.partialUpdateOne(staleCopy, {
357
+ $set: { label: "next" },
358
+ });
359
+
360
+ assert.equal(result, next);
361
+ assert.deepEqual(actions, [{ type: "updated", changes: [[stored, next]] }]);
362
+ });
363
+
364
+ it("reads prev before a replace, and throws NotFoundError on a missing key", async () => {
365
+ const replaced: TestModel[] = [];
366
+ const { subscribeStore, actions } = createSubscribeStore({
367
+ findByKey: (key) => Promise.resolve(key === "1" ? stored : undefined),
368
+ replaceOne: (object) => {
369
+ replaced.push(object);
370
+ return Promise.resolve(object);
371
+ },
372
+ });
373
+
374
+ const result = await subscribeStore.replaceOne(staleCopy);
375
+ await assert.rejects(
376
+ subscribeStore.replaceOne({ ...staleCopy, _id: "2" }),
377
+ NotFoundError,
378
+ );
379
+
380
+ assert.equal(result, staleCopy);
381
+ assert.deepEqual(replaced, [staleCopy]);
382
+ assert.deepEqual(actions, [
383
+ { type: "updated", changes: [[stored, staleCopy]] },
384
+ ]);
385
+ });
386
+
387
+ it("throws NotFoundError on a partial update of a missing key", async () => {
388
+ const { subscribeStore, actions } = createSubscribeStore({
389
+ findOne: () => Promise.resolve(undefined),
390
+ });
391
+
392
+ await assert.rejects(
393
+ subscribeStore.partialUpdateByKey("1", { $set: { label: "next" } }),
394
+ NotFoundError,
395
+ );
396
+ assert.deepEqual(actions, []);
397
+ });
398
+
399
+ it("reads prev before a delete, and skips a missing key", async () => {
400
+ const deletedKeys: string[] = [];
401
+ const { subscribeStore, actions } = createSubscribeStore({
402
+ findByKey: (key) => Promise.resolve(key === "1" ? stored : undefined),
403
+ deleteByKey: (key) => {
404
+ deletedKeys.push(key);
405
+ return Promise.resolve();
406
+ },
407
+ });
408
+
409
+ await subscribeStore.deleteOne(staleCopy);
410
+ await subscribeStore.deleteByKey("2");
411
+
412
+ assert.deepEqual(deletedKeys, ["1"]);
413
+ assert.deepEqual(actions, [{ type: "deleted", prev: [stored] }]);
414
+ });
415
+
416
+ it("reads all documents then deletes once on deleteMany", async () => {
417
+ const deletedCriteria: unknown[] = [];
418
+ const { subscribeStore, actions } = createSubscribeStore({
419
+ cursor: (() =>
420
+ Promise.resolve({
421
+ toArray: () => Promise.resolve([stored]),
422
+ })) as unknown as TestStore["cursor"],
423
+ deleteMany: (criteria) => {
424
+ deletedCriteria.push(criteria);
425
+ return Promise.resolve();
426
+ },
427
+ });
428
+
429
+ await subscribeStore.deleteMany({ label: "stored" });
430
+
431
+ assert.deepEqual(deletedCriteria, [{ label: "stored" }]);
432
+ assert.deepEqual(actions, [{ type: "deleted", prev: [stored] }]);
433
+ });
434
+ });
435
+
436
+ describe("SubscribeStore deleteMany over a store with deleteByKeyWithInfo", () => {
437
+ it("emits exactly the documents deleted, re-checking the criteria", async () => {
438
+ const calls: unknown[][] = [];
439
+ const { subscribeStore, actions } = createSubscribeStore({
440
+ ...createCursorStore([staleCopy, { ...staleCopy, _id: "2" }]),
441
+ deleteByKeyWithInfo: (...args) => {
442
+ calls.push(args);
443
+ return Promise.resolve(args[0] === "1" ? { prev: stored } : undefined);
444
+ },
445
+ });
446
+
447
+ await subscribeStore.deleteMany({ label: "stale" });
448
+
449
+ assert.deepEqual(calls, [
450
+ ["1", { label: "stale" }],
451
+ ["2", { label: "stale" }],
452
+ ]);
453
+ assert.deepEqual(actions, [{ type: "deleted", prev: [stored] }]);
454
+ });
455
+
456
+ it("emits the deletes done before a failure", async () => {
457
+ const failure = new Error("delete failed");
458
+ const { subscribeStore, actions } = createSubscribeStore({
459
+ ...createCursorStore([staleCopy, { ...staleCopy, _id: "2" }]),
460
+ deleteByKeyWithInfo: (key) =>
461
+ key === "1"
462
+ ? Promise.resolve({ prev: stored })
463
+ : Promise.reject(failure),
464
+ });
465
+
466
+ await assert.rejects(
467
+ subscribeStore.deleteMany({ label: "stale" }),
468
+ failure,
469
+ );
470
+ assert.deepEqual(actions, [{ type: "deleted", prev: [stored] }]);
471
+ });
472
+
473
+ it("emits nothing when no document is deleted", async () => {
474
+ const { subscribeStore, actions } = createSubscribeStore({
475
+ ...createCursorStore([staleCopy]),
476
+ deleteByKeyWithInfo: () => Promise.resolve(undefined),
477
+ });
478
+
479
+ await subscribeStore.deleteMany({ label: "stale" });
480
+
481
+ assert.deepEqual(actions, []);
482
+ });
483
+ });
484
+
485
+ describe("SubscribeStore bulk writes with listeners", () => {
486
+ it("reads every key before the first write of partialUpdateMany", async () => {
487
+ const events: string[] = [];
488
+ const { subscribeStore } = createSubscribeStore({
489
+ cursor: (() =>
490
+ Promise.resolve({
491
+ forEach: async (
492
+ callback: (model: TestModel) => Promise<void> | void,
493
+ ) => {
494
+ for (const _id of ["1", "2"]) {
495
+ events.push(`read ${_id}`);
496
+ await callback({ ...staleCopy, _id });
497
+ }
498
+ },
499
+ })) as unknown as TestStore["cursor"],
500
+ partialUpdateByKeyWithInfo: (key) => {
501
+ events.push(`write ${key}`);
502
+ return Promise.resolve({ prev: stored, next });
503
+ },
504
+ });
505
+
506
+ await subscribeStore.partialUpdateMany(
507
+ { label: "stale" },
508
+ { $set: { label: "next" } },
509
+ );
510
+
511
+ assert.deepEqual(events, ["read 1", "read 2", "write 1", "write 2"]);
512
+ });
513
+ });
514
+
515
+ describe("SubscribeStore without listeners", () => {
516
+ const createUnsubscribedStore = (
517
+ store: Partial<TestStore>,
518
+ ): TestSubscribeStore =>
519
+ new SubscribeStore({ keyPath: "_id", ...store } as TestStore);
520
+
521
+ const unexpected = (): Promise<never> =>
522
+ Promise.reject(new Error("per-document path used without listeners"));
523
+
524
+ it("delegates partial updates to the wrapped store", async () => {
525
+ const calls: unknown[][] = [];
526
+ const subscribeStore = createUnsubscribedStore({
527
+ partialUpdateByKey: (...args) => {
528
+ calls.push(["partialUpdateByKey", ...args]);
529
+ return Promise.resolve(next);
530
+ },
531
+ partialUpdateMany: (...args) => {
532
+ calls.push(["partialUpdateMany", ...args]);
533
+ return Promise.resolve();
534
+ },
535
+ partialUpdateByKeyWithInfo: unexpected,
536
+ cursor: unexpected,
537
+ });
538
+
539
+ const result = await subscribeStore.partialUpdateOne(
540
+ staleCopy,
541
+ { $set: { label: "next" } },
542
+ { touchUpdated: false },
543
+ );
544
+ await subscribeStore.partialUpdateMany(
545
+ { label: "stale" },
546
+ { $set: { label: "next" } },
547
+ { touchUpdated: false },
548
+ );
549
+
550
+ assert.equal(result, next);
551
+ assert.deepEqual(calls, [
552
+ [
553
+ "partialUpdateByKey",
554
+ "1",
555
+ { $set: { label: "next" } },
556
+ undefined,
557
+ { touchUpdated: false },
558
+ ],
559
+ [
560
+ "partialUpdateMany",
561
+ { label: "stale" },
562
+ { $set: { label: "next" } },
563
+ { touchUpdated: false },
564
+ ],
565
+ ]);
566
+ });
567
+
568
+ it("delegates replaces to the wrapped store", async () => {
569
+ const replaced: unknown[] = [];
570
+ const subscribeStore = createUnsubscribedStore({
571
+ replaceOne: (object) => {
572
+ replaced.push(object);
573
+ return Promise.resolve(next);
574
+ },
575
+ replaceSeveral: (objects) => {
576
+ replaced.push(objects);
577
+ return Promise.resolve([next]);
578
+ },
579
+ replaceOneWithInfo: unexpected,
580
+ });
581
+
582
+ assert.equal(await subscribeStore.replaceOne(staleCopy), next);
583
+ assert.deepEqual(await subscribeStore.replaceSeveral([staleCopy]), [next]);
584
+ assert.deepEqual(replaced, [staleCopy, [staleCopy]]);
585
+ });
586
+
587
+ it("delegates deletes to the wrapped store", async () => {
588
+ const calls: unknown[][] = [];
589
+ const subscribeStore = createUnsubscribedStore({
590
+ deleteByKey: (...args) => {
591
+ calls.push(["deleteByKey", ...args]);
592
+ return Promise.resolve();
593
+ },
594
+ deleteMany: (...args) => {
595
+ calls.push(["deleteMany", ...args]);
596
+ return Promise.resolve();
597
+ },
598
+ deleteByKeyWithInfo: unexpected,
599
+ cursor: unexpected,
600
+ });
601
+
602
+ await subscribeStore.deleteOne(staleCopy);
603
+ await subscribeStore.deleteMany({ label: "stale" });
604
+
605
+ assert.deepEqual(calls, [
606
+ ["deleteByKey", "1", undefined],
607
+ ["deleteMany", { label: "stale" }],
608
+ ]);
609
+ });
610
+ });