@procedures/mongo 0.52.2 → 0.52.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@procedures/mongo",
4
- "version": "0.52.2",
4
+ "version": "0.52.3",
5
5
  "description": "MongoDB as typed procedures: documents, bulk writes, distinct, explain, collections, indexes, aggregation, change streams and database info, with safe update and delete targets.",
6
6
  "keywords": [
7
7
  "mongodb",
@@ -68,8 +68,8 @@
68
68
  "@mark1russell7/rigor-generators": "^0.1.5",
69
69
  "@mark1russell7/rigor-statistics": "^0.1.9",
70
70
  "@mark1russell7/service": "^0.6.0",
71
- "@procedures/peer": "0.52.2",
72
- "@procedures/store": "0.52.2",
71
+ "@procedures/peer": "0.52.3",
72
+ "@procedures/store": "0.52.3",
73
73
  "@types/node": "^22.0.0",
74
74
  "typescript": "^7.0.2",
75
75
  "vitest": "^5.0.3"
@@ -78,8 +78,8 @@
78
78
  "procedures": "./dist/register.js"
79
79
  },
80
80
  "dependencies": {
81
- "@procedures/client": "0.52.2",
82
- "@procedures/collections": "0.52.2",
81
+ "@procedures/client": "0.52.3",
82
+ "@procedures/collections": "0.52.3",
83
83
  "mongodb": "^7.0.0",
84
84
  "zod": "^4.6.5"
85
85
  },
@@ -559,6 +559,20 @@ export interface RawStorageWrite {
559
559
  /** The result of an adoption: `adopted`, `lost` (the document changed since: another adoption or write won), or `unknown` (no image and no stored `_id` tells the id and the partition). */
560
560
  export type RawAdoption = "adopted" | "lost" | "unknown";
561
561
 
562
+ /**
563
+ * The key of a stored `_id` in a map: its extended JSON. Two object ids with other fields are two
564
+ * keys, and an `ObjectId` and its hex text are two keys too (audit 2026-11-08).
565
+ */
566
+ function storedIdKey(id: unknown): string {
567
+ return BSON.EJSON.stringify({ v: id as never }, { relaxed: false });
568
+ }
569
+
570
+ /** The order of two stored `_id` values of one place: two texts by their text, else by their keys. */
571
+ function idOrder(a: unknown, b: unknown, keyA: string, keyB: string): number {
572
+ const [x, y] = typeof a === "string" && typeof b === "string" ? [a, b] : [keyA, keyB];
573
+ return x < y ? -1 : x > y ? 1 : 0;
574
+ }
575
+
562
576
  /** True when two values have the same BSON. */
563
577
  function sameBson(a: unknown, b: unknown): boolean {
564
578
  try {
@@ -1398,13 +1412,16 @@ export class MongoDocumentStorage<T> implements CollectionStorage<T> {
1398
1412
 
1399
1413
  /** The listed documents with later documents over them, in the order of the entries. A tombstone removes its document. */
1400
1414
  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]));
1415
+ // The key knows the type of the `_id`: before, `String()` gave each object `_id` one key
1416
+ const keyOf = (doc: Document): string => storedIdKey(doc["_id"]);
1417
+ const docs = new Map(listed.map((doc) => [keyOf(doc), doc]));
1403
1418
  for (const doc of later) {
1404
- if (doc[TOMB] === true) docs.delete(idOf(doc));
1405
- else docs.set(idOf(doc), doc);
1419
+ if (doc[TOMB] === true) docs.delete(keyOf(doc));
1420
+ else docs.set(keyOf(doc), doc);
1406
1421
  }
1407
- return [...docs.values()].sort((a, b) => this.placeOf(a) - this.placeOf(b) || (idOf(a) < idOf(b) ? -1 : idOf(a) > idOf(b) ? 1 : 0));
1422
+ return [...docs]
1423
+ .sort(([keyA, a], [keyB, b]) => this.placeOf(a) - this.placeOf(b) || idOrder(a["_id"], b["_id"], keyA, keyB))
1424
+ .map(([, doc]) => doc);
1408
1425
  }
1409
1426
 
1410
1427
  async getChangesSince(since?: string): Promise<StorageChanges<T>> {
@@ -940,6 +940,8 @@ export function watchStorageChanges<T>(storage: MongoDocumentStorage<T>, publish
940
940
  }
941
941
  stream = next;
942
942
  const joiner = eventJoiner();
943
+ /** The token before the document of the last token: the token before the event at fragment 1. */
944
+ let tokenBefore: unknown = token;
943
945
  /** The token at the open: a reopen at the same token is no success, so the backoff does not start again. */
944
946
  const openToken = JSON.stringify(token ?? null);
945
947
  let ended = false;
@@ -975,7 +977,11 @@ export function watchStorageChanges<T>(storage: MongoDocumentStorage<T>, publish
975
977
  next.on("resumeTokenChanged", (resumeToken: unknown) => {
976
978
  if (resumeToken === undefined || resumeToken === null) return;
977
979
  // In the middle of a split event, the token stays before the event: a stream that opens again gets all its fragments
978
- if (!joiner.partial) token = resumeToken;
980
+ if (!joiner.partial) {
981
+ // The driver gives the token of a document before its change: fragment 1 puts the token back (audit 2026-11-08)
982
+ tokenBefore = token;
983
+ token = resumeToken;
984
+ }
979
985
  opened = true;
980
986
  if (JSON.stringify(resumeToken) !== openToken) attempt = 0;
981
987
  markReady();
@@ -988,7 +994,11 @@ export function watchStorageChanges<T>(storage: MongoDocumentStorage<T>, publish
988
994
  live = true;
989
995
  afterQueue();
990
996
  const result = joiner.push(change);
991
- if (result.kind === "part") return;
997
+ if (result.kind === "part") {
998
+ // The first fragment of a split event: the token goes back to the token before the event
999
+ if ((change["splitEvent"] as { fragment?: unknown } | undefined)?.fragment === 1) token = tokenBefore;
1000
+ return;
1001
+ }
992
1002
  if (result.kind === "missing") {
993
1003
  // The stream started after a fragment: the event cannot be read, so the subscribers read the collection in full
994
1004
  token = result.change["_id"];