@lossless.org/client 1.22.2 → 1.23.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/nosqldb/classes.bootstraprecord.js +4 -4
- package/dist_ts/nosqldb/classes.connection.d.ts +3 -0
- package/dist_ts/nosqldb/classes.connection.js +3 -2
- package/dist_ts/nosqldb/classes.db.d.ts +31 -0
- package/dist_ts/nosqldb/classes.db.js +45 -2
- package/dist_ts/nosqldb/classes.doc.js +5 -5
- package/dist_ts/nosqldb/classes.exactpersistence.d.ts +3 -2
- package/dist_ts/nosqldb/classes.exactpersistence.js +8 -7
- package/dist_ts/nosqldb/classes.session.d.ts +7 -6
- package/dist_ts/nosqldb/classes.session.js +76 -29
- package/dist_ts/nosqldb/classes.writelog.d.ts +129 -0
- package/dist_ts/nosqldb/classes.writelog.js +497 -0
- package/dist_ts/nosqldb/classes.writeobserver.d.ts +13 -0
- package/dist_ts/nosqldb/classes.writeobserver.js +47 -25
- package/dist_ts/nosqldb/classes.writerevisions.d.ts +16 -2
- package/dist_ts/nosqldb/classes.writerevisions.js +129 -29
- package/dist_ts/nosqldb/engineidentity.d.ts +6 -0
- package/dist_ts/nosqldb/engineidentity.js +14 -1
- package/package.json +2 -2
- package/readme.md +11 -10
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.bootstraprecord.ts +3 -3
- package/ts/nosqldb/classes.connection.ts +5 -1
- package/ts/nosqldb/classes.db.ts +68 -0
- package/ts/nosqldb/classes.doc.ts +4 -4
- package/ts/nosqldb/classes.exactpersistence.ts +8 -7
- package/ts/nosqldb/classes.session.ts +92 -31
- package/ts/nosqldb/classes.writelog.ts +701 -0
- package/ts/nosqldb/classes.writeobserver.ts +62 -28
- package/ts/nosqldb/classes.writerevisions.ts +157 -31
- package/ts/nosqldb/engineidentity.ts +16 -0
|
@@ -77,28 +77,37 @@ interface IWriteObserver {
|
|
|
77
77
|
const maximumObservedFields = 64;
|
|
78
78
|
const observersByDb = new WeakMap<SmartdataDb, Set<IWriteObserver>>();
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
/**
|
|
81
|
+
* @internal Validates the top-level field names a report carries: 1 to 64
|
|
82
|
+
* distinct names, no `.` and no leading `$`. `subjectArg` names what requires
|
|
83
|
+
* them in the refusal.
|
|
84
|
+
*/
|
|
85
|
+
export const normalizeObservedFields = (fieldsArg: unknown, subjectArg: string): readonly string[] => {
|
|
86
86
|
if (
|
|
87
|
-
!Array.isArray(
|
|
88
|
-
||
|
|
89
|
-
||
|
|
90
|
-
||
|
|
87
|
+
!Array.isArray(fieldsArg)
|
|
88
|
+
|| fieldsArg.length === 0
|
|
89
|
+
|| fieldsArg.length > maximumObservedFields
|
|
90
|
+
|| fieldsArg.some((fieldArg) =>
|
|
91
91
|
typeof fieldArg !== 'string'
|
|
92
92
|
|| fieldArg.length === 0
|
|
93
93
|
|| fieldArg.includes('.')
|
|
94
94
|
|| fieldArg.startsWith('$'))
|
|
95
|
-
|| new Set(
|
|
95
|
+
|| new Set(fieldsArg).size !== fieldsArg.length
|
|
96
96
|
) {
|
|
97
97
|
throw new SmartdataPersistenceError('invalid_argument',
|
|
98
|
-
|
|
98
|
+
`${subjectArg} requires 1 to ${maximumObservedFields} distinct top-level field names.`);
|
|
99
|
+
}
|
|
100
|
+
return Object.freeze([...fieldsArg] as string[]);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const normalizeObserverOptions = (optionsArg: unknown): IWriteObserver => {
|
|
104
|
+
const { fields, listener } = assertDataOptions(optionsArg, ['fields', 'listener']);
|
|
105
|
+
if (typeof listener !== 'function') {
|
|
106
|
+
throw new SmartdataPersistenceError('invalid_argument',
|
|
107
|
+
'A SmartData write observer requires a listener function.');
|
|
99
108
|
}
|
|
100
109
|
return {
|
|
101
|
-
fields:
|
|
110
|
+
fields: normalizeObservedFields(fields, 'A SmartData write observer'),
|
|
102
111
|
listener: listener as IWriteObserver['listener'],
|
|
103
112
|
};
|
|
104
113
|
};
|
|
@@ -195,6 +204,41 @@ const collectSelectorAnchors = (
|
|
|
195
204
|
}
|
|
196
205
|
};
|
|
197
206
|
|
|
207
|
+
/**
|
|
208
|
+
* @internal The report of one acknowledged write with the values of `fieldsArg`,
|
|
209
|
+
* copied now and frozen.
|
|
210
|
+
*/
|
|
211
|
+
export const reportOfSmartdataWrite = (
|
|
212
|
+
writeArg: ISmartdataWrite,
|
|
213
|
+
fieldsArg: readonly string[],
|
|
214
|
+
): ISmartdataWriteReport => {
|
|
215
|
+
const anchors: Record<string, TSmartdataObservedValue> = {};
|
|
216
|
+
collectSelectorAnchors(writeArg.selector, new Set(fieldsArg), writeArg.identityDocumentIdField, anchors);
|
|
217
|
+
return Object.freeze({
|
|
218
|
+
collection: writeArg.collection,
|
|
219
|
+
kind: writeArg.kind,
|
|
220
|
+
documents: writeArg.documents === null
|
|
221
|
+
? null
|
|
222
|
+
: Object.freeze(writeArg.documents.map((documentArg) =>
|
|
223
|
+
Object.freeze(pickDocumentFields(documentArg, fieldsArg)))),
|
|
224
|
+
anchors: Object.freeze(anchors),
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/** @internal The same report with only the values of `fieldsArg`, which the report's fields include. */
|
|
229
|
+
export const narrowSmartdataWriteReport = (
|
|
230
|
+
reportArg: ISmartdataWriteReport,
|
|
231
|
+
fieldsArg: readonly string[],
|
|
232
|
+
): ISmartdataWriteReport => Object.freeze({
|
|
233
|
+
collection: reportArg.collection,
|
|
234
|
+
kind: reportArg.kind,
|
|
235
|
+
documents: reportArg.documents === null
|
|
236
|
+
? null
|
|
237
|
+
: Object.freeze(reportArg.documents.map((documentArg) =>
|
|
238
|
+
Object.freeze(pickDocumentFields(documentArg, fieldsArg)))),
|
|
239
|
+
anchors: Object.freeze(pickDocumentFields(reportArg.anchors, fieldsArg)),
|
|
240
|
+
});
|
|
241
|
+
|
|
198
242
|
const describeError = (errorArg: unknown): string =>
|
|
199
243
|
errorArg instanceof Error ? errorArg.message : String(errorArg);
|
|
200
244
|
|
|
@@ -212,25 +256,15 @@ export const captureSmartdataWrite = (
|
|
|
212
256
|
const registered = observersByDb.get(dbArg);
|
|
213
257
|
if (!registered || registered.size === 0) return undefined;
|
|
214
258
|
const observers = [...registered];
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
collectSelectorAnchors(writeArg.selector, new Set(fields), writeArg.identityDocumentIdField, anchors);
|
|
259
|
+
const report = reportOfSmartdataWrite(
|
|
260
|
+
writeArg,
|
|
261
|
+
[...new Set(observers.flatMap((observerArg) => observerArg.fields))],
|
|
262
|
+
);
|
|
220
263
|
return () => {
|
|
221
264
|
for (const observer of observers) {
|
|
222
265
|
if (!observersByDb.get(dbArg)?.has(observer)) continue;
|
|
223
|
-
const report: ISmartdataWriteReport = Object.freeze({
|
|
224
|
-
collection: writeArg.collection,
|
|
225
|
-
kind: writeArg.kind,
|
|
226
|
-
documents: documents === null
|
|
227
|
-
? null
|
|
228
|
-
: Object.freeze(documents.map((documentArg) =>
|
|
229
|
-
Object.freeze(pickDocumentFields(documentArg, observer.fields)))),
|
|
230
|
-
anchors: Object.freeze(pickDocumentFields(anchors, observer.fields)),
|
|
231
|
-
});
|
|
232
266
|
try {
|
|
233
|
-
observer.listener(report);
|
|
267
|
+
observer.listener(narrowSmartdataWriteReport(report, observer.fields));
|
|
234
268
|
} catch (errorArg) {
|
|
235
269
|
logger.log('error',
|
|
236
270
|
`A SmartData write observer threw on a write to "${writeArg.collection}": ${describeError(errorArg)}`);
|
|
@@ -4,12 +4,18 @@ import type { SmartdataDb } from './classes.db.js';
|
|
|
4
4
|
import { assertDataOptions } from './classes.operationbudget.js';
|
|
5
5
|
import type { SmartdataOperationDeadline } from './classes.operationdeadline.js';
|
|
6
6
|
import { SmartdataPersistenceError, isMongoDuplicateKeyError } from './classes.persistence.js';
|
|
7
|
+
import { logger } from './logging.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Write revisions let a read set tell writes of other processes apart from
|
|
10
11
|
* no writes at all. Every model write bumps a counter per collection and
|
|
11
12
|
* partition in the same transaction, and a read set compares the counters it
|
|
12
13
|
* read before its reads with the current ones.
|
|
14
|
+
*
|
|
15
|
+
* A bump inserts an entry of its own instead of incrementing a shared
|
|
16
|
+
* document, so concurrent transactions of one partition never conflict on its
|
|
17
|
+
* counter. A counter's value is its base document plus its entries; a
|
|
18
|
+
* background compaction folds entries into the base, which keeps the sum.
|
|
13
19
|
*/
|
|
14
20
|
export interface ISmartdataWriteRevisionsOptions {
|
|
15
21
|
/**
|
|
@@ -32,6 +38,17 @@ interface IWriteRevisionDocument {
|
|
|
32
38
|
|
|
33
39
|
/** The counter every write revision belongs to; a new generation restarts all counters. */
|
|
34
40
|
const epochId = '$epoch';
|
|
41
|
+
/**
|
|
42
|
+
* Joins a counter's id and the suffix of one of its entries. No counter id
|
|
43
|
+
* contains it, so a counter's base and entries are exactly the ids from the
|
|
44
|
+
* counter's own up to, not including, the id followed by `entryRangeEnd`.
|
|
45
|
+
*/
|
|
46
|
+
const entrySeparator = '\u0000';
|
|
47
|
+
const entryRangeEnd = '\u0001';
|
|
48
|
+
/** Entries one process adds to a counter before it folds the counter's entries into its base. */
|
|
49
|
+
const compactionEntries = 64;
|
|
50
|
+
const maximumCompactedEntries = 1024;
|
|
51
|
+
const compactionTimeoutMs = 5_000;
|
|
35
52
|
const maximumPartitionLength = 512;
|
|
36
53
|
const maximumPinnedPartitions = 64;
|
|
37
54
|
/** Beyond this many documents, a write by selector bumps the shared counter instead. */
|
|
@@ -68,7 +85,10 @@ export const isSmartdataWriteRevisionsActive = (dbArg: SmartdataDb): boolean =>
|
|
|
68
85
|
|
|
69
86
|
/** A string the counters can name, or `undefined`. */
|
|
70
87
|
const partitionOf = (valueArg: unknown): string | undefined =>
|
|
71
|
-
typeof valueArg === 'string'
|
|
88
|
+
typeof valueArg === 'string'
|
|
89
|
+
&& valueArg.length <= maximumPartitionLength
|
|
90
|
+
&& valueArg.isWellFormed()
|
|
91
|
+
&& !valueArg.includes(entrySeparator)
|
|
72
92
|
? valueArg
|
|
73
93
|
: undefined;
|
|
74
94
|
|
|
@@ -228,7 +248,8 @@ const connectedDb = (dbArg: SmartdataDb): plugins.mongodb.Db => {
|
|
|
228
248
|
/**
|
|
229
249
|
* @internal The current value of each snapshot key, or `undefined` for a key
|
|
230
250
|
* whose value cannot be known: a missing epoch means the counters restarted.
|
|
231
|
-
*
|
|
251
|
+
* Every counter and collection sum is one read, so a compaction that moves
|
|
252
|
+
* entries into a base never shows half done. Bounded by its own deadline.
|
|
232
253
|
*/
|
|
233
254
|
const readStoredWriteRevisionSnapshot = async (
|
|
234
255
|
dbArg: SmartdataDb,
|
|
@@ -242,36 +263,37 @@ const readStoredWriteRevisionSnapshot = async (
|
|
|
242
263
|
timeoutMode: 'cursorLifetime' as const,
|
|
243
264
|
signal,
|
|
244
265
|
});
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
{ _id: { $
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
266
|
+
// `$sum` skips a value that is not a number, so such values are counted apart.
|
|
267
|
+
const sumOf = async (fromArg: string, toArg: string): Promise<string | undefined> => {
|
|
268
|
+
const [sum] = await collection.aggregate<{ n: unknown; invalid: unknown }>([
|
|
269
|
+
{ $match: { _id: { $gte: fromArg, $lt: toArg } } },
|
|
270
|
+
{ $group: { _id: null, n: { $sum: '$n' }, invalid: { $sum: { $cond: [{ $isNumber: '$n' }, 0, 1] } } } },
|
|
271
|
+
], options()).toArray();
|
|
272
|
+
if (sum === undefined) return '0';
|
|
273
|
+
return sum.invalid === 0 && typeof sum.n === 'number' && Number.isSafeInteger(sum.n)
|
|
274
|
+
? String(sum.n)
|
|
275
|
+
: undefined;
|
|
276
|
+
};
|
|
277
|
+
const sums = keysArg.flatMap((keyArg): [string, Promise<string | undefined>][] => {
|
|
278
|
+
if (keyArg.startsWith('c')) {
|
|
279
|
+
const id = keyArg.slice(1);
|
|
280
|
+
return [[keyArg, sumOf(id, `${id}${entryRangeEnd}`)]];
|
|
281
|
+
}
|
|
282
|
+
if (keyArg.startsWith('r')) {
|
|
283
|
+
const collectionName = keyArg.slice(1);
|
|
284
|
+
return [[keyArg, sumOf(`${collectionName}$`, `${collectionName}%`)]];
|
|
285
|
+
}
|
|
286
|
+
return [];
|
|
287
|
+
});
|
|
288
|
+
const [epoch, ...values] = await Promise.all([
|
|
289
|
+
collection.findOne({ _id: epochId }, { projection: { n: 1, generation: 1 }, ...options() }),
|
|
290
|
+
...sums.map(([, sumArg]) => sumArg),
|
|
256
291
|
]);
|
|
257
|
-
const byId = new Map(counters.map((documentArg) => [documentArg._id, documentArg]));
|
|
258
292
|
const snapshot = new Map<string, string | undefined>();
|
|
259
|
-
const epoch = byId.get(epochId);
|
|
260
293
|
snapshot.set(epochKey, typeof epoch?.generation === 'string' && Number.isSafeInteger(epoch.n)
|
|
261
294
|
? `${epoch.generation}:${epoch.n}`
|
|
262
295
|
: undefined);
|
|
263
|
-
for (const
|
|
264
|
-
const counter = byId.get(id);
|
|
265
|
-
snapshot.set(counterKey(id), counter === undefined
|
|
266
|
-
? '0'
|
|
267
|
-
: Number.isSafeInteger(counter.n) ? String(counter.n) : undefined);
|
|
268
|
-
}
|
|
269
|
-
for (const [index, collectionArg] of ranges.entries()) {
|
|
270
|
-
const sum = sums[index].length === 0 ? 0 : sums[index][0].n;
|
|
271
|
-
snapshot.set(rangeKey(collectionArg), typeof sum === 'number' && Number.isSafeInteger(sum)
|
|
272
|
-
? String(sum)
|
|
273
|
-
: undefined);
|
|
274
|
-
}
|
|
296
|
+
for (const [index, [key]] of sums.entries()) snapshot.set(key, values[index]);
|
|
275
297
|
return snapshot;
|
|
276
298
|
};
|
|
277
299
|
|
|
@@ -342,8 +364,8 @@ export const bumpSmartdataWriteRevisionEpoch = async (
|
|
|
342
364
|
|
|
343
365
|
/**
|
|
344
366
|
* @internal Bumps the counters a transaction's writes touched, inside that
|
|
345
|
-
* transaction, right before its commit
|
|
346
|
-
*
|
|
367
|
+
* transaction, right before its commit: one new entry per counter, which no
|
|
368
|
+
* concurrent transaction writes.
|
|
347
369
|
*/
|
|
348
370
|
export const flushSmartdataWriteRevisions = async (
|
|
349
371
|
mongoDbArg: plugins.mongodb.Db,
|
|
@@ -352,8 +374,9 @@ export const flushSmartdataWriteRevisions = async (
|
|
|
352
374
|
signalArg: AbortSignal | undefined,
|
|
353
375
|
): Promise<void> => {
|
|
354
376
|
if (idsArg.size === 0) return;
|
|
355
|
-
await revisionsCollection(mongoDbArg).
|
|
356
|
-
|
|
377
|
+
await revisionsCollection(mongoDbArg).insertMany([...idsArg].map((idArg) => ({
|
|
378
|
+
_id: `${idArg}${entrySeparator}${plugins.nodeCrypto.randomUUID()}`,
|
|
379
|
+
n: 1,
|
|
357
380
|
})), {
|
|
358
381
|
ordered: true,
|
|
359
382
|
session: rawSessionArg,
|
|
@@ -361,6 +384,109 @@ export const flushSmartdataWriteRevisions = async (
|
|
|
361
384
|
});
|
|
362
385
|
};
|
|
363
386
|
|
|
387
|
+
interface IWriteRevisionCompactions {
|
|
388
|
+
/** Committed entries this process added per counter since it last compacted it. */
|
|
389
|
+
entries: Map<string, number>;
|
|
390
|
+
running: Map<string, Promise<void>>;
|
|
391
|
+
controller: AbortController;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const revisionCompactions = new WeakMap<SmartdataDb, IWriteRevisionCompactions>();
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Folds up to `maximumCompactedEntries` entries of one counter into its base
|
|
398
|
+
* in one transaction, which keeps the counter's value. A concurrent compaction
|
|
399
|
+
* of the same counter conflicts on the base and on the entries, and the driver
|
|
400
|
+
* retries it within the deadline; one that still fails leaves the entries for
|
|
401
|
+
* the next.
|
|
402
|
+
*/
|
|
403
|
+
const compactWriteRevisionEntries = async (
|
|
404
|
+
dbArg: SmartdataDb,
|
|
405
|
+
idArg: string,
|
|
406
|
+
signalArg: AbortSignal,
|
|
407
|
+
): Promise<void> => {
|
|
408
|
+
const collection = revisionsCollection(connectedDb(dbArg));
|
|
409
|
+
const abortable: plugins.mongodb.Abortable = { signal: signalArg };
|
|
410
|
+
const session = dbArg.mongoDbClient.startSession();
|
|
411
|
+
try {
|
|
412
|
+
await session.withTransaction(async () => {
|
|
413
|
+
const entries = await collection.find(
|
|
414
|
+
{ _id: { $gt: `${idArg}${entrySeparator}`, $lt: `${idArg}${entryRangeEnd}` } },
|
|
415
|
+
{ projection: { n: 1 }, limit: maximumCompactedEntries, session, ...abortable },
|
|
416
|
+
).toArray();
|
|
417
|
+
if (entries.length === 0) return;
|
|
418
|
+
let sum = 0;
|
|
419
|
+
for (const entry of entries) {
|
|
420
|
+
if (!Number.isSafeInteger(entry.n)) {
|
|
421
|
+
throw new SmartdataPersistenceError('invalid_document',
|
|
422
|
+
`Write-revision entry "${entry._id}" holds no safe integer.`);
|
|
423
|
+
}
|
|
424
|
+
sum += entry.n;
|
|
425
|
+
if (!Number.isSafeInteger(sum)) {
|
|
426
|
+
throw new SmartdataPersistenceError('invalid_document',
|
|
427
|
+
`Write-revision entries of "${idArg}" do not sum to a safe integer.`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
await collection.deleteMany(
|
|
431
|
+
{ _id: { $in: entries.map((entryArg) => entryArg._id) } },
|
|
432
|
+
{ session, ...abortable },
|
|
433
|
+
);
|
|
434
|
+
await collection.updateOne(
|
|
435
|
+
{ _id: idArg },
|
|
436
|
+
{ $inc: { n: sum } },
|
|
437
|
+
{ upsert: true, session, ...abortable },
|
|
438
|
+
);
|
|
439
|
+
}, { timeoutMS: compactionTimeoutMs });
|
|
440
|
+
} finally {
|
|
441
|
+
await session.endSession();
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @internal Counts the entries a committed transaction of this process added,
|
|
447
|
+
* and starts a compaction of each counter that reached `compactionEntries`.
|
|
448
|
+
* Compactions run in the background; `stopSmartdataWriteRevisionCompactions`
|
|
449
|
+
* ends them.
|
|
450
|
+
*/
|
|
451
|
+
export const noteSmartdataWriteRevisionEntries = (
|
|
452
|
+
dbArg: SmartdataDb,
|
|
453
|
+
idsArg: ReadonlySet<string>,
|
|
454
|
+
): void => {
|
|
455
|
+
if (idsArg.size === 0 || dbArg.status !== 'connected' || !isSmartdataWriteRevisionsActive(dbArg)) return;
|
|
456
|
+
let state = revisionCompactions.get(dbArg);
|
|
457
|
+
if (!state) {
|
|
458
|
+
state = { entries: new Map(), running: new Map(), controller: new AbortController() };
|
|
459
|
+
revisionCompactions.set(dbArg, state);
|
|
460
|
+
}
|
|
461
|
+
const compactions = state;
|
|
462
|
+
for (const id of idsArg) {
|
|
463
|
+
const entries = (compactions.entries.get(id) ?? 0) + 1;
|
|
464
|
+
if (entries < compactionEntries || compactions.running.has(id)) {
|
|
465
|
+
compactions.entries.set(id, entries);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
compactions.entries.delete(id);
|
|
469
|
+
const signal = compactions.controller.signal;
|
|
470
|
+
const compaction = compactWriteRevisionEntries(dbArg, id, signal)
|
|
471
|
+
.catch((errorArg) => {
|
|
472
|
+
if (signal.aborted) return;
|
|
473
|
+
logger.log('warn', `Compacting the write revisions of "${id}" failed; a later compaction retries: ${
|
|
474
|
+
errorArg instanceof Error ? errorArg.message : String(errorArg)}`);
|
|
475
|
+
})
|
|
476
|
+
.finally(() => { compactions.running.delete(id); });
|
|
477
|
+
compactions.running.set(id, compaction);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
/** @internal Aborts the database's compactions and waits until they settled; `close()` calls it. */
|
|
482
|
+
export const stopSmartdataWriteRevisionCompactions = async (dbArg: SmartdataDb): Promise<void> => {
|
|
483
|
+
const state = revisionCompactions.get(dbArg);
|
|
484
|
+
if (!state) return;
|
|
485
|
+
revisionCompactions.delete(dbArg);
|
|
486
|
+
state.controller.abort();
|
|
487
|
+
await Promise.allSettled(state.running.values());
|
|
488
|
+
};
|
|
489
|
+
|
|
364
490
|
/**
|
|
365
491
|
* @internal The counters of the documents a write by selector is about to
|
|
366
492
|
* change, read in its transaction before it runs. Snapshot isolation makes
|
|
@@ -32,6 +32,12 @@ export const unidentifiedSmartdataEngine: ISmartdataEngineIdentity = Object.free
|
|
|
32
32
|
*/
|
|
33
33
|
const compositeGroupingEngineRelease = '10.5.0';
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The NoSQLDB release that publishes its durable commit journal through the
|
|
37
|
+
* `getCommittedWrites` command: @lossless.org/nosqldb 10.9.0.
|
|
38
|
+
*/
|
|
39
|
+
const commitJournalEngineRelease = '10.9.0';
|
|
40
|
+
|
|
35
41
|
const parseEngineVersion = (versionArg: string): readonly number[] | undefined => {
|
|
36
42
|
const parts = versionArg.split('.');
|
|
37
43
|
if (parts.length !== 3) {
|
|
@@ -91,6 +97,16 @@ export const smartdataCompositeGrouping = (
|
|
|
91
97
|
}
|
|
92
98
|
};
|
|
93
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Whether the identified engine publishes a durable, commit-ordered journal of
|
|
102
|
+
* the transactions it committed. Only NoSQLDB does, from 10.9.0 on; MongoDB's
|
|
103
|
+
* change streams are a different contract and are not used for it.
|
|
104
|
+
*/
|
|
105
|
+
export const smartdataCommitJournal = (identityArg: ISmartdataEngineIdentity): boolean =>
|
|
106
|
+
identityArg.product === 'NoSQLDB'
|
|
107
|
+
&& identityArg.version !== null
|
|
108
|
+
&& engineVersionReaches(identityArg.version, commitJournalEngineRelease);
|
|
109
|
+
|
|
94
110
|
export const composeSmartdataCapabilities = (
|
|
95
111
|
backendArg: TSmartdataEngineBackend,
|
|
96
112
|
compositeGroupingArg: TCompositeGrouping,
|