@procedures/mongo 0.50.0 → 0.52.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.
@@ -69,7 +69,7 @@
69
69
  * it. One case stays on a single server: a process that stops between such a late write and the
70
70
  * move of its documents. Then a reader that passed the old numbers does not get that change.
71
71
  * - A reader can get a write two times, with the same value or a later value. A full answer takes
72
- * its mark before it lists the items, so it can hold a write above its mark, and the next delta
72
+ * its mark after it lists the items, and it can hold a write above its mark, so the next delta
73
73
  * gives the write again. A reader can also read a late write at its old numbers, and then the move
74
74
  * gives the write new numbers and a new version. The second case occurs only for a write that ends
75
75
  * after `writeLease`.
@@ -312,72 +312,45 @@ function hasTransactions(db: Db): Promise<boolean> {
312
312
 
313
313
  /** The most moves of a late write to new numbers. */
314
314
  const MAX_MOVES = 10;
315
-
316
- /** The ends of the work of one collection. These promises do not reject. */
317
- interface WorkQueue {
318
- /** The end of each earlier write and change read. */
319
- all: Promise<unknown>;
320
- /** The end of the last write. */
321
- writes: Promise<unknown>;
322
- }
315
+ /** The most lists of one full result, when a prune or a new epoch passes its marks. */
316
+ const MOST_FULL_TRIES = 3;
323
317
 
324
318
  /**
325
- * The writes and the change reads of each collection: by the client, then by the names of the database and the
326
- * collection. A new `Db` object of the same client has the same queue (roadmap CLI-93). A write goes alone, in the
327
- * order of the calls. A change read goes after each earlier write, next to the other change reads. So a read and a
328
- * write keep the order of their calls, and a read does not wait for a read (roadmap CLI-91).
319
+ * The end of the last write of each collection: by the client, then by the names of the database and the collection.
320
+ * These promises do not reject. A new `Db` object of the same client has the same queue (roadmap CLI-93). A write goes
321
+ * alone, in the order of the calls. A change read is not in this queue (roadmap CLI-102). The mark of a read stops
322
+ * below each write in flight, so a read does not need the end of a write of this process.
329
323
  */
330
- const QUEUES = new WeakMap<object, Map<string, WorkQueue>>();
324
+ const QUEUES = new WeakMap<object, Map<string, Promise<unknown>>>();
331
325
 
332
326
  /** The change reads of each client at one time. A burst of reads leaves the pool to the other operations. */
333
327
  const READS = new WeakMap<object, Semaphore>();
334
328
 
335
- function queueOf(db: Db, name: string): { queues: Map<string, WorkQueue>; collection: string; queue: WorkQueue } {
329
+ /** This function starts a write after each earlier write of the collection. */
330
+ function enqueueWrite<R>(db: Db, name: string, task: () => Promise<R>): Promise<R> {
336
331
  const owner = ownerOf(db);
337
332
  let queues = QUEUES.get(owner);
338
333
  if (!queues) QUEUES.set(owner, (queues = new Map()));
339
334
  const collection = `${db.databaseName}\u0000${name}`;
340
- let queue = queues.get(collection);
341
- if (!queue) queues.set(collection, (queue = { all: Promise.resolve(), writes: Promise.resolve() }));
342
- return { queues, collection, queue };
343
- }
344
-
345
- /** This function drops the queue of a collection when this end is the last of its work. */
346
- function dropWhenLast(queues: Map<string, WorkQueue>, collection: string, queue: WorkQueue, end: Promise<unknown>): void {
347
- void end.then(() => {
348
- if (queue.all === end && queues.get(collection) === queue) queues.delete(collection);
349
- });
350
- }
351
-
352
- const settledOf = (run: Promise<unknown>): Promise<unknown> =>
353
- run.then(
335
+ const run = (queues.get(collection) ?? Promise.resolve()).then(task);
336
+ const settled = run.then(
354
337
  () => undefined,
355
338
  () => undefined
356
339
  );
357
-
358
- /** This function starts a write after each earlier write and change read of the collection. */
359
- function enqueueWrite<R>(db: Db, name: string, task: () => Promise<R>): Promise<R> {
360
- const { queues, collection, queue } = queueOf(db, name);
361
- const run = queue.all.then(task);
362
- const settled = settledOf(run);
363
- queue.all = settled;
364
- queue.writes = settled;
365
- dropWhenLast(queues, collection, queue, settled);
340
+ const known = queues;
341
+ known.set(collection, settled);
342
+ void settled.then(() => {
343
+ if (known.get(collection) === settled) known.delete(collection);
344
+ });
366
345
  return run;
367
346
  }
368
347
 
369
- /** This function starts a change read after each earlier write of the collection, next to the other change reads. */
370
- function enqueueRead<R>(db: Db, name: string, task: () => Promise<R>): Promise<R> {
371
- const { queues, collection, queue } = queueOf(db, name);
348
+ /** This function starts a change read within the share of the client. The read waits for no write. */
349
+ function withReadPermit<R>(db: Db, task: () => Promise<R>): Promise<R> {
372
350
  const owner = ownerOf(db);
373
351
  let reads = READS.get(owner);
374
352
  if (!reads) READS.set(owner, (reads = new Semaphore(poolShare(db, CHANGE_READ_DIVISOR))));
375
- const permits = reads;
376
- const run = queue.writes.then(() => permits.withPermit(task));
377
- const end = Promise.all([queue.all, settledOf(run)]);
378
- queue.all = end;
379
- dropWhenLast(queues, collection, queue, end);
380
- return run;
353
+ return reads.withPermit(task);
381
354
  }
382
355
 
383
356
  /**
@@ -781,7 +754,7 @@ interface Counter {
781
754
  interface Watermark extends Counter {
782
755
  /** The floor of the log of the partition: 0 when the partition has no floor document. */
783
756
  floor: number;
784
- /** True when a pending write holds the watermark below the counter. */
757
+ /** True when the version stops below a pending number. */
785
758
  readonly held: boolean;
786
759
  }
787
760
 
@@ -1390,19 +1363,57 @@ export class MongoDocumentStorage<T> implements CollectionStorage<T> {
1390
1363
  return doc ? seqVersion(doc) : undefined;
1391
1364
  }
1392
1365
 
1366
+ /**
1367
+ * This method gives a full result. An event with values can land between the first mark and the
1368
+ * list, so the first mark is not a version that this list can have. The method takes a second mark
1369
+ * after the list and reads the rows between the two marks. The second mark is the version.
1370
+ *
1371
+ * That read stops at the second mark. A row past the mark keeps the value of the list, and its later
1372
+ * value comes after this result, in order. So the value of an id does not go back at a subscriber.
1373
+ * The cost is one more aggregate for each full result, and two more finds when the mark moved.
1374
+ */
1375
+ private async fullResult(db: Db, items: Collection<Stored>, first: Watermark): Promise<StorageChanges<T>> {
1376
+ const result = (mark: Watermark, docs: readonly Document[]): StorageChanges<T> => ({
1377
+ version: `${mark.epoch}.${mark.seq}`,
1378
+ full: true,
1379
+ changed: this.decodeAll(docs),
1380
+ deleted: [],
1381
+ ...(mark.held ? { held: true } : {}),
1382
+ });
1383
+ let mark = first;
1384
+ for (let tries = 1; ; tries += 1) {
1385
+ const listed = await items.find(this.liveFilter()).sort({ [ORD]: 1, _id: 1 }).toArray();
1386
+ const after = await this.watermark(db);
1387
+ if (after.epoch === mark.epoch) {
1388
+ if (after.seq <= mark.seq) return result(mark, listed);
1389
+ const docs = await items.find({ ...this.partitionFilter(), [SEQ]: { $gt: mark.seq, $lte: after.seq } } as Filter<Stored>).toArray();
1390
+ // A prune raises the floor before its delete, so a query that missed a tombstone sees the new floor here (roadmap CLI-93)
1391
+ if (mark.seq >= (await this.floorOf(db))) return result(after, this.withLaterDocs(listed, docs));
1392
+ }
1393
+ // The log does not tell the changes since this mark. Each later read from this version, an event too, gets a full result
1394
+ if (tries === MOST_FULL_TRIES) return result(mark, listed);
1395
+ mark = after;
1396
+ }
1397
+ }
1398
+
1399
+ /** The listed documents with later documents over them, in the order of the entries. A tombstone removes its document. */
1400
+ private withLaterDocs(listed: readonly Document[], later: readonly Document[]): Document[] {
1401
+ const idOf = (doc: Document): string => String(doc["_id"]);
1402
+ const docs = new Map(listed.map((doc) => [idOf(doc), doc]));
1403
+ for (const doc of later) {
1404
+ if (doc[TOMB] === true) docs.delete(idOf(doc));
1405
+ else docs.set(idOf(doc), doc);
1406
+ }
1407
+ return [...docs.values()].sort((a, b) => this.placeOf(a) - this.placeOf(b) || (idOf(a) < idOf(b) ? -1 : idOf(a) > idOf(b) ? 1 : 0));
1408
+ }
1409
+
1393
1410
  async getChangesSince(since?: string): Promise<StorageChanges<T>> {
1394
1411
  const { db, items } = await this.collection();
1395
- return enqueueRead(db, this.collectionName, async () => {
1412
+ return withReadPermit(db, async () => {
1396
1413
  const counter = await this.watermark(db);
1397
1414
  const version = `${counter.epoch}.${counter.seq}`;
1398
1415
  const held = counter.held ? { held: true } : {};
1399
- const full = async (): Promise<StorageChanges<T>> => ({
1400
- version,
1401
- full: true,
1402
- changed: this.decodeAll(await items.find(this.liveFilter()).sort({ [ORD]: 1, _id: 1 }).toArray()),
1403
- deleted: [],
1404
- ...held,
1405
- });
1416
+ const full = (): Promise<StorageChanges<T>> => this.fullResult(db, items, counter);
1406
1417
  const match = since === undefined ? null : /^([^.]+)\.(\d+)$/.exec(since);
1407
1418
  if (!match || match[1] !== counter.epoch) return full();
1408
1419
  const from = Number(match[2]);