@procedures/mongo 0.34.0 → 0.35.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.
- package/CHANGELOG.md +33 -0
- package/README.md +34 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/storage/document-storage.d.ts +177 -8
- package/dist/storage/document-storage.d.ts.map +1 -1
- package/dist/storage/document-storage.js +383 -72
- package/dist/storage/document-storage.js.map +1 -1
- package/dist/storage/index.d.ts +3 -3
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +2 -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 +37 -15
- package/dist/storage/materializer.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +11 -3
- package/src/storage/document-storage.ts +441 -71
- package/src/storage/index.ts +11 -3
- package/src/storage/materializer.ts +54 -22
package/src/storage/index.ts
CHANGED
|
@@ -4,8 +4,16 @@
|
|
|
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
|
+
} from "./document-storage.js";
|
|
16
|
+
export type { MongoDocumentStorageOptions, SelectorPrefilterOptions, StoredForm, DecodeStoredDocumentOptions, StoredDocumentEntry } from "./document-storage.js";
|
|
9
17
|
export {
|
|
10
18
|
BLOCKED_PIPELINE_OPERATORS,
|
|
11
19
|
ALLOWED_PIPELINE_STAGES,
|
|
@@ -19,7 +27,7 @@ export {
|
|
|
19
27
|
export type { PipelineLimits, PipelineScope } from "./pipeline-scope.js";
|
|
20
28
|
// Materialized views driven by change streams (Graph's split plan, P4)
|
|
21
29
|
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";
|
|
30
|
+
export type { MaterializedView, MaterializedViewStatus, ViewMaterializerOptions, ViewPartitionOf, ViewPipelineScope, ViewStorageSource, ViewTargetId } from "./materializer.js";
|
|
23
31
|
// A read-only storage of a view for the collection procedures: one generation for each read (roadmap CLI-38)
|
|
24
32
|
export { MongoViewStorage } from "./view-storage.js";
|
|
25
33
|
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,7 +53,7 @@
|
|
|
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
|
|
|
58
59
|
/** The definition of a view, as a document of the views collection. */
|
|
@@ -142,13 +143,15 @@ export interface ViewMaterializerOptions {
|
|
|
142
143
|
*/
|
|
143
144
|
passLease?: Duration | undefined;
|
|
144
145
|
/**
|
|
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
|
-
*
|
|
146
|
+
* The sources that are collections of a `MongoDocumentStorage`. For each one, a function that
|
|
147
|
+
* gives the partition of the views from the partition of the storage (the storage has the default
|
|
148
|
+
* form), or a `ViewStorageSource` with the stored form of the storage (`storage.form`) and that
|
|
149
|
+
* function. A change without a pre-image then reaches only the views of its partition: the
|
|
150
|
+
* `partitionOf` of the form reads the storage partition from the document of the change (for a
|
|
151
|
+
* delete, only `documentKey`). Give `identityPartition` when the two partitions are the same.
|
|
152
|
+
* When the form or the function gives `undefined`, the change reaches the views of each partition.
|
|
150
153
|
*/
|
|
151
|
-
storageSources?: Readonly<Record<string, ViewPartitionOf>> | undefined;
|
|
154
|
+
storageSources?: Readonly<Record<string, ViewPartitionOf | ViewStorageSource>> | undefined;
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
/** The function that gives the stored `_id` of a row of a view. */
|
|
@@ -212,6 +215,14 @@ export type ViewPartitionOf = (storagePartition: string) => string | undefined;
|
|
|
212
215
|
/** The partition of the views is the partition of the storage. */
|
|
213
216
|
export const identityPartition: ViewPartitionOf = (storagePartition) => storagePartition;
|
|
214
217
|
|
|
218
|
+
/** A source of `storageSources` with its stored form (roadmap CLI-76). */
|
|
219
|
+
export interface ViewStorageSource {
|
|
220
|
+
/** The stored form of the storage, for example `storage.form`. Its `partitionOf` reads the storage partition of a document. */
|
|
221
|
+
form: Pick<StoredForm, "partitionOf">;
|
|
222
|
+
/** The partition of the views from the partition of the storage. */
|
|
223
|
+
viewPartitionOf: ViewPartitionOf;
|
|
224
|
+
}
|
|
225
|
+
|
|
215
226
|
/** A view that the materializer knows. */
|
|
216
227
|
interface RunningView {
|
|
217
228
|
view: MaterializedView;
|
|
@@ -278,7 +289,7 @@ export class ViewMaterializer {
|
|
|
278
289
|
private readonly maxTransactionRows: number;
|
|
279
290
|
private readonly graceMs: number;
|
|
280
291
|
private readonly leaseMs: number;
|
|
281
|
-
private readonly storageSources: ReadonlyMap<string,
|
|
292
|
+
private readonly storageSources: ReadonlyMap<string, ViewStorageSource>;
|
|
282
293
|
/** False after `stopIntake()`: a change starts no pass. */
|
|
283
294
|
private intake = false;
|
|
284
295
|
/** The work of each partition, one at a time: the passes and the rebalances. */
|
|
@@ -300,7 +311,15 @@ export class ViewMaterializer {
|
|
|
300
311
|
this.maxTransactionRows = options.maxTransactionRows ?? 10_000;
|
|
301
312
|
this.graceMs = optionalMs(options.generationGrace, "generationGrace") ?? 60_000;
|
|
302
313
|
this.leaseMs = Math.round(optionalMs(options.passLease, "passLease") ?? 60_000);
|
|
303
|
-
this.storageSources = new Map(
|
|
314
|
+
this.storageSources = new Map(
|
|
315
|
+
Object.entries(options.storageSources ?? {}).map(([source, entry]): [string, ViewStorageSource] => {
|
|
316
|
+
const normal = typeof entry === "function" ? { form: DEFAULT_STORED_FORM, viewPartitionOf: entry } : entry;
|
|
317
|
+
if (typeof normal?.form?.partitionOf !== "function" || typeof normal.viewPartitionOf !== "function") {
|
|
318
|
+
throw new TypeError(`The storage source ${source} needs a function, or a form with partitionOf and a viewPartitionOf`);
|
|
319
|
+
}
|
|
320
|
+
return [source, normal];
|
|
321
|
+
})
|
|
322
|
+
);
|
|
304
323
|
}
|
|
305
324
|
|
|
306
325
|
private log(level: "info" | "warn" | "error", message: string): void {
|
|
@@ -310,11 +329,13 @@ export class ViewMaterializer {
|
|
|
310
329
|
|
|
311
330
|
/**
|
|
312
331
|
* 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.
|
|
332
|
+
* generation. A source document has no generation. A tombstone of a `MongoDocumentStorage` (a
|
|
333
|
+
* deleted item) is not a row of a source (roadmap CLI-79).
|
|
314
334
|
*/
|
|
315
335
|
private scoped(pipeline: Document[], partition: string, view: MaterializedView, generations: readonly ObjectId[]): Document[] {
|
|
316
336
|
const match = {
|
|
317
337
|
[this.partitionField]: partition,
|
|
338
|
+
[TOMBSTONE_FIELD]: { $ne: true },
|
|
318
339
|
$or: [{ [VIEW_GENERATION_FIELD]: { $exists: false } }, { [VIEW_GENERATION_FIELD]: { $in: [...generations] } }],
|
|
319
340
|
};
|
|
320
341
|
if (this.options.scope) return this.options.scope(pipeline, partition, view, match);
|
|
@@ -570,21 +591,32 @@ export class ViewMaterializer {
|
|
|
570
591
|
|
|
571
592
|
/**
|
|
572
593
|
* True for a change without a pre-image (a delete, a replace, an update), in a source of
|
|
573
|
-
* `storageSources`, whose
|
|
574
|
-
* views.
|
|
575
|
-
*
|
|
594
|
+
* `storageSources`, whose document names a storage partition that maps to another partition of
|
|
595
|
+
* the views. The document is `fullDocument`, else `fullDocumentBeforeChange`, else `documentKey`,
|
|
596
|
+
* and `partitionOf` of the form reads its storage partition. A document that is in the partition
|
|
597
|
+
* after the change, a document that does not tell its partition, a key (`documentKey`) that tells
|
|
598
|
+
* another storage partition than the document after the change (a move), a mapping that gives
|
|
599
|
+
* `undefined` and a function that fails do not tell another partition: false.
|
|
576
600
|
*/
|
|
577
601
|
private isOtherPartition(change: Document, source: string, partition: string): boolean {
|
|
578
|
-
const
|
|
579
|
-
if (!
|
|
602
|
+
const storageSource = this.storageSources.get(source);
|
|
603
|
+
if (!storageSource || change["fullDocumentBeforeChange"]) return false;
|
|
580
604
|
if (!["delete", "replace", "update"].includes(change["operationType"] as string)) return false;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const
|
|
584
|
-
|
|
605
|
+
const after = change["fullDocument"] as Document | null | undefined;
|
|
606
|
+
if (after?.[this.partitionField] === partition) return false;
|
|
607
|
+
const key = change["documentKey"] as Document | null | undefined;
|
|
608
|
+
const doc = (after ?? (change["fullDocumentBeforeChange"] as Document | null | undefined) ?? key) as Document | null | undefined;
|
|
609
|
+
if (!doc) return false;
|
|
585
610
|
let viewPartition: string | undefined;
|
|
586
611
|
try {
|
|
587
|
-
|
|
612
|
+
const storagePartition = storageSource.form.partitionOf(doc);
|
|
613
|
+
if (typeof storagePartition !== "string") return false;
|
|
614
|
+
// A document whose key tells another partition than the document after the change can have moved out of this partition
|
|
615
|
+
if (after && key) {
|
|
616
|
+
const before = storageSource.form.partitionOf(key);
|
|
617
|
+
if (before !== undefined && before !== storagePartition) return false;
|
|
618
|
+
}
|
|
619
|
+
viewPartition = storageSource.viewPartitionOf(storagePartition);
|
|
588
620
|
} catch {
|
|
589
621
|
return false;
|
|
590
622
|
}
|