@procedures/mongo 0.34.0 → 0.35.1
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/CHANGELOG.md +69 -0
- package/README.md +61 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/module.d.ts +2 -2
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +4 -5
- package/dist/module.js.map +1 -1
- package/dist/procedures/input-schemas.d.ts.map +1 -1
- package/dist/procedures/input-schemas.js +29 -2
- package/dist/procedures/input-schemas.js.map +1 -1
- package/dist/storage/document-storage.d.ts +338 -10
- package/dist/storage/document-storage.d.ts.map +1 -1
- package/dist/storage/document-storage.js +704 -75
- package/dist/storage/document-storage.js.map +1 -1
- package/dist/storage/index.d.ts +5 -3
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +4 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/materializer.d.ts +27 -12
- package/dist/storage/materializer.d.ts.map +1 -1
- package/dist/storage/materializer.js +44 -15
- package/dist/storage/materializer.js.map +1 -1
- package/dist/storage/own-writes.d.ts +51 -0
- package/dist/storage/own-writes.d.ts.map +1 -0
- package/dist/storage/own-writes.js +130 -0
- package/dist/storage/own-writes.js.map +1 -0
- package/dist/storage/storage-hints.d.ts +230 -0
- package/dist/storage/storage-hints.d.ts.map +1 -0
- package/dist/storage/storage-hints.js +876 -0
- package/dist/storage/storage-hints.js.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +15 -3
- package/src/module.ts +5 -5
- package/src/procedures/input-schemas.ts +29 -2
- package/src/storage/document-storage.ts +832 -75
- package/src/storage/index.ts +15 -3
- package/src/storage/materializer.ts +61 -22
- package/src/storage/own-writes.ts +157 -0
- package/src/storage/storage-hints.ts +971 -0
package/src/storage/index.ts
CHANGED
|
@@ -4,8 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
export { MongoStorage } from "./mongo-storage.js";
|
|
6
6
|
export type { MongoStorageOptions } from "./mongo-storage.js";
|
|
7
|
-
|
|
8
|
-
export
|
|
7
|
+
// The stored form of a MongoDocumentStorage is an option (roadmap CLI-76)
|
|
8
|
+
export {
|
|
9
|
+
MongoDocumentStorage,
|
|
10
|
+
selectorPrefilter,
|
|
11
|
+
DOCUMENT_STORAGE_FIELDS,
|
|
12
|
+
TOMBSTONE_FIELD,
|
|
13
|
+
DEFAULT_STORED_FORM,
|
|
14
|
+
decodeStoredDocument,
|
|
15
|
+
versionOfStored,
|
|
16
|
+
} from "./document-storage.js";
|
|
17
|
+
export type { MongoDocumentStorageOptions, SelectorPrefilterOptions, StoredForm, DecodeStoredDocumentOptions, StoredDocumentEntry } from "./document-storage.js";
|
|
18
|
+
// The change hints of a storage collection from its change stream (roadmap CLI-77)
|
|
19
|
+
export { watchStorageChanges, storageChangeOf } from "./storage-hints.js";
|
|
20
|
+
export type { StorageChange, StorageChangeHint, StorageChangesWatch, StorageHintPublisher, WatchStorageChangesOptions } from "./storage-hints.js";
|
|
9
21
|
export {
|
|
10
22
|
BLOCKED_PIPELINE_OPERATORS,
|
|
11
23
|
ALLOWED_PIPELINE_STAGES,
|
|
@@ -19,7 +31,7 @@ export {
|
|
|
19
31
|
export type { PipelineLimits, PipelineScope } from "./pipeline-scope.js";
|
|
20
32
|
// Materialized views driven by change streams (Graph's split plan, P4)
|
|
21
33
|
export { ViewMaterializer, identityPartition, partitionTargetId, viewFilter, VIEW_GENERATION_FIELD, VIEW_HASH_FIELD } from "./materializer.js";
|
|
22
|
-
export type { MaterializedView, MaterializedViewStatus, ViewMaterializerOptions, ViewPartitionOf, ViewPipelineScope, ViewTargetId } from "./materializer.js";
|
|
34
|
+
export type { MaterializedView, MaterializedViewStatus, ViewMaterializerOptions, ViewPartitionOf, ViewPipelineScope, ViewStorageSource, ViewTargetId } from "./materializer.js";
|
|
23
35
|
// A read-only storage of a view for the collection procedures: one generation for each read (roadmap CLI-38)
|
|
24
36
|
export { MongoViewStorage } from "./view-storage.js";
|
|
25
37
|
export type { MongoViewStorageOptions } from "./view-storage.js";
|
|
@@ -38,8 +38,9 @@
|
|
|
38
38
|
*
|
|
39
39
|
* A change reaches only the views of its partition when its partition is known. A delete has no
|
|
40
40
|
* document: its partition comes from the pre-image (a collection with
|
|
41
|
-
* `changeStreamPreAndPostImages`), or
|
|
42
|
-
* mapping. Else it reaches the views of each
|
|
41
|
+
* `changeStreamPreAndPostImages`), or, in a source of `storageSources`, from `partitionOf` of the
|
|
42
|
+
* stored form and the mapping of the source (roadmap CLI-76). Else it reaches the views of each
|
|
43
|
+
* partition of its source. With pre-images, an update
|
|
43
44
|
* that moves a document to another partition also reaches the views of its old partition.
|
|
44
45
|
*
|
|
45
46
|
* Change streams and transactions need a replica set (MongoDB 6 or later). The target of a view is
|
|
@@ -52,9 +53,12 @@
|
|
|
52
53
|
import { createHash } from "node:crypto";
|
|
53
54
|
import { BSON, ObjectId, type ChangeStream, type ClientSession, type Collection, type Db, type Document } from "mongodb";
|
|
54
55
|
import { optionalMs, realClock, type Cancel, type Clock, type Duration } from "@procedures/client";
|
|
55
|
-
import {
|
|
56
|
+
import { DEFAULT_STORED_FORM, TOMBSTONE_FIELD, type StoredForm } from "./document-storage.js";
|
|
56
57
|
import { scopePipeline } from "./pipeline-scope.js";
|
|
57
58
|
|
|
59
|
+
/** The fields of a storage that a row never keeps: its sequence number and its tombstone flag (roadmap CLI-77). */
|
|
60
|
+
const ROW_DROPPED_FIELDS = ["_seq", TOMBSTONE_FIELD] as const;
|
|
61
|
+
|
|
58
62
|
/** The definition of a view, as a document of the views collection. */
|
|
59
63
|
export interface MaterializedView {
|
|
60
64
|
_id: string;
|
|
@@ -142,13 +146,15 @@ export interface ViewMaterializerOptions {
|
|
|
142
146
|
*/
|
|
143
147
|
passLease?: Duration | undefined;
|
|
144
148
|
/**
|
|
145
|
-
* The sources that are collections of a `MongoDocumentStorage
|
|
146
|
-
* gives the partition of the views from the partition of the storage
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
149
|
+
* The sources that are collections of a `MongoDocumentStorage`. For each one, a function that
|
|
150
|
+
* gives the partition of the views from the partition of the storage (the storage has the default
|
|
151
|
+
* form), or a `ViewStorageSource` with the stored form of the storage (`storage.form`) and that
|
|
152
|
+
* function. A change without a pre-image then reaches only the views of its partition: the
|
|
153
|
+
* `partitionOf` of the form reads the storage partition from the document of the change (for a
|
|
154
|
+
* delete, only `documentKey`). Give `identityPartition` when the two partitions are the same.
|
|
155
|
+
* When the form or the function gives `undefined`, the change reaches the views of each partition.
|
|
150
156
|
*/
|
|
151
|
-
storageSources?: Readonly<Record<string, ViewPartitionOf>> | undefined;
|
|
157
|
+
storageSources?: Readonly<Record<string, ViewPartitionOf | ViewStorageSource>> | undefined;
|
|
152
158
|
}
|
|
153
159
|
|
|
154
160
|
/** The function that gives the stored `_id` of a row of a view. */
|
|
@@ -212,6 +218,14 @@ export type ViewPartitionOf = (storagePartition: string) => string | undefined;
|
|
|
212
218
|
/** The partition of the views is the partition of the storage. */
|
|
213
219
|
export const identityPartition: ViewPartitionOf = (storagePartition) => storagePartition;
|
|
214
220
|
|
|
221
|
+
/** A source of `storageSources` with its stored form (roadmap CLI-76). */
|
|
222
|
+
export interface ViewStorageSource {
|
|
223
|
+
/** The stored form of the storage, for example `storage.form`. Its `partitionOf` reads the storage partition of a document. */
|
|
224
|
+
form: Pick<StoredForm, "partitionOf">;
|
|
225
|
+
/** The partition of the views from the partition of the storage. */
|
|
226
|
+
viewPartitionOf: ViewPartitionOf;
|
|
227
|
+
}
|
|
228
|
+
|
|
215
229
|
/** A view that the materializer knows. */
|
|
216
230
|
interface RunningView {
|
|
217
231
|
view: MaterializedView;
|
|
@@ -278,7 +292,7 @@ export class ViewMaterializer {
|
|
|
278
292
|
private readonly maxTransactionRows: number;
|
|
279
293
|
private readonly graceMs: number;
|
|
280
294
|
private readonly leaseMs: number;
|
|
281
|
-
private readonly storageSources: ReadonlyMap<string,
|
|
295
|
+
private readonly storageSources: ReadonlyMap<string, ViewStorageSource>;
|
|
282
296
|
/** False after `stopIntake()`: a change starts no pass. */
|
|
283
297
|
private intake = false;
|
|
284
298
|
/** The work of each partition, one at a time: the passes and the rebalances. */
|
|
@@ -300,7 +314,15 @@ export class ViewMaterializer {
|
|
|
300
314
|
this.maxTransactionRows = options.maxTransactionRows ?? 10_000;
|
|
301
315
|
this.graceMs = optionalMs(options.generationGrace, "generationGrace") ?? 60_000;
|
|
302
316
|
this.leaseMs = Math.round(optionalMs(options.passLease, "passLease") ?? 60_000);
|
|
303
|
-
this.storageSources = new Map(
|
|
317
|
+
this.storageSources = new Map(
|
|
318
|
+
Object.entries(options.storageSources ?? {}).map(([source, entry]): [string, ViewStorageSource] => {
|
|
319
|
+
const normal = typeof entry === "function" ? { form: DEFAULT_STORED_FORM, viewPartitionOf: entry } : entry;
|
|
320
|
+
if (typeof normal?.form?.partitionOf !== "function" || typeof normal.viewPartitionOf !== "function") {
|
|
321
|
+
throw new TypeError(`The storage source ${source} needs a function, or a form with partitionOf and a viewPartitionOf`);
|
|
322
|
+
}
|
|
323
|
+
return [source, normal];
|
|
324
|
+
})
|
|
325
|
+
);
|
|
304
326
|
}
|
|
305
327
|
|
|
306
328
|
private log(level: "info" | "warn" | "error", message: string): void {
|
|
@@ -310,11 +332,13 @@ export class ViewMaterializer {
|
|
|
310
332
|
|
|
311
333
|
/**
|
|
312
334
|
* The guard of a pipeline of a view. A target of another view counts only in its current
|
|
313
|
-
* generation. A source document has no generation.
|
|
335
|
+
* generation. A source document has no generation. A tombstone of a `MongoDocumentStorage` (a
|
|
336
|
+
* deleted item) is not a row of a source (roadmap CLI-79).
|
|
314
337
|
*/
|
|
315
338
|
private scoped(pipeline: Document[], partition: string, view: MaterializedView, generations: readonly ObjectId[]): Document[] {
|
|
316
339
|
const match = {
|
|
317
340
|
[this.partitionField]: partition,
|
|
341
|
+
[TOMBSTONE_FIELD]: { $ne: true },
|
|
318
342
|
$or: [{ [VIEW_GENERATION_FIELD]: { $exists: false } }, { [VIEW_GENERATION_FIELD]: { $in: [...generations] } }],
|
|
319
343
|
};
|
|
320
344
|
if (this.options.scope) return this.options.scope(pipeline, partition, view, match);
|
|
@@ -570,21 +594,32 @@ export class ViewMaterializer {
|
|
|
570
594
|
|
|
571
595
|
/**
|
|
572
596
|
* True for a change without a pre-image (a delete, a replace, an update), in a source of
|
|
573
|
-
* `storageSources`, whose
|
|
574
|
-
* views.
|
|
575
|
-
*
|
|
597
|
+
* `storageSources`, whose document names a storage partition that maps to another partition of
|
|
598
|
+
* the views. The document is `fullDocument`, else `fullDocumentBeforeChange`, else `documentKey`,
|
|
599
|
+
* and `partitionOf` of the form reads its storage partition. A document that is in the partition
|
|
600
|
+
* after the change, a document that does not tell its partition, a key (`documentKey`) that tells
|
|
601
|
+
* another storage partition than the document after the change (a move), a mapping that gives
|
|
602
|
+
* `undefined` and a function that fails do not tell another partition: false.
|
|
576
603
|
*/
|
|
577
604
|
private isOtherPartition(change: Document, source: string, partition: string): boolean {
|
|
578
|
-
const
|
|
579
|
-
if (!
|
|
605
|
+
const storageSource = this.storageSources.get(source);
|
|
606
|
+
if (!storageSource || change["fullDocumentBeforeChange"]) return false;
|
|
580
607
|
if (!["delete", "replace", "update"].includes(change["operationType"] as string)) return false;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const
|
|
584
|
-
|
|
608
|
+
const after = change["fullDocument"] as Document | null | undefined;
|
|
609
|
+
if (after?.[this.partitionField] === partition) return false;
|
|
610
|
+
const key = change["documentKey"] as Document | null | undefined;
|
|
611
|
+
const doc = (after ?? (change["fullDocumentBeforeChange"] as Document | null | undefined) ?? key) as Document | null | undefined;
|
|
612
|
+
if (!doc) return false;
|
|
585
613
|
let viewPartition: string | undefined;
|
|
586
614
|
try {
|
|
587
|
-
|
|
615
|
+
const storagePartition = storageSource.form.partitionOf(doc);
|
|
616
|
+
if (typeof storagePartition !== "string") return false;
|
|
617
|
+
// A document whose key tells another partition than the document after the change can have moved out of this partition
|
|
618
|
+
if (after && key) {
|
|
619
|
+
const before = storageSource.form.partitionOf(key);
|
|
620
|
+
if (before !== undefined && before !== storagePartition) return false;
|
|
621
|
+
}
|
|
622
|
+
viewPartition = storageSource.viewPartitionOf(storagePartition);
|
|
588
623
|
} catch {
|
|
589
624
|
return false;
|
|
590
625
|
}
|
|
@@ -842,6 +877,10 @@ export class ViewMaterializer {
|
|
|
842
877
|
// The fields of the materializer, for a row that comes from a target: not in the content
|
|
843
878
|
const { _id, [this.partitionField]: _partition, [VIEW_GENERATION_FIELD]: _generation, [VIEW_HASH_FIELD]: _hash, ...content } = row;
|
|
844
879
|
if (_id === undefined || _id === null) throw new ViewRunError("each row of the result needs an _id");
|
|
880
|
+
// The sequence number and the tombstone flag of a MongoDocumentStorage, for a row that passes a
|
|
881
|
+
// source document through: a row is no write of a storage, so a watcher of the target must not
|
|
882
|
+
// read them (roadmap CLI-77). The other fields, for example _json and _key, are the content of the row.
|
|
883
|
+
for (const name of ROW_DROPPED_FIELDS) delete content[name];
|
|
845
884
|
const stored = this.targetId(_id, partition, generation);
|
|
846
885
|
const hash = createHash("sha1").update(canonical(content)).digest("hex");
|
|
847
886
|
group.push({
|
|
@@ -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
|
+
}
|