@procedures/mongo 0.35.0 → 0.36.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +27 -0
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/index.d.ts +3 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +3 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/module.d.ts +2 -2
  9. package/dist/module.d.ts.map +1 -1
  10. package/dist/module.js +4 -5
  11. package/dist/module.js.map +1 -1
  12. package/dist/procedures/input-schemas.d.ts.map +1 -1
  13. package/dist/procedures/input-schemas.js +29 -2
  14. package/dist/procedures/input-schemas.js.map +1 -1
  15. package/dist/storage/document-storage.d.ts +162 -3
  16. package/dist/storage/document-storage.d.ts.map +1 -1
  17. package/dist/storage/document-storage.js +325 -7
  18. package/dist/storage/document-storage.js.map +1 -1
  19. package/dist/storage/index.d.ts +3 -1
  20. package/dist/storage/index.d.ts.map +1 -1
  21. package/dist/storage/index.js +3 -1
  22. package/dist/storage/index.js.map +1 -1
  23. package/dist/storage/materializer.d.ts.map +1 -1
  24. package/dist/storage/materializer.js +7 -0
  25. package/dist/storage/materializer.js.map +1 -1
  26. package/dist/storage/own-writes.d.ts +51 -0
  27. package/dist/storage/own-writes.d.ts.map +1 -0
  28. package/dist/storage/own-writes.js +130 -0
  29. package/dist/storage/own-writes.js.map +1 -0
  30. package/dist/storage/storage-hints.d.ts +230 -0
  31. package/dist/storage/storage-hints.d.ts.map +1 -0
  32. package/dist/storage/storage-hints.js +876 -0
  33. package/dist/storage/storage-hints.js.map +1 -0
  34. package/package.json +3 -3
  35. package/src/index.ts +4 -0
  36. package/src/module.ts +5 -5
  37. package/src/procedures/input-schemas.ts +29 -2
  38. package/src/storage/document-storage.ts +394 -7
  39. package/src/storage/index.ts +4 -0
  40. package/src/storage/materializer.ts +7 -0
  41. package/src/storage/own-writes.ts +157 -0
  42. package/src/storage/storage-hints.ts +971 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * The writes of this process (roadmap CLI-77). Each write of a `MongoDocumentStorage` takes
3
+ * numbers of the sequence of its collection (`_seq`). While a watcher of the collection
4
+ * (`watchStorageChanges`) tracks the collection, the storage notes here each number that it takes,
5
+ * and the stored `_id` of each tombstone that it prunes. The watcher then knows the change events
6
+ * of the writes of this process.
7
+ *
8
+ * - The numbers: the collection procedures of this process publish the events of these writes
9
+ * already, so a watcher with `ownWrites: "skip"` skips them.
10
+ * - The pruned tombstones: the delete of a pruned tombstone deletes no item. Without the image
11
+ * before the change, the event of the delete has only the stored `_id`, so the watcher reads it
12
+ * here, and skips the event.
13
+ * - Each watcher has its own tracker. A tracker takes each noted entry once: the event of the
14
+ * write takes it. A later event with the same `_seq` (for example a raw `$set` that keeps the
15
+ * field) is not of this process, and the watcher reads it as a raw write.
16
+ * - The storage notes entries only while a tracker of the collection is open, so a process without
17
+ * a watcher keeps nothing.
18
+ * - The trackers of a collection are by the `MongoClient` of the database and the names of the
19
+ * database and of the collection: two `Db` objects of one database share them.
20
+ * - An entry without an event (for example of an aborted transaction) goes after
21
+ * `OWN_WRITE_TTL_MS` on the clock of the tracker, and a tracker keeps at most `MAX_OWN_WRITES`
22
+ * entries of each kind: the oldest go first. An entry that goes too early makes a second hint at
23
+ * most, never a lost hint.
24
+ * - The numbers of an adoption of a raw write (D39) are never noted: the watcher publishes the
25
+ * hint of the adoption.
26
+ */
27
+ import { BSON, type Db } from "mongodb";
28
+ import type { Clock } from "@procedures/client";
29
+
30
+ /** The longest time that a noted entry waits for its change event: 5 minutes. */
31
+ const OWN_WRITE_TTL_MS = 5 * 60_000;
32
+
33
+ /** The most noted entries of each kind of one tracker. */
34
+ const MAX_OWN_WRITES = 100_000;
35
+
36
+ /** The noted writes of one watcher of one collection. */
37
+ export interface OwnNumbers {
38
+ /** True when a write of this process noted the number and no event took it yet. The number is then taken. */
39
+ take(seq: number): boolean;
40
+ /** True when this process pruned the tombstone of the stored `_id` and no event took it yet. The entry is then taken. */
41
+ takePruned(storedId: unknown): boolean;
42
+ /** This method ends the tracking: the storage notes no more entries for it. */
43
+ close(): void;
44
+ }
45
+
46
+ /** The options of `trackOwnNumbers()`. */
47
+ export interface TrackOwnNumbersOptions {
48
+ /** False: the tracker keeps only the pruned tombstones, not the numbers (default true). */
49
+ readonly numbers?: boolean | undefined;
50
+ /** The clock of the time limit of the entries (default `Date.now`). */
51
+ readonly clock?: Pick<Clock, "now"> | undefined;
52
+ }
53
+
54
+ interface Tracker {
55
+ readonly numbers: Map<number, number> | undefined;
56
+ readonly pruned: Map<string, number>;
57
+ readonly now: () => number;
58
+ }
59
+
60
+ /** The trackers of each collection: by the client (else the `Db` object), then by the names of the database and the collection. */
61
+ const TRACKERS = new WeakMap<object, Map<string, Set<Tracker>>>();
62
+
63
+ function ownerOf(db: Db): object {
64
+ const client = (db as { client?: unknown }).client;
65
+ return typeof client === "object" && client !== null ? client : db;
66
+ }
67
+
68
+ function nameOf(db: Db, collection: string): string {
69
+ return `${db.databaseName}\u0000${collection}`;
70
+ }
71
+
72
+ function trackersOf(db: Db, collection: string): Set<Tracker> | undefined {
73
+ return TRACKERS.get(ownerOf(db))?.get(nameOf(db, collection));
74
+ }
75
+
76
+ /** The key of a stored `_id` in a tracker: its extended JSON, so an `ObjectId` and a text never meet. */
77
+ function idKey(storedId: unknown): string {
78
+ return BSON.EJSON.stringify({ v: storedId as never }, { relaxed: false });
79
+ }
80
+
81
+ /** This function notes some keys in a map with a time limit, and drops the old ones. */
82
+ function noteAll<K>(map: Map<K, number>, keys: Iterable<K>, now: number): void {
83
+ for (const key of keys) {
84
+ // A key noted again (an aborted transaction gives its numbers again) moves to the end
85
+ map.delete(key);
86
+ map.set(key, now + OWN_WRITE_TTL_MS);
87
+ }
88
+ // The map keeps the order of the notes: the oldest keys are first
89
+ for (const [key, until] of map) {
90
+ if (until > now && map.size <= MAX_OWN_WRITES) break;
91
+ map.delete(key);
92
+ }
93
+ }
94
+
95
+ /** This function starts a tracker of the writes of this process in a collection. */
96
+ export function trackOwnNumbers(db: Db, collection: string, options: TrackOwnNumbersOptions = {}): OwnNumbers {
97
+ const owner = ownerOf(db);
98
+ const name = nameOf(db, collection);
99
+ const clock = options.clock;
100
+ const tracker: Tracker = {
101
+ numbers: options.numbers === false ? undefined : new Map(),
102
+ pruned: new Map(),
103
+ now: clock ? () => clock.now() : Date.now,
104
+ };
105
+ let byName = TRACKERS.get(owner);
106
+ if (!byName) {
107
+ byName = new Map();
108
+ TRACKERS.set(owner, byName);
109
+ }
110
+ let trackers = byName.get(name);
111
+ if (!trackers) {
112
+ trackers = new Set();
113
+ byName.set(name, trackers);
114
+ }
115
+ trackers.add(tracker);
116
+ const live = (map: Map<unknown, number>, key: unknown): boolean => {
117
+ const until = map.get(key);
118
+ if (until === undefined) return false;
119
+ map.delete(key);
120
+ return until > tracker.now();
121
+ };
122
+ return {
123
+ take(seq: number): boolean {
124
+ return tracker.numbers !== undefined && live(tracker.numbers as Map<unknown, number>, seq);
125
+ },
126
+ takePruned(storedId: unknown): boolean {
127
+ return live(tracker.pruned as Map<unknown, number>, idKey(storedId));
128
+ },
129
+ close(): void {
130
+ const set = TRACKERS.get(owner)?.get(name);
131
+ set?.delete(tracker);
132
+ if (set?.size === 0) TRACKERS.get(owner)?.delete(name);
133
+ tracker.numbers?.clear();
134
+ tracker.pruned.clear();
135
+ },
136
+ };
137
+ }
138
+
139
+ /** This function notes the numbers `first` to `first + count - 1` of a write of this process, for each open tracker of the collection. */
140
+ export function noteOwnNumbers(db: Db, collection: string, first: number, count: number): void {
141
+ const trackers = trackersOf(db, collection);
142
+ if (!trackers) return;
143
+ for (const tracker of trackers) {
144
+ if (!tracker.numbers) continue;
145
+ const numbers: number[] = [];
146
+ for (let seq = first; seq < first + count; seq++) numbers.push(seq);
147
+ noteAll(tracker.numbers, numbers, tracker.now());
148
+ }
149
+ }
150
+
151
+ /** This function notes the stored `_id` of each tombstone that this process prunes, for each open tracker of the collection. Call it before the delete. */
152
+ export function notePrunedIds(db: Db, collection: string, storedIds: readonly unknown[]): void {
153
+ const trackers = trackersOf(db, collection);
154
+ if (!trackers) return;
155
+ const keys = storedIds.map(idKey);
156
+ for (const tracker of trackers) noteAll(tracker.pruned, keys, tracker.now());
157
+ }