@procedures/mongo 0.35.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 +36 -0
- package/README.md +27 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +3 -1
- 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 +162 -3
- package/dist/storage/document-storage.d.ts.map +1 -1
- package/dist/storage/document-storage.js +325 -7
- package/dist/storage/document-storage.js.map +1 -1
- package/dist/storage/index.d.ts +3 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +3 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/materializer.d.ts.map +1 -1
- package/dist/storage/materializer.js +7 -0
- 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 +4 -0
- package/src/module.ts +5 -5
- package/src/procedures/input-schemas.ts +29 -2
- package/src/storage/document-storage.ts +394 -7
- package/src/storage/index.ts +4 -0
- package/src/storage/materializer.ts +7 -0
- package/src/storage/own-writes.ts +157 -0
- package/src/storage/storage-hints.ts +971 -0
|
@@ -92,9 +92,11 @@
|
|
|
92
92
|
*/
|
|
93
93
|
import { randomUUID } from "node:crypto";
|
|
94
94
|
import { isDeepStrictEqual } from "node:util";
|
|
95
|
+
import { BSON } from "mongodb";
|
|
95
96
|
import { VersionConflictError, applyPatch, preconditionNotSupported, queryEntries, versionConflict, } from "@procedures/collections";
|
|
96
97
|
import { optionalMs, toMs } from "@procedures/client";
|
|
97
98
|
import { ensureConnection } from "../connection.js";
|
|
99
|
+
import { noteOwnNumbers, notePrunedIds } from "./own-writes.js";
|
|
98
100
|
import { boundPipeline, cutResults, DEFAULT_PIPELINE_LIMITS, scopePipeline } from "./pipeline-scope.js";
|
|
99
101
|
/** The fields of the storage in a stored document. */
|
|
100
102
|
const KEY = "_key";
|
|
@@ -270,6 +272,58 @@ export function decodeStoredDocument(doc, options = {}) {
|
|
|
270
272
|
const entries = Object.entries(form.partitionFields(partition));
|
|
271
273
|
return { id, partition, item: itemOf(doc, id, entries, options.addId ?? true) };
|
|
272
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* This function gives the version of a raw stored document of a `MongoDocumentStorage`: the
|
|
277
|
+
* version that `storage.versionOf(id)` gives for the same document (roadmap CLI-77). The storage
|
|
278
|
+
* uses it too, so the two never differ.
|
|
279
|
+
*
|
|
280
|
+
* - The version is the sequence number of the last write through the storage (`_seq`), as text.
|
|
281
|
+
* - A document without `_seq` (an old document, or one that a raw replace wrote) has the version `0`.
|
|
282
|
+
* - A tombstone, a value that is not a plain object, and a document whose id neither `idOf` nor
|
|
283
|
+
* `_key` tells give `undefined`: they are no items, as for `decodeStoredDocument`.
|
|
284
|
+
*
|
|
285
|
+
* @param doc - The stored document, for example the `fullDocument` of a change event
|
|
286
|
+
* @param form - The stored form of the storage, for example `storage.form` (default `DEFAULT_STORED_FORM`)
|
|
287
|
+
* @param partition - The partition of the document. The default is `form.partitionOf(doc)`.
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* ```typescript
|
|
291
|
+
* const version = versionOfStored(change.fullDocument, notes.form); // the same as await notes.versionOf(id)
|
|
292
|
+
* ```
|
|
293
|
+
*/
|
|
294
|
+
export function versionOfStored(doc, form = DEFAULT_STORED_FORM, partition) {
|
|
295
|
+
if (!isPlainObject(doc) || doc[TOMB] === true)
|
|
296
|
+
return undefined;
|
|
297
|
+
const place = partition ?? form.partitionOf(doc);
|
|
298
|
+
if (typeof form.idOf(doc["_id"], place) !== "string" && typeof doc[KEY] !== "string")
|
|
299
|
+
return undefined;
|
|
300
|
+
return seqVersion(doc);
|
|
301
|
+
}
|
|
302
|
+
/** The version of the `_seq` of a stored document: the number as text, or `0` without it. */
|
|
303
|
+
function seqVersion(doc) {
|
|
304
|
+
const seq = doc[SEQ];
|
|
305
|
+
return typeof seq === "number" ? String(seq) : "0";
|
|
306
|
+
}
|
|
307
|
+
/** True when two values have the same BSON. */
|
|
308
|
+
function sameBson(a, b) {
|
|
309
|
+
try {
|
|
310
|
+
return Buffer.from(BSON.serialize({ v: a })).equals(Buffer.from(BSON.serialize({ v: b })));
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/** The most floor documents that `partitions()` reads. */
|
|
317
|
+
const MAX_KNOWN_PARTITIONS = 10_000;
|
|
318
|
+
function escapeRegex(text) {
|
|
319
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
320
|
+
}
|
|
321
|
+
/** The reader of the parts of a storage. The class sets it, so it can read its private fields. */
|
|
322
|
+
let sourceOfStorage;
|
|
323
|
+
/** This function gives the parts of a storage that `watchStorageChanges` reads. The package does not export it. */
|
|
324
|
+
export function storageChangeSource(storage) {
|
|
325
|
+
return sourceOfStorage(storage);
|
|
326
|
+
}
|
|
273
327
|
/** True for a plain object. */
|
|
274
328
|
function isPlainObject(value) {
|
|
275
329
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
@@ -405,7 +459,23 @@ export class MongoDocumentStorage {
|
|
|
405
459
|
/** The fields that only the storage writes: its fields and the partition fields. */
|
|
406
460
|
reserved;
|
|
407
461
|
indexes;
|
|
462
|
+
/** True after this storage wrote (or found) the floor document of its partition (roadmap O66). A failed write leaves it false. */
|
|
463
|
+
floorNoted = false;
|
|
408
464
|
closed = false;
|
|
465
|
+
static {
|
|
466
|
+
sourceOfStorage = (storage) => ({
|
|
467
|
+
db: () => storage.database(),
|
|
468
|
+
collection: storage.collectionName,
|
|
469
|
+
counters: storage.countersName,
|
|
470
|
+
form: storage.form,
|
|
471
|
+
partition: storage.partition,
|
|
472
|
+
adopt: (raw) => storage.adoptRaw(raw),
|
|
473
|
+
adoptAll: (raws) => storage.adoptRawAll(raws),
|
|
474
|
+
rawDocuments: (after, limit) => storage.rawDocuments(after, limit),
|
|
475
|
+
storedDocuments: (ids) => storage.storedDocuments(ids),
|
|
476
|
+
partitions: () => storage.knownPartitions(),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
409
479
|
constructor(options, partitions) {
|
|
410
480
|
if (options.partition !== undefined && (options.partition === "" || options.partition.includes(SEPARATOR))) {
|
|
411
481
|
throw new Error("A partition must be a text without the character U+001F");
|
|
@@ -648,9 +718,11 @@ export class MongoDocumentStorage {
|
|
|
648
718
|
/**
|
|
649
719
|
* This method takes numbers and writes with them. On a replica set, the two are one
|
|
650
720
|
* transaction. Else the numbers stay pending until the write ends, and a late write moves its
|
|
651
|
-
* documents to new numbers.
|
|
721
|
+
* documents to new numbers. With `own` false (an adoption of a raw write, D39), a watcher of this
|
|
722
|
+
* process does not take the numbers as its own.
|
|
652
723
|
*/
|
|
653
|
-
async withNumbers(db, collection, count, write) {
|
|
724
|
+
async withNumbers(db, collection, count, write, own = true) {
|
|
725
|
+
const noteOwn = own ? noteOwnNumbers : () => undefined;
|
|
654
726
|
const client = await this.transactionClient(db);
|
|
655
727
|
if (client) {
|
|
656
728
|
const session = client.startSession();
|
|
@@ -659,6 +731,8 @@ export class MongoDocumentStorage {
|
|
|
659
731
|
const counter = (await db.collection(this.countersName).findOneAndUpdate({ _id: this.collectionName }, { $inc: { seq: count }, $setOnInsert: { epoch: randomUUID() } }, { upsert: true, returnDocument: "after", session }));
|
|
660
732
|
if (!counter)
|
|
661
733
|
throw new Error(`The sequence of ${this.collectionName} is not available`);
|
|
734
|
+
// A watcher of the collection in this process skips the events of these numbers (roadmap CLI-77)
|
|
735
|
+
noteOwn(db, this.collectionName, counter.seq - count + 1, count);
|
|
662
736
|
return write(counter.seq - count + 1, session);
|
|
663
737
|
});
|
|
664
738
|
}
|
|
@@ -667,6 +741,7 @@ export class MongoDocumentStorage {
|
|
|
667
741
|
}
|
|
668
742
|
}
|
|
669
743
|
let { first } = await this.allocate(db, count);
|
|
744
|
+
noteOwn(db, this.collectionName, first, count);
|
|
670
745
|
let result;
|
|
671
746
|
try {
|
|
672
747
|
result = await write(first);
|
|
@@ -680,6 +755,7 @@ export class MongoDocumentStorage {
|
|
|
680
755
|
throw new Error(`A write of ${this.collectionName} stayed late after ${MAX_MOVES} moves to new numbers`);
|
|
681
756
|
// Late: a reader can have passed these numbers. The write takes new ones.
|
|
682
757
|
const next = (await this.allocate(db, count)).first;
|
|
758
|
+
noteOwn(db, this.collectionName, next, count);
|
|
683
759
|
await this.move(collection, first, count, next);
|
|
684
760
|
first = next;
|
|
685
761
|
}
|
|
@@ -722,6 +798,33 @@ export class MongoDocumentStorage {
|
|
|
722
798
|
floorId() {
|
|
723
799
|
return `floor${SEPARATOR}${this.collectionName}${SEPARATOR}${this.partition ?? ""}`;
|
|
724
800
|
}
|
|
801
|
+
/**
|
|
802
|
+
* This method writes the floor document of the partition once for this storage, before its first
|
|
803
|
+
* write (roadmap O66). Then `knownPartitions()` of each process finds the partition, also without
|
|
804
|
+
* a prune, and a gap of `watchStorageChanges()` sends it a reset hint.
|
|
805
|
+
*
|
|
806
|
+
* - The floor `0` is neutral: `getChangesSince` gives a full result only for a version below the
|
|
807
|
+
* floor, and a version is never below `0`. A missing floor document also reads as `0`. So the
|
|
808
|
+
* document changes no result of the change log.
|
|
809
|
+
* - `$setOnInsert` writes only a new document: a floor of a prune (`$max`) stays as it is.
|
|
810
|
+
* - A duplicate key (another process inserted the same document first) is a success.
|
|
811
|
+
* - The upsert runs in the queue of the writes, before the task, so never in the transaction of
|
|
812
|
+
* `withNumbers()`: it adds no write conflict to the transaction of the user, and a transaction
|
|
813
|
+
* that tries again does not write it again.
|
|
814
|
+
* - A failure does not fail the write of the user. The flag stays false, so the next write tries again.
|
|
815
|
+
*/
|
|
816
|
+
async noteFloor(db) {
|
|
817
|
+
if (this.floorNoted)
|
|
818
|
+
return;
|
|
819
|
+
try {
|
|
820
|
+
await db.collection(this.countersName).updateOne({ _id: this.floorId() }, { $setOnInsert: { floor: 0 } }, { upsert: true });
|
|
821
|
+
this.floorNoted = true;
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
if (isDuplicateKey(error))
|
|
825
|
+
this.floorNoted = true;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
725
828
|
/** This method drops the oldest tombstones of the partition over the limit, and keeps the floor of the log. */
|
|
726
829
|
async pruneTombstones(db, items) {
|
|
727
830
|
const count = await items.countDocuments({ ...this.partitionFilter(), [TOMB]: true });
|
|
@@ -735,6 +838,8 @@ export class MongoDocumentStorage {
|
|
|
735
838
|
.toArray();
|
|
736
839
|
if (oldest.length === 0)
|
|
737
840
|
return;
|
|
841
|
+
// A watcher of this process reads the delete of a pruned tombstone without the image before the change (roadmap CLI-77)
|
|
842
|
+
notePrunedIds(db, this.collectionName, oldest.map((doc) => doc._id));
|
|
738
843
|
await items.deleteMany({ _id: { $in: oldest.map((doc) => doc._id) } });
|
|
739
844
|
const floor = Math.max(...oldest.map((doc) => Number(doc[SEQ])));
|
|
740
845
|
await db.collection(this.countersName).updateOne({ _id: this.floorId() }, { $max: { floor } }, { upsert: true });
|
|
@@ -848,14 +953,14 @@ export class MongoDocumentStorage {
|
|
|
848
953
|
}
|
|
849
954
|
/**
|
|
850
955
|
* The version of an item: the sequence number of its last write, or undefined for a missing id.
|
|
851
|
-
* An item without a write through the storage (no `_seq`) has the version `0`.
|
|
956
|
+
* An item without a write through the storage (no `_seq`) has the version `0`. It is the version
|
|
957
|
+
* of `versionOfStored()` of the stored document (roadmap CLI-77): the two share one rule.
|
|
852
958
|
*/
|
|
853
959
|
async versionOf(id) {
|
|
854
960
|
const { items } = await this.collection();
|
|
855
961
|
const doc = await items.findOne(this.liveFilter({ _id: this.storedId(id) }), { projection: { [SEQ]: 1 } });
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
return typeof doc[SEQ] === "number" ? String(doc[SEQ]) : "0";
|
|
962
|
+
// The filter finds the document by the stored `_id` of the id in the partition: it is the item
|
|
963
|
+
return doc ? seqVersion(doc) : undefined;
|
|
859
964
|
}
|
|
860
965
|
async getChangesSince(since) {
|
|
861
966
|
const { db, items } = await this.collection();
|
|
@@ -900,7 +1005,11 @@ export class MongoDocumentStorage {
|
|
|
900
1005
|
// ===========================================================================
|
|
901
1006
|
async write(task) {
|
|
902
1007
|
const { db, items } = await this.collection();
|
|
903
|
-
return enqueue(db, this.collectionName, () =>
|
|
1008
|
+
return enqueue(db, this.collectionName, async () => {
|
|
1009
|
+
// The floor document of the partition, outside each transaction of the task (roadmap O66)
|
|
1010
|
+
await this.noteFloor(db);
|
|
1011
|
+
return task(db, items);
|
|
1012
|
+
});
|
|
904
1013
|
}
|
|
905
1014
|
async set(id, value, options) {
|
|
906
1015
|
await this.setBatch([[id, value]], options?.expectedVersion === undefined ? undefined : { expectedVersions: [[id, options.expectedVersion]] });
|
|
@@ -1024,6 +1133,215 @@ export class MongoDocumentStorage {
|
|
|
1024
1133
|
});
|
|
1025
1134
|
await this.pruneTombstones(db, collection);
|
|
1026
1135
|
}
|
|
1136
|
+
// ===========================================================================
|
|
1137
|
+
// The raw writes (D39, roadmap CLI-77)
|
|
1138
|
+
// ===========================================================================
|
|
1139
|
+
/**
|
|
1140
|
+
* This method adopts a raw write: one conditional write through the path of each write gives the
|
|
1141
|
+
* document a new `_seq` (see `RawStorageWrite`). It is for `watchStorageChanges()`, and it is no
|
|
1142
|
+
* general setter of `_seq`.
|
|
1143
|
+
*/
|
|
1144
|
+
async adoptRaw(raw) {
|
|
1145
|
+
const [result] = await this.adoptRawAll([raw]);
|
|
1146
|
+
if (result === undefined || typeof result !== "string")
|
|
1147
|
+
throw result?.error;
|
|
1148
|
+
return result;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* This method adopts several raw writes. The writes of each partition take one block of numbers
|
|
1152
|
+
* through the path of each write. A number of a write that does not win stays unused.
|
|
1153
|
+
*/
|
|
1154
|
+
async adoptRawAll(raws) {
|
|
1155
|
+
const results = raws.map(() => "unknown");
|
|
1156
|
+
const groups = new Map();
|
|
1157
|
+
raws.forEach((raw, index) => {
|
|
1158
|
+
const target = this.adoptionTarget(raw);
|
|
1159
|
+
if (!target)
|
|
1160
|
+
return;
|
|
1161
|
+
let group = groups.get(target.storage);
|
|
1162
|
+
if (!group)
|
|
1163
|
+
groups.set(target.storage, (group = []));
|
|
1164
|
+
group.push({ index, raw, id: target.id });
|
|
1165
|
+
});
|
|
1166
|
+
for (const [storage, group] of groups) {
|
|
1167
|
+
try {
|
|
1168
|
+
const done = await storage.adoptGroup(group);
|
|
1169
|
+
group.forEach((entry, position) => (results[entry.index] = done[position] ?? "lost"));
|
|
1170
|
+
}
|
|
1171
|
+
catch (error) {
|
|
1172
|
+
for (const entry of group)
|
|
1173
|
+
results[entry.index] = { error };
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return results;
|
|
1177
|
+
}
|
|
1178
|
+
/** The adoption of raw writes of this partition, with one block of numbers. */
|
|
1179
|
+
async adoptGroup(group) {
|
|
1180
|
+
return this.write(async (db, collection) => {
|
|
1181
|
+
const results = await this.withNumbers(db, collection, group.length, async (first, session) => {
|
|
1182
|
+
const out = [];
|
|
1183
|
+
for (const [position, { raw, id }] of group.entries()) {
|
|
1184
|
+
const seq = first + position;
|
|
1185
|
+
out.push(raw.after === undefined ? await this.adoptDelete(collection, raw, id, seq, session) : await this.adoptWrite(collection, raw, raw.after, id, seq, session));
|
|
1186
|
+
}
|
|
1187
|
+
return out;
|
|
1188
|
+
}, false);
|
|
1189
|
+
if (results.some((result, position) => result === "adopted" && group[position].raw.after === undefined))
|
|
1190
|
+
await this.pruneTombstones(db, collection);
|
|
1191
|
+
return results;
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
/** A raw hard delete: a tombstone with the id and the partition of the image before the change. */
|
|
1195
|
+
async adoptDelete(collection, raw, id, seq, session) {
|
|
1196
|
+
// In a transaction, a duplicate key would end the transaction: the read comes first
|
|
1197
|
+
if (session && (await collection.findOne({ _id: raw.storedId }, { projection: { _id: 1 }, session })))
|
|
1198
|
+
return "lost";
|
|
1199
|
+
const tomb = { _id: raw.storedId, [KEY]: id, [SEQ]: seq, [TOMB]: true };
|
|
1200
|
+
this.putPartition(tomb);
|
|
1201
|
+
try {
|
|
1202
|
+
await collection.insertOne(tomb, session ? { session } : {});
|
|
1203
|
+
return "adopted";
|
|
1204
|
+
}
|
|
1205
|
+
catch (error) {
|
|
1206
|
+
// A raw insert of the same _id came after the delete: that insert is the newer state
|
|
1207
|
+
if (isDuplicateKey(error))
|
|
1208
|
+
return "lost";
|
|
1209
|
+
throw error;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
/** A raw insert, replace or update: one conditional write on the exact state of the event. */
|
|
1213
|
+
async adoptWrite(collection, raw, after, id, seq, session) {
|
|
1214
|
+
const fields = this.restoredFields(raw, after, id);
|
|
1215
|
+
// The exact state of the event: the same `_seq` (or none), and no field that the adoption sets
|
|
1216
|
+
const filter = { _id: raw.storedId, [SEQ]: raw.seq ?? { $exists: false } };
|
|
1217
|
+
for (const name of Object.keys(fields))
|
|
1218
|
+
filter[name] = { $exists: false };
|
|
1219
|
+
const result = await collection.updateOne(filter, { $set: { ...fields, [SEQ]: seq } }, session ? { session } : {});
|
|
1220
|
+
return result.matchedCount === 1 ? "adopted" : "lost";
|
|
1221
|
+
}
|
|
1222
|
+
/** One page of the documents without `_seq`, in the order of `_id` (the sweep of `watchStorageChanges()`). */
|
|
1223
|
+
async rawDocuments(after, limit) {
|
|
1224
|
+
const db = await this.database();
|
|
1225
|
+
// The scan walks the index of _id from the last _id: `min` bounds the scan, in the order of the
|
|
1226
|
+
// BSON types. `min` includes that _id, so the page skips it. No index can find the documents
|
|
1227
|
+
// without a field (a partial index cannot express $exists: false): the scan reads each document.
|
|
1228
|
+
let cursor = db.collection(this.collectionName).find({ [SEQ]: { $exists: false } }).hint({ _id: 1 });
|
|
1229
|
+
if (after)
|
|
1230
|
+
cursor = cursor.min({ _id: after.id });
|
|
1231
|
+
const docs = await cursor.limit(after ? limit + 1 : limit).toArray();
|
|
1232
|
+
if (after && docs.length > 0 && sameBson(docs[0]["_id"], after.id))
|
|
1233
|
+
docs.shift();
|
|
1234
|
+
return docs.slice(0, limit);
|
|
1235
|
+
}
|
|
1236
|
+
/** The stored documents of these stored `_id`s, for the adoptions that `watchStorageChanges()` does again. */
|
|
1237
|
+
async storedDocuments(ids) {
|
|
1238
|
+
const db = await this.database();
|
|
1239
|
+
const filter = { _id: { $in: [...ids] } };
|
|
1240
|
+
return db.collection(this.collectionName).find(filter).toArray();
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* The storage of the partition of a raw write and the id of its document, or undefined when
|
|
1244
|
+
* neither the documents of the event nor the stored `_id` tell them. The partition comes from
|
|
1245
|
+
* `form.partitionOf()` of the image before the change, else of the document after the write,
|
|
1246
|
+
* else of the stored `_id` (the order of `storageChangeOf()`). The document must not have a
|
|
1247
|
+
* partition field with another value.
|
|
1248
|
+
*/
|
|
1249
|
+
adoptionTarget(raw) {
|
|
1250
|
+
const doc = raw.after ?? raw.before;
|
|
1251
|
+
if (!doc)
|
|
1252
|
+
return undefined;
|
|
1253
|
+
const places = new Set();
|
|
1254
|
+
for (const source of [raw.before, raw.after, { _id: raw.storedId }])
|
|
1255
|
+
if (isPlainObject(source))
|
|
1256
|
+
places.add(this.form.partitionOf(source));
|
|
1257
|
+
for (const place of places) {
|
|
1258
|
+
if (place !== this.partition && (this.partition !== undefined || place === undefined))
|
|
1259
|
+
continue;
|
|
1260
|
+
let storage;
|
|
1261
|
+
try {
|
|
1262
|
+
storage = place === this.partition ? this : this.forPartition(place);
|
|
1263
|
+
}
|
|
1264
|
+
catch {
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
if (!storage.holds(doc))
|
|
1268
|
+
continue;
|
|
1269
|
+
const fromId = this.form.idOf(raw.storedId, place);
|
|
1270
|
+
const key = raw.after?.[KEY] ?? raw.before?.[KEY];
|
|
1271
|
+
const id = typeof fromId === "string" ? fromId : typeof key === "string" ? key : undefined;
|
|
1272
|
+
if (id === undefined || id.includes(SEPARATOR))
|
|
1273
|
+
continue;
|
|
1274
|
+
return { storage, id };
|
|
1275
|
+
}
|
|
1276
|
+
return undefined;
|
|
1277
|
+
}
|
|
1278
|
+
/** True when each partition field of a document has the value of this partition, or is absent (a raw replace can drop it). */
|
|
1279
|
+
holds(doc) {
|
|
1280
|
+
for (const [name, value] of this.partitionEntries) {
|
|
1281
|
+
if (!(name in doc))
|
|
1282
|
+
continue;
|
|
1283
|
+
if (value === undefined || !isDeepStrictEqual(doc[name], value))
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* The storage fields and the partition fields that a raw write took from the document, for the
|
|
1290
|
+
* adoption to set again. No field of the item is in it.
|
|
1291
|
+
*
|
|
1292
|
+
* - The partition fields of this partition that the document does not have.
|
|
1293
|
+
* - `_key` (with `_kid`) of the image before the change. Without the image, `_key` only when
|
|
1294
|
+
* `form.idOf()` does not tell the id, with `_kid`: the item keeps its `_id`, as a document from
|
|
1295
|
+
* outside the storage.
|
|
1296
|
+
* - `_ord` of the image before the change, else `0`, the place of a document without `_ord`
|
|
1297
|
+
* (the rule of the old documents).
|
|
1298
|
+
* - The names of `_kpf` of the image before the change that the document has with the value of
|
|
1299
|
+
* the partition.
|
|
1300
|
+
*/
|
|
1301
|
+
restoredFields(raw, after, id) {
|
|
1302
|
+
const before = raw.before;
|
|
1303
|
+
const fields = {};
|
|
1304
|
+
for (const [name, value] of this.partitionEntries)
|
|
1305
|
+
if (value !== undefined && !(name in after))
|
|
1306
|
+
fields[name] = value;
|
|
1307
|
+
if (after[KEY] === undefined) {
|
|
1308
|
+
if (typeof before?.[KEY] === "string") {
|
|
1309
|
+
fields[KEY] = before[KEY];
|
|
1310
|
+
if (before[KEEP_ID] === true && after[KEEP_ID] === undefined)
|
|
1311
|
+
fields[KEEP_ID] = true;
|
|
1312
|
+
}
|
|
1313
|
+
else if (this.form.idOf(raw.storedId, this.partition) !== id) {
|
|
1314
|
+
fields[KEY] = id;
|
|
1315
|
+
if (after[KEEP_ID] === undefined)
|
|
1316
|
+
fields[KEEP_ID] = true;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (after[ORD] === undefined)
|
|
1320
|
+
fields[ORD] = typeof before?.[ORD] === "number" ? before[ORD] : 0;
|
|
1321
|
+
const kept = before?.[KEEP_PART];
|
|
1322
|
+
if (after[KEEP_PART] === undefined && Array.isArray(kept)) {
|
|
1323
|
+
const names = kept.filter((name) => typeof name === "string" && this.partitionValues.has(name) && isDeepStrictEqual(after[name], this.partitionValues.get(name)));
|
|
1324
|
+
if (names.length > 0)
|
|
1325
|
+
fields[KEEP_PART] = names;
|
|
1326
|
+
}
|
|
1327
|
+
return fields;
|
|
1328
|
+
}
|
|
1329
|
+
/** The partitions that the storage knows: the floor documents of the counters (of the first writes and of the prunes), and the storages of `forPartition()`. */
|
|
1330
|
+
async knownPartitions() {
|
|
1331
|
+
const db = await this.database();
|
|
1332
|
+
const prefix = `floor${SEPARATOR}${this.collectionName}${SEPARATOR}`;
|
|
1333
|
+
const floors = await db
|
|
1334
|
+
.collection(this.countersName)
|
|
1335
|
+
.find({ _id: { $regex: `^${escapeRegex(prefix)}` } }, { projection: { _id: 1 } })
|
|
1336
|
+
.limit(MAX_KNOWN_PARTITIONS)
|
|
1337
|
+
.toArray();
|
|
1338
|
+
const found = new Set(this.partitions.keys());
|
|
1339
|
+
for (const floor of floors) {
|
|
1340
|
+
const rest = String(floor._id).slice(prefix.length);
|
|
1341
|
+
found.add(rest === "" ? undefined : rest);
|
|
1342
|
+
}
|
|
1343
|
+
return [...found];
|
|
1344
|
+
}
|
|
1027
1345
|
/**
|
|
1028
1346
|
* The preconditions of a write (roadmap CLI-16), after a check of their ids. A storage without
|
|
1029
1347
|
* `versionOf` throws a `TypeError` before the write starts.
|