@lossless.org/client 1.21.1 → 1.22.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.atomicdelete.js +8 -4
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +19 -4
- package/dist_ts/nosqldb/classes.atomicupdate.js +16 -5
- package/dist_ts/nosqldb/classes.bootstraprecord.js +31 -5
- package/dist_ts/nosqldb/classes.collection.js +17 -14
- 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 +27 -2
- package/dist_ts/nosqldb/classes.db.js +42 -3
- package/dist_ts/nosqldb/classes.doc.d.ts +3 -1
- package/dist_ts/nosqldb/classes.doc.js +15 -9
- package/dist_ts/nosqldb/classes.exactpersistence.d.ts +13 -2
- package/dist_ts/nosqldb/classes.exactpersistence.js +64 -17
- package/dist_ts/nosqldb/classes.operationdeadline.d.ts +5 -0
- package/dist_ts/nosqldb/classes.operationdeadline.js +8 -1
- package/dist_ts/nosqldb/classes.readtracker.d.ts +26 -0
- package/dist_ts/nosqldb/classes.readtracker.js +124 -1
- package/dist_ts/nosqldb/classes.session.d.ts +34 -5
- package/dist_ts/nosqldb/classes.session.js +134 -8
- package/dist_ts/nosqldb/classes.storedinspection.js +7 -4
- package/dist_ts/nosqldb/classes.writeobserver.d.ts +5 -0
- package/dist_ts/nosqldb/classes.writeobserver.js +1 -1
- package/dist_ts/nosqldb/classes.writerevisions.d.ts +81 -0
- package/dist_ts/nosqldb/classes.writerevisions.js +246 -0
- package/package.json +4 -4
- package/readme.md +1 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.atomicdelete.ts +11 -2
- package/ts/nosqldb/classes.atomicfindoneandupdate.ts +25 -2
- package/ts/nosqldb/classes.atomicupdate.ts +25 -3
- package/ts/nosqldb/classes.bootstraprecord.ts +29 -4
- package/ts/nosqldb/classes.collection.ts +20 -12
- package/ts/nosqldb/classes.connection.ts +5 -1
- package/ts/nosqldb/classes.db.ts +65 -2
- package/ts/nosqldb/classes.doc.ts +16 -8
- package/ts/nosqldb/classes.exactpersistence.ts +83 -21
- package/ts/nosqldb/classes.operationdeadline.ts +8 -0
- package/ts/nosqldb/classes.readtracker.ts +139 -0
- package/ts/nosqldb/classes.session.ts +191 -8
- package/ts/nosqldb/classes.storedinspection.ts +6 -3
- package/ts/nosqldb/classes.writeobserver.ts +5 -0
- package/ts/nosqldb/classes.writerevisions.ts +328 -0
|
@@ -34,6 +34,7 @@ import { hasSmartdataWriteObservers } from './classes.writeobserver.js';
|
|
|
34
34
|
import {
|
|
35
35
|
leaseOrdinarySmartdataSession,
|
|
36
36
|
reportSmartdataCollectionWrite,
|
|
37
|
+
readPreviousWriteRevisions,
|
|
37
38
|
runWithOrdinarySmartdataSession,
|
|
38
39
|
trackSmartdataRead,
|
|
39
40
|
type TSmartdataOrdinarySession,
|
|
@@ -1955,6 +1956,7 @@ export class SmartdataCollection<T> {
|
|
|
1955
1956
|
{
|
|
1956
1957
|
ordinaryWrite: optionsArg.ordinaryWrite,
|
|
1957
1958
|
prepared: this.isInitializedForCurrentDatabase(),
|
|
1959
|
+
prepare: () => this.init(),
|
|
1958
1960
|
collectionName: this.collectionName,
|
|
1959
1961
|
timeoutMS: optionsArg.timeoutMS,
|
|
1960
1962
|
signal: optionsArg.signal,
|
|
@@ -2423,11 +2425,11 @@ export class SmartdataCollection<T> {
|
|
|
2423
2425
|
{ ordinaryWrite: false, timeoutMS: opts?.timeoutMS },
|
|
2424
2426
|
async (rawSessionArg, deadlineArg) => {
|
|
2425
2427
|
await deadlineArg.join(this.init());
|
|
2426
|
-
const tracked = trackSmartdataRead(this, opts?.session, {
|
|
2428
|
+
const tracked = await trackSmartdataRead(this, opts?.session, {
|
|
2427
2429
|
kind: 'documents',
|
|
2428
2430
|
filter: filterObject,
|
|
2429
2431
|
limited: true,
|
|
2430
|
-
});
|
|
2432
|
+
}, deadlineArg);
|
|
2431
2433
|
const document = await this.mongoDbCollection.findOne(filterObject, {
|
|
2432
2434
|
projection: opts?.projection,
|
|
2433
2435
|
maxTimeMS: opts?.maxTimeMS,
|
|
@@ -2457,7 +2459,7 @@ export class SmartdataCollection<T> {
|
|
|
2457
2459
|
});
|
|
2458
2460
|
try {
|
|
2459
2461
|
await this.init();
|
|
2460
|
-
const tracked = trackSmartdataRead(this, opts?.session, {
|
|
2462
|
+
const tracked = await trackSmartdataRead(this, opts?.session, {
|
|
2461
2463
|
kind: 'documents',
|
|
2462
2464
|
filter: filterObjectArg,
|
|
2463
2465
|
});
|
|
@@ -2483,10 +2485,10 @@ export class SmartdataCollection<T> {
|
|
|
2483
2485
|
{ ordinaryWrite: false, timeoutMS: opts?.timeoutMS },
|
|
2484
2486
|
async (rawSessionArg, deadlineArg) => {
|
|
2485
2487
|
await deadlineArg.join(this.init());
|
|
2486
|
-
const tracked = trackSmartdataRead(this, opts?.session, {
|
|
2488
|
+
const tracked = await trackSmartdataRead(this, opts?.session, {
|
|
2487
2489
|
kind: 'documents',
|
|
2488
2490
|
filter: filterObject,
|
|
2489
|
-
});
|
|
2491
|
+
}, deadlineArg);
|
|
2490
2492
|
const cursor = this.mongoDbCollection.find(filterObject, {
|
|
2491
2493
|
session: rawSessionArg,
|
|
2492
2494
|
...deadlineArg.cursorOptions(),
|
|
@@ -2692,11 +2694,11 @@ export class SmartdataCollection<T> {
|
|
|
2692
2694
|
async (rawSessionArg, deadlineArg) => {
|
|
2693
2695
|
await deadlineArg.join(this.init());
|
|
2694
2696
|
// Totals depend on every matching document, not on which ones come back.
|
|
2695
|
-
trackSmartdataRead(this, opts?.session, {
|
|
2697
|
+
await trackSmartdataRead(this, opts?.session, {
|
|
2696
2698
|
kind: 'documents',
|
|
2697
2699
|
filter: matchArg ?? {},
|
|
2698
2700
|
matches: true,
|
|
2699
|
-
});
|
|
2701
|
+
}, deadlineArg);
|
|
2700
2702
|
const pipeline: plugins.mongodb.Document[] = [];
|
|
2701
2703
|
if (matchArg && Object.keys(matchArg).length > 0) {
|
|
2702
2704
|
pipeline.push({ $match: matchArg });
|
|
@@ -2921,6 +2923,9 @@ export class SmartdataCollection<T> {
|
|
|
2921
2923
|
: {}),
|
|
2922
2924
|
};
|
|
2923
2925
|
try {
|
|
2926
|
+
const previousPartitionsRead = await readPreviousWriteRevisions(
|
|
2927
|
+
this.smartdataDb, this.mongoDbCollection, rawSessionArg, identifiableObject, deadlineArg,
|
|
2928
|
+
);
|
|
2924
2929
|
const result = await this.mongoDbCollection.updateOne(
|
|
2925
2930
|
identifiableObject,
|
|
2926
2931
|
update,
|
|
@@ -2933,7 +2938,7 @@ export class SmartdataCollection<T> {
|
|
|
2933
2938
|
result.upsertedCount > 0 ? 'insert' : 'update',
|
|
2934
2939
|
[saveableObject],
|
|
2935
2940
|
identifiableObject,
|
|
2936
|
-
{ images: 'none', update },
|
|
2941
|
+
{ images: 'none', update, ...(previousPartitionsRead ? { previousPartitionsRead } : {}) },
|
|
2937
2942
|
);
|
|
2938
2943
|
}
|
|
2939
2944
|
return result;
|
|
@@ -2963,6 +2968,9 @@ export class SmartdataCollection<T> {
|
|
|
2963
2968
|
const observedDocument = hasSmartdataWriteObservers(this.smartdataDb)
|
|
2964
2969
|
? await dbDocArg.createSavableObject() as object
|
|
2965
2970
|
: undefined;
|
|
2971
|
+
const previousPartitionsRead = await readPreviousWriteRevisions(
|
|
2972
|
+
this.smartdataDb, this.mongoDbCollection, rawSessionArg, identifiableObject, deadlineArg,
|
|
2973
|
+
);
|
|
2966
2974
|
const result = await this.mongoDbCollection.deleteOne(identifiableObject, {
|
|
2967
2975
|
session: rawSessionArg,
|
|
2968
2976
|
...deadlineArg.commandOptions(),
|
|
@@ -2974,7 +2982,7 @@ export class SmartdataCollection<T> {
|
|
|
2974
2982
|
'delete',
|
|
2975
2983
|
observedDocument ? [observedDocument] : null,
|
|
2976
2984
|
identifiableObject,
|
|
2977
|
-
{ images: 'none' },
|
|
2985
|
+
{ images: 'none', ...(previousPartitionsRead ? { previousPartitionsRead } : {}) },
|
|
2978
2986
|
);
|
|
2979
2987
|
}
|
|
2980
2988
|
return result;
|
|
@@ -3026,11 +3034,11 @@ export class SmartdataCollection<T> {
|
|
|
3026
3034
|
{ ordinaryWrite: false, timeoutMS: opts?.timeoutMS, signal: opts?.signal },
|
|
3027
3035
|
async (rawSessionArg, deadlineArg) => {
|
|
3028
3036
|
await deadlineArg.join(this.init());
|
|
3029
|
-
const tracked = trackSmartdataRead(this, opts?.session, {
|
|
3037
|
+
const tracked = await trackSmartdataRead(this, opts?.session, {
|
|
3030
3038
|
kind: 'count',
|
|
3031
3039
|
filter: filterObject,
|
|
3032
3040
|
hint: opts?.hint,
|
|
3033
|
-
});
|
|
3041
|
+
}, deadlineArg);
|
|
3034
3042
|
const pipeline: plugins.mongodb.Document[] = [{ $match: filterObject }];
|
|
3035
3043
|
if (opts?.limit !== undefined) {
|
|
3036
3044
|
pipeline.push({ $limit: opts.limit });
|
|
@@ -3108,7 +3116,7 @@ export class SmartdataCollection<T> {
|
|
|
3108
3116
|
duplicateFields: Array<{ field: string; duplicateValues: number }>;
|
|
3109
3117
|
}> {
|
|
3110
3118
|
await this.init();
|
|
3111
|
-
trackSmartdataRead(this, undefined, { kind: 'collection' });
|
|
3119
|
+
await trackSmartdataRead(this, undefined, { kind: 'collection' });
|
|
3112
3120
|
const result = {
|
|
3113
3121
|
ok: true,
|
|
3114
3122
|
estimatedCount: 0,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SmartdataDb } from './classes.db.js';
|
|
2
|
+
import type { ISmartdataWriteRevisionsOptions } from './classes.writerevisions.js';
|
|
2
3
|
import {
|
|
3
4
|
composeSmartdataCapabilities,
|
|
4
5
|
smartdataCompositeGrouping,
|
|
@@ -14,6 +15,8 @@ export interface INoSqlConnectionOptions {
|
|
|
14
15
|
username?: string;
|
|
15
16
|
password?: string;
|
|
16
17
|
connectionLimit?: number;
|
|
18
|
+
/** See `ISmartdataDbOptions.writeRevisions`. */
|
|
19
|
+
writeRevisions?: ISmartdataWriteRevisionsOptions;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export class NoSqlConnection extends SmartdataDb {
|
|
@@ -24,7 +27,8 @@ export class NoSqlConnection extends SmartdataDb {
|
|
|
24
27
|
constructor(public readonly options: INoSqlConnectionOptions) {
|
|
25
28
|
super({ mongoDbUrl: options.url, mongoDbName: options.database,
|
|
26
29
|
mongoDbUser: options.username, mongoDbPass: options.password,
|
|
27
|
-
...(options.connectionLimit === undefined ? {} : { maxPoolSize: options.connectionLimit })
|
|
30
|
+
...(options.connectionLimit === undefined ? {} : { maxPoolSize: options.connectionLimit }),
|
|
31
|
+
...(options.writeRevisions === undefined ? {} : { writeRevisions: options.writeRevisions }) });
|
|
28
32
|
this.backend = options.backend;
|
|
29
33
|
this.declaredCapabilities = composeSmartdataCapabilities(this.backend,
|
|
30
34
|
smartdataCompositeGrouping(this.engineIdentity));
|
package/ts/nosqldb/classes.db.ts
CHANGED
|
@@ -29,6 +29,17 @@ import {
|
|
|
29
29
|
trackSmartdataReads,
|
|
30
30
|
type ISmartdataTrackedReads,
|
|
31
31
|
} from './classes.readtracker.js';
|
|
32
|
+
import {
|
|
33
|
+
bumpSmartdataWriteRevisionEpoch,
|
|
34
|
+
configureSmartdataWriteRevisions,
|
|
35
|
+
ensureSmartdataWriteRevisions,
|
|
36
|
+
isSmartdataWriteRevisionsActive,
|
|
37
|
+
type ISmartdataWriteRevisionsOptions,
|
|
38
|
+
} from './classes.writerevisions.js';
|
|
39
|
+
import {
|
|
40
|
+
requireOperationSignal,
|
|
41
|
+
requireOperationTimeoutMS,
|
|
42
|
+
} from './classes.operationdeadline.js';
|
|
32
43
|
|
|
33
44
|
export { SmartdataSession } from './classes.session.js';
|
|
34
45
|
export { SmartdataReadSet } from './classes.readtracker.js';
|
|
@@ -38,6 +49,7 @@ export type {
|
|
|
38
49
|
TSmartdataOrdinarySession,
|
|
39
50
|
TSmartdataTransactionCommitOutcome,
|
|
40
51
|
} from './classes.session.js';
|
|
52
|
+
export type { ISmartdataWriteRevisionsOptions } from './classes.writerevisions.js';
|
|
41
53
|
export type {
|
|
42
54
|
ISmartdataWriteObserverOptions,
|
|
43
55
|
ISmartdataWriteReport,
|
|
@@ -91,6 +103,19 @@ export interface ISmartdataDbOptions {
|
|
|
91
103
|
serverSelectionTimeoutMS?: number;
|
|
92
104
|
/** Milliseconds a socket may stay inactive before it is closed. Default 30000. */
|
|
93
105
|
socketTimeoutMS?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Keeps write revisions, so `reads.isCurrent()` of `trackReads()` also
|
|
108
|
+
* notices writes of other processes. Every process that writes the database
|
|
109
|
+
* must enable it with the same `partitionField`; see `trackReads()`. Read
|
|
110
|
+
* once, by the constructor.
|
|
111
|
+
*/
|
|
112
|
+
writeRevisions?: ISmartdataWriteRevisionsOptions;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ISmartdataInvalidateTrackedReadsOptions {
|
|
116
|
+
/** Client deadline of the write-revision epoch's bump (1..120000 ms). */
|
|
117
|
+
timeoutMS?: number;
|
|
118
|
+
signal?: AbortSignal;
|
|
94
119
|
}
|
|
95
120
|
|
|
96
121
|
export interface ISmartdataReadinessOptions {
|
|
@@ -220,6 +245,7 @@ export class SmartdataDb {
|
|
|
220
245
|
private capabilitiesState: ICapabilities | undefined;
|
|
221
246
|
|
|
222
247
|
constructor(smartdataOptions: ISmartdataDbOptions) {
|
|
248
|
+
configureSmartdataWriteRevisions(this, smartdataOptions.writeRevisions);
|
|
223
249
|
this.smartdataOptions = smartdataOptions;
|
|
224
250
|
this.status = 'initial';
|
|
225
251
|
void this.statusConnectedDeferred.promise.catch(() => {});
|
|
@@ -312,6 +338,10 @@ export class SmartdataDb {
|
|
|
312
338
|
this.applyEngineIdentity(await probeSmartdataEngineIdentity(candidateDb, budget));
|
|
313
339
|
// The probe absorbs its own failures, so a budget that expired during it must still refuse here.
|
|
314
340
|
budget?.check();
|
|
341
|
+
if (isSmartdataWriteRevisionsActive(this)) {
|
|
342
|
+
await ensureSmartdataWriteRevisions(candidateDb, budget?.operationOptions());
|
|
343
|
+
budget?.check();
|
|
344
|
+
}
|
|
315
345
|
this.mongoDbClient = candidateClient;
|
|
316
346
|
this.mongoDb = candidateDb;
|
|
317
347
|
candidateClient = undefined;
|
|
@@ -562,8 +592,11 @@ export class SmartdataDb {
|
|
|
562
592
|
* as one that did. A cache keyed by `reads` rebuilds only what such a write
|
|
563
593
|
* touched.
|
|
564
594
|
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
595
|
+
* `reads.changed` does not follow writes of other processes, raw driver
|
|
596
|
+
* access or migrations. With the `writeRevisions` option, the awaited
|
|
597
|
+
* `reads.isCurrent()` also answers for the model writes of every process
|
|
598
|
+
* that enables it, and for raw writes once `invalidateTrackedReads()` states
|
|
599
|
+
* them. A read inside a transaction, a read of another database and a
|
|
567
600
|
* write made inside `fnArg` mark the set changed at once. Queries are
|
|
568
601
|
* evaluated by MongoDB's simple binary comparison, so a read of a view or of
|
|
569
602
|
* a collection with a default collation marks the set changed as well; each
|
|
@@ -576,6 +609,36 @@ export class SmartdataDb {
|
|
|
576
609
|
return trackSmartdataReads(this, fnArg);
|
|
577
610
|
}
|
|
578
611
|
|
|
612
|
+
/**
|
|
613
|
+
* States that something wrote the database behind its models, such as a
|
|
614
|
+
* migration or an operator command on the raw driver: every open read set
|
|
615
|
+
* of this process changes, and with `writeRevisions` active the database's
|
|
616
|
+
* epoch is bumped, so `isCurrent()` turns false in every process. Call it
|
|
617
|
+
* after the raw writes; a raw write in flight meanwhile can still be missed.
|
|
618
|
+
*/
|
|
619
|
+
public async invalidateTrackedReads(
|
|
620
|
+
optionsArg: ISmartdataInvalidateTrackedReadsOptions = {},
|
|
621
|
+
): Promise<void> {
|
|
622
|
+
const { timeoutMS, signal } = assertDataOptions(optionsArg, ['timeoutMS', 'signal']);
|
|
623
|
+
const options = {
|
|
624
|
+
timeoutMS: requireOperationTimeoutMS(timeoutMS, 'invalidateTrackedReads timeoutMS'),
|
|
625
|
+
signal: requireOperationSignal(signal, 'invalidateTrackedReads signal'),
|
|
626
|
+
};
|
|
627
|
+
changeSmartdataReadSetsOfDb(this);
|
|
628
|
+
if (!isSmartdataWriteRevisionsActive(this)) return;
|
|
629
|
+
try {
|
|
630
|
+
await bumpSmartdataWriteRevisionEpoch(this, {
|
|
631
|
+
...(options.timeoutMS === undefined ? {} : { timeoutMS: options.timeoutMS }),
|
|
632
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
633
|
+
});
|
|
634
|
+
} catch (errorArg) {
|
|
635
|
+
if (isMongoDeadlineError(errorArg)) {
|
|
636
|
+
throw createSmartdataTimeoutError('Bumping the write-revision epoch exceeded its client deadline.', errorArg);
|
|
637
|
+
}
|
|
638
|
+
throw errorArg;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
579
642
|
/**
|
|
580
643
|
* Creates a narrowly scoped MongoDB administration facade bound to this
|
|
581
644
|
* SmartData lifecycle. Applications never need direct driver access.
|
|
@@ -30,6 +30,7 @@ import { SmartdataDbWatcher } from './classes.watcher.js';
|
|
|
30
30
|
import { SmartdataLuceneAdapter } from './classes.lucene.adapter.js';
|
|
31
31
|
import { executeAtomicDelete, executeAtomicDeleteMany } from './classes.atomicdelete.js';
|
|
32
32
|
import { requireOperationSignal } from './classes.operationdeadline.js';
|
|
33
|
+
import { isSmartdataWriteRevisionsActive } from './classes.writerevisions.js';
|
|
33
34
|
import {
|
|
34
35
|
executeAtomicFindOneAndReplace, executeAtomicFindOneAndUpdate,
|
|
35
36
|
} from './classes.atomicfindoneandupdate.js';
|
|
@@ -4602,7 +4603,9 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4602
4603
|
* unless the caller supplies a transaction session. With ordered semantics
|
|
4603
4604
|
* (the default) MongoDB stops at the first failure; documents MongoDB
|
|
4604
4605
|
* reports as inserted are marked database-backed even when the batch fails
|
|
4605
|
-
* part-way, so callers can compensate precisely.
|
|
4606
|
+
* part-way, so callers can compensate precisely. On a database with active
|
|
4607
|
+
* write revisions, a batch outside a transaction runs in one of its own and
|
|
4608
|
+
* inserts every document or none.
|
|
4606
4609
|
*/
|
|
4607
4610
|
public static async insertMany<T extends SmartDataDbDoc<any, any>>(
|
|
4608
4611
|
this: plugins.tsclass.typeFest.Class<T>,
|
|
@@ -4674,6 +4677,9 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4674
4677
|
}
|
|
4675
4678
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
|
4676
4679
|
const inTransaction = isOrdinarySessionInTransaction(opts?.session);
|
|
4680
|
+
// Outside a transaction, a database with write revisions runs the batch
|
|
4681
|
+
// in one of its own, so a failed batch inserted nothing.
|
|
4682
|
+
const insertsWhole = !inTransaction && isSmartdataWriteRevisionsActive(collection.smartdataDb);
|
|
4677
4683
|
const markInsertedDocuments = (
|
|
4678
4684
|
insertedIdsArg: Record<number, unknown> | undefined,
|
|
4679
4685
|
): void => {
|
|
@@ -4701,7 +4707,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
4701
4707
|
const bulkError = error instanceof SmartdataPersistenceError
|
|
4702
4708
|
? error.cause
|
|
4703
4709
|
: error;
|
|
4704
|
-
if (bulkError instanceof plugins.mongodb.MongoBulkWriteError) {
|
|
4710
|
+
if (bulkError instanceof plugins.mongodb.MongoBulkWriteError && !insertsWhole) {
|
|
4705
4711
|
markInsertedDocuments(
|
|
4706
4712
|
bulkError.result?.insertedIds as unknown as
|
|
4707
4713
|
| Record<number, unknown>
|
|
@@ -6057,12 +6063,12 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
6057
6063
|
}, async (rawSessionArg, deadlineArg) => {
|
|
6058
6064
|
await deadlineArg.join(collection.init());
|
|
6059
6065
|
const sort = { [optionsArg.sortField]: sortDirection, [uniqueField]: sortDirection } as const;
|
|
6060
|
-
const tracked = trackSmartdataRead(collection, optionsArg.session, {
|
|
6066
|
+
const tracked = await trackSmartdataRead(collection, optionsArg.session, {
|
|
6061
6067
|
kind: 'documents',
|
|
6062
6068
|
filter: selector,
|
|
6063
6069
|
sort,
|
|
6064
6070
|
limited: true,
|
|
6065
|
-
});
|
|
6071
|
+
}, deadlineArg);
|
|
6066
6072
|
const rawCursor = collection.mongoDbCollection
|
|
6067
6073
|
.find(selector, { session: rawSessionArg, ...deadlineArg.cursorOptions() })
|
|
6068
6074
|
.sort(sort)
|
|
@@ -6080,7 +6086,9 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
6080
6086
|
);
|
|
6081
6087
|
const lastRow: any = pageRows[pageRows.length - 1];
|
|
6082
6088
|
const countTracked = optionsArg.withTotal
|
|
6083
|
-
? trackSmartdataRead(
|
|
6089
|
+
? await trackSmartdataRead(
|
|
6090
|
+
collection, optionsArg.session, { kind: 'count', filter: baseSelector }, deadlineArg,
|
|
6091
|
+
)
|
|
6084
6092
|
: undefined;
|
|
6085
6093
|
const total = optionsArg.withTotal
|
|
6086
6094
|
? await collection.mongoDbCollection.countDocuments(baseSelector, {
|
|
@@ -6230,7 +6238,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
6230
6238
|
const filter = (this as any).normalizeReadFilter(filterArg);
|
|
6231
6239
|
// A modifier changes the query in ways a read set cannot follow, and a
|
|
6232
6240
|
// skipped page depends on the documents before it.
|
|
6233
|
-
const tracked = trackSmartdataRead(collection, session, modifier
|
|
6241
|
+
const tracked = await trackSmartdataRead(collection, session, modifier
|
|
6234
6242
|
? { kind: 'collection' }
|
|
6235
6243
|
: { kind: 'documents', filter, sort, hint, matches: skip !== undefined && skip > 0, limited: Boolean(limit) });
|
|
6236
6244
|
let rawCursor: plugins.mongodb.FindCursor<any> =
|
|
@@ -6332,9 +6340,9 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
6332
6340
|
defaultTimeoutMS: 5_000,
|
|
6333
6341
|
signal: opts.signal,
|
|
6334
6342
|
}, async (rawSessionArg, deadlineArg) => {
|
|
6335
|
-
const tracked = trackSmartdataRead(collection, opts.session, {
|
|
6343
|
+
const tracked = await trackSmartdataRead(collection, opts.session, {
|
|
6336
6344
|
kind: 'documents', filter, sort, hint, matches: skip > 0, limited: true,
|
|
6337
|
-
});
|
|
6345
|
+
}, deadlineArg);
|
|
6338
6346
|
const rawCursor = collection.mongoDbCollection.find(filter, {
|
|
6339
6347
|
projection, sort, hint, skip, limit, maxTimeMS,
|
|
6340
6348
|
...deadlineArg.cursorOptions(), session: rawSessionArg,
|
|
@@ -6,11 +6,14 @@ import { getOrdinaryPersistencePolicy } from './classes.ordinarypersistence.js';
|
|
|
6
6
|
import type { SmartdataCollection } from './classes.collection.js';
|
|
7
7
|
import { getIdentityIndexName } from './classes.collectiontopology.js';
|
|
8
8
|
import { registerCollectionReconnectHandler } from './classes.collectionlifecycle.js';
|
|
9
|
-
import type { SmartdataSession } from './classes.session.js';
|
|
10
9
|
import {
|
|
10
|
+
SmartdataSession,
|
|
11
11
|
acquireSmartdataSession,
|
|
12
|
+
needsSmartdataRevisionTransaction,
|
|
13
|
+
readPreviousWriteRevisions,
|
|
12
14
|
reportSmartdataCollectionWrite,
|
|
13
15
|
reportSmartdataWriteFailure,
|
|
16
|
+
runSmartdataRevisionTransaction,
|
|
14
17
|
settleSmartdataOperationError,
|
|
15
18
|
resolveSmartdataOperationDeadline,
|
|
16
19
|
trackSmartdataRead,
|
|
@@ -1068,6 +1071,19 @@ export class SmartdataExactCollection<
|
|
|
1068
1071
|
await this.initialize();
|
|
1069
1072
|
}
|
|
1070
1073
|
|
|
1074
|
+
/** Refuses an owned session until the collection is prepared: its DDL cannot run in a transaction. */
|
|
1075
|
+
private requirePreparedForSession(): void {
|
|
1076
|
+
if (
|
|
1077
|
+
!this.collection.isInitializedForCurrentDatabase()
|
|
1078
|
+
|| this.initializedCollection !== this.collection.mongoDbCollection
|
|
1079
|
+
) {
|
|
1080
|
+
throw new SmartdataExactPersistenceError(
|
|
1081
|
+
'unsupported_operation',
|
|
1082
|
+
'Exact collection must be prepared before it participates in a transaction.',
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1071
1087
|
private async beginOperation(
|
|
1072
1088
|
sessionArg: SmartdataSession | undefined,
|
|
1073
1089
|
boundsArg: { timeoutMS?: number; signal?: AbortSignal },
|
|
@@ -1081,15 +1097,7 @@ export class SmartdataExactCollection<
|
|
|
1081
1097
|
await deadline.join(this.initialize());
|
|
1082
1098
|
return { deadline, release: () => {} };
|
|
1083
1099
|
}
|
|
1084
|
-
|
|
1085
|
-
!this.collection.isInitializedForCurrentDatabase()
|
|
1086
|
-
|| this.initializedCollection !== this.collection.mongoDbCollection
|
|
1087
|
-
) {
|
|
1088
|
-
throw new SmartdataExactPersistenceError(
|
|
1089
|
-
'unsupported_operation',
|
|
1090
|
-
'Exact collection must be prepared before it participates in a transaction.',
|
|
1091
|
-
);
|
|
1092
|
-
}
|
|
1100
|
+
this.requirePreparedForSession();
|
|
1093
1101
|
const lease = acquireSmartdataSession(
|
|
1094
1102
|
sessionArg,
|
|
1095
1103
|
this.collection.smartdataDb,
|
|
@@ -1141,7 +1149,8 @@ export class SmartdataExactCollection<
|
|
|
1141
1149
|
/**
|
|
1142
1150
|
* Runs one exact write through `runOperation`. A write that fails once
|
|
1143
1151
|
* started may still have applied, so it changes every open read set that
|
|
1144
|
-
* read the collection.
|
|
1152
|
+
* read the collection. A write of a database with write revisions outside
|
|
1153
|
+
* an owned transaction starts one, on the caller's session if it passed one.
|
|
1145
1154
|
*/
|
|
1146
1155
|
private async runWriteOperation<TResult>(
|
|
1147
1156
|
optionsArg: IExactBoundedOptions,
|
|
@@ -1150,6 +1159,9 @@ export class SmartdataExactCollection<
|
|
|
1150
1159
|
deadlineArg: SmartdataOperationDeadline,
|
|
1151
1160
|
) => Promise<TResult>,
|
|
1152
1161
|
): Promise<TResult> {
|
|
1162
|
+
if (needsSmartdataRevisionTransaction(optionsArg.session, this.collection.smartdataDb)) {
|
|
1163
|
+
return this.runRevisionWriteOperation(optionsArg, operationArg);
|
|
1164
|
+
}
|
|
1153
1165
|
let started = false;
|
|
1154
1166
|
try {
|
|
1155
1167
|
return await runSmartdataWrite(this.collection.smartdataDb, () =>
|
|
@@ -1167,6 +1179,39 @@ export class SmartdataExactCollection<
|
|
|
1167
1179
|
}
|
|
1168
1180
|
}
|
|
1169
1181
|
|
|
1182
|
+
/**
|
|
1183
|
+
* Runs one exact write in a transaction that carries the write's revisions:
|
|
1184
|
+
* on the caller's owned session, which needs the collection prepared, or on
|
|
1185
|
+
* a session of its own, which prepares the collection first. The
|
|
1186
|
+
* transaction's commit is the write's outcome: one whose commit outcome is
|
|
1187
|
+
* unknown is `ambiguous_write`.
|
|
1188
|
+
*/
|
|
1189
|
+
private async runRevisionWriteOperation<TResult>(
|
|
1190
|
+
optionsArg: IExactBoundedOptions,
|
|
1191
|
+
operationArg: (
|
|
1192
|
+
sessionArg: plugins.mongodb.ClientSession | undefined,
|
|
1193
|
+
deadlineArg: SmartdataOperationDeadline,
|
|
1194
|
+
) => Promise<TResult>,
|
|
1195
|
+
): Promise<TResult> {
|
|
1196
|
+
const timeoutMS = requireOperationTimeoutMS(optionsArg.timeoutMS, 'Exact operation timeoutMS');
|
|
1197
|
+
const signal = requireOperationSignal(optionsArg.signal, 'Exact operation signal');
|
|
1198
|
+
signal?.throwIfAborted();
|
|
1199
|
+
if (optionsArg.session) this.requirePreparedForSession();
|
|
1200
|
+
try {
|
|
1201
|
+
return await runSmartdataRevisionTransaction(optionsArg.session, this.collection.smartdataDb, {
|
|
1202
|
+
...(optionsArg.session ? { prepared: true } : { prepare: () => this.initialize() }),
|
|
1203
|
+
collectionName: this.collection.collectionName,
|
|
1204
|
+
...(timeoutMS === undefined ? {} : { timeoutMS }),
|
|
1205
|
+
...(signal === undefined ? {} : { signal }),
|
|
1206
|
+
}, (sessionArg) => this.runWriteOperation({ session: sessionArg }, operationArg));
|
|
1207
|
+
} catch (errorArg) {
|
|
1208
|
+
if (SmartdataSession.getCommitOutcome(errorArg) === 'unknown') {
|
|
1209
|
+
throw this.ambiguousWriteError('The commit outcome of an exact write is unknown.', errorArg);
|
|
1210
|
+
}
|
|
1211
|
+
throw errorArg;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1170
1215
|
/**
|
|
1171
1216
|
* The error of a non-transactional write that its client deadline or
|
|
1172
1217
|
* signal interrupted after the write was handed to the driver: the command
|
|
@@ -1712,11 +1757,11 @@ export class SmartdataExactCollection<
|
|
|
1712
1757
|
): Promise<TStoredDocument<TPersisted> | null> {
|
|
1713
1758
|
return this.runOperation(optionsArg, async (sessionArg, deadlineArg) => {
|
|
1714
1759
|
const filter = convertFilterForMongoDb(filterArg);
|
|
1715
|
-
const tracked = trackSmartdataRead(this.collection, optionsArg.session, {
|
|
1760
|
+
const tracked = await trackSmartdataRead(this.collection, optionsArg.session, {
|
|
1716
1761
|
kind: 'documents',
|
|
1717
1762
|
filter,
|
|
1718
1763
|
limited: true,
|
|
1719
|
-
});
|
|
1764
|
+
}, deadlineArg);
|
|
1720
1765
|
const cursor = this.collection.mongoDbCollection.find(
|
|
1721
1766
|
filter,
|
|
1722
1767
|
{
|
|
@@ -1758,13 +1803,13 @@ export class SmartdataExactCollection<
|
|
|
1758
1803
|
return this.runOperation(optionsArg, async (sessionArg, deadlineArg) => {
|
|
1759
1804
|
const filter = convertFilterForMongoDb(optionsArg.filter || {});
|
|
1760
1805
|
// A skipped page depends on the documents before it, which it never returns.
|
|
1761
|
-
const tracked = trackSmartdataRead(this.collection, optionsArg.session, {
|
|
1806
|
+
const tracked = await trackSmartdataRead(this.collection, optionsArg.session, {
|
|
1762
1807
|
kind: 'documents',
|
|
1763
1808
|
filter,
|
|
1764
1809
|
sort: optionsArg.sort,
|
|
1765
1810
|
matches: optionsArg.skip !== undefined && optionsArg.skip > 0,
|
|
1766
1811
|
limited: optionsArg.limit !== undefined,
|
|
1767
|
-
});
|
|
1812
|
+
}, deadlineArg);
|
|
1768
1813
|
let cursor = this.collection.mongoDbCollection.find(
|
|
1769
1814
|
filter,
|
|
1770
1815
|
{ session: sessionArg, ...deadlineArg.cursorOptions() },
|
|
@@ -1813,7 +1858,9 @@ export class SmartdataExactCollection<
|
|
|
1813
1858
|
): Promise<number> {
|
|
1814
1859
|
return this.runOperation(optionsArg, async (sessionArg, deadlineArg) => {
|
|
1815
1860
|
const filter = convertFilterForMongoDb(filterArg);
|
|
1816
|
-
const tracked = trackSmartdataRead(
|
|
1861
|
+
const tracked = await trackSmartdataRead(
|
|
1862
|
+
this.collection, optionsArg.session, { kind: 'count', filter }, deadlineArg,
|
|
1863
|
+
);
|
|
1817
1864
|
const count = await this.collection.mongoDbCollection.countDocuments(
|
|
1818
1865
|
filter,
|
|
1819
1866
|
{ session: sessionArg, ...deadlineArg.commandOptions() },
|
|
@@ -1834,11 +1881,11 @@ export class SmartdataExactCollection<
|
|
|
1834
1881
|
>>> {
|
|
1835
1882
|
return this.runOperation(optionsArg, async (sessionArg, deadlineArg) => {
|
|
1836
1883
|
const filter = convertFilterForMongoDb(filterArg);
|
|
1837
|
-
trackSmartdataRead(this.collection, optionsArg.session, {
|
|
1884
|
+
await trackSmartdataRead(this.collection, optionsArg.session, {
|
|
1838
1885
|
kind: 'documents',
|
|
1839
1886
|
filter,
|
|
1840
1887
|
matches: true,
|
|
1841
|
-
});
|
|
1888
|
+
}, deadlineArg);
|
|
1842
1889
|
return (await this.collection.mongoDbCollection.distinct(
|
|
1843
1890
|
fieldArg as string,
|
|
1844
1891
|
filter,
|
|
@@ -1904,7 +1951,12 @@ export class SmartdataExactCollection<
|
|
|
1904
1951
|
? { _id: current._id, _smartdataRevision: current._smartdataRevision }
|
|
1905
1952
|
: { _id: current._id, _smartdataRevision: { $exists: false } };
|
|
1906
1953
|
|
|
1907
|
-
// Only a revision pins the stored body the selector matched
|
|
1954
|
+
// Only a revision pins the stored body the selector matched; without one
|
|
1955
|
+
// the write-revision partition it held is read first.
|
|
1956
|
+
const previousPartitionsRead = current._smartdataRevision === undefined
|
|
1957
|
+
&& await readPreviousWriteRevisions(
|
|
1958
|
+
this.collection.smartdataDb, this.collection.mongoDbCollection, sessionArg, selector, deadlineArg,
|
|
1959
|
+
);
|
|
1908
1960
|
const reportTransition = (): void => {
|
|
1909
1961
|
reportSmartdataCollectionWrite(
|
|
1910
1962
|
this.collection,
|
|
@@ -1912,7 +1964,10 @@ export class SmartdataExactCollection<
|
|
|
1912
1964
|
'replace',
|
|
1913
1965
|
[current, expectedStoredDocument],
|
|
1914
1966
|
selector,
|
|
1915
|
-
{
|
|
1967
|
+
{
|
|
1968
|
+
images: current._smartdataRevision !== undefined ? 'beforeAndAfter' : 'after',
|
|
1969
|
+
...(previousPartitionsRead ? { previousPartitionsRead } : {}),
|
|
1970
|
+
},
|
|
1916
1971
|
);
|
|
1917
1972
|
};
|
|
1918
1973
|
const replaceOptions = {
|
|
@@ -2006,6 +2061,10 @@ export class SmartdataExactCollection<
|
|
|
2006
2061
|
[this.reconcileBy]: identity,
|
|
2007
2062
|
_smartdataRevision: { $exists: false },
|
|
2008
2063
|
};
|
|
2064
|
+
const previousPartitionsRead = current._smartdataRevision === undefined
|
|
2065
|
+
&& await readPreviousWriteRevisions(
|
|
2066
|
+
this.collection.smartdataDb, this.collection.mongoDbCollection, sessionArg, selector, deadlineArg,
|
|
2067
|
+
);
|
|
2009
2068
|
let result: plugins.mongodb.DeleteResult;
|
|
2010
2069
|
const deleteOptions = { session: sessionArg, ...deadlineArg.commandOptions() };
|
|
2011
2070
|
try {
|
|
@@ -2032,7 +2091,10 @@ export class SmartdataExactCollection<
|
|
|
2032
2091
|
'delete',
|
|
2033
2092
|
[current],
|
|
2034
2093
|
selector,
|
|
2035
|
-
{
|
|
2094
|
+
{
|
|
2095
|
+
images: current._smartdataRevision !== undefined ? 'before' : 'none',
|
|
2096
|
+
...(previousPartitionsRead ? { previousPartitionsRead } : {}),
|
|
2097
|
+
},
|
|
2036
2098
|
);
|
|
2037
2099
|
return { status: 'deleted', deletedCount: 1 };
|
|
2038
2100
|
}
|
|
@@ -76,6 +76,14 @@ export class SmartdataOperationDeadline {
|
|
|
76
76
|
return this.expiresAt !== undefined;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The time left for a deadline handed on whole, such as a transaction's;
|
|
81
|
+
* `undefined` when unbounded. Throws `timeout` once it has passed.
|
|
82
|
+
*/
|
|
83
|
+
public remainingTimeoutMS(): number | undefined {
|
|
84
|
+
return this.expiresAt === undefined ? undefined : this.remainingMS();
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
/** Throws the signal's reason once the call was aborted. */
|
|
80
88
|
public throwIfAborted(): void {
|
|
81
89
|
this.signal?.throwIfAborted();
|