@lossless.org/client 1.19.0 → 1.20.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 +12 -4
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +11 -2
- package/dist_ts/nosqldb/classes.atomicupdate.d.ts +2 -0
- package/dist_ts/nosqldb/classes.atomicupdate.js +32 -5
- package/dist_ts/nosqldb/classes.bootstraprecord.js +7 -2
- package/dist_ts/nosqldb/classes.collection.js +52 -7
- package/dist_ts/nosqldb/classes.db.d.ts +19 -0
- package/dist_ts/nosqldb/classes.db.js +21 -1
- package/dist_ts/nosqldb/classes.doc.js +2 -2
- package/dist_ts/nosqldb/classes.exactpersistence.js +17 -2
- package/dist_ts/nosqldb/classes.persistence.d.ts +6 -0
- package/dist_ts/nosqldb/classes.persistence.js +16 -1
- package/dist_ts/nosqldb/classes.session.d.ts +11 -0
- package/dist_ts/nosqldb/classes.session.js +51 -1
- package/dist_ts/nosqldb/classes.writeobserver.d.ts +57 -0
- package/dist_ts/nosqldb/classes.writeobserver.js +139 -0
- package/package.json +3 -3
- package/readme.md +2 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.atomicdelete.ts +11 -2
- package/ts/nosqldb/classes.atomicfindoneandupdate.ts +22 -0
- package/ts/nosqldb/classes.atomicupdate.ts +53 -3
- package/ts/nosqldb/classes.bootstraprecord.ts +7 -1
- package/ts/nosqldb/classes.collection.ts +63 -4
- package/ts/nosqldb/classes.db.ts +31 -0
- package/ts/nosqldb/classes.doc.ts +1 -1
- package/ts/nosqldb/classes.exactpersistence.ts +24 -0
- package/ts/nosqldb/classes.persistence.ts +16 -0
- package/ts/nosqldb/classes.session.ts +73 -0
- package/ts/nosqldb/classes.writeobserver.ts +224 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import type { SmartdataCollection } from './classes.collection.js';
|
|
3
|
-
import { normalizeOrdinaryPersistenceError } from './classes.persistence.js';
|
|
4
3
|
import {
|
|
4
|
+
getMongoBulkWriteErrorIndexes,
|
|
5
|
+
normalizeOrdinaryPersistenceError,
|
|
6
|
+
} from './classes.persistence.js';
|
|
7
|
+
import {
|
|
8
|
+
reportSmartdataCollectionWrite,
|
|
5
9
|
runWithOrdinarySmartdataSession,
|
|
6
10
|
type TSmartdataOrdinarySession,
|
|
7
11
|
} from './classes.session.js';
|
|
@@ -21,6 +25,8 @@ export const executeAtomicUpdate = async <T>(
|
|
|
21
25
|
upsert?: boolean;
|
|
22
26
|
session?: TSmartdataOrdinarySession;
|
|
23
27
|
timeoutMS?: number;
|
|
28
|
+
/** The document image the update writes, when the caller holds one: a fenced save's instance. */
|
|
29
|
+
observedDocument?: object;
|
|
24
30
|
},
|
|
25
31
|
): Promise<plugins.mongodb.UpdateResult> => {
|
|
26
32
|
return runWithOrdinarySmartdataSession(
|
|
@@ -35,7 +41,7 @@ export const executeAtomicUpdate = async <T>(
|
|
|
35
41
|
async (rawSessionArg, deadlineArg) => {
|
|
36
42
|
await deadlineArg.join(collectionArg.init());
|
|
37
43
|
try {
|
|
38
|
-
|
|
44
|
+
const result = await collectionArg.mongoDbCollection.updateOne(
|
|
39
45
|
filterArg,
|
|
40
46
|
updateArg,
|
|
41
47
|
{
|
|
@@ -44,6 +50,16 @@ export const executeAtomicUpdate = async <T>(
|
|
|
44
50
|
...deadlineArg.commandOptions(),
|
|
45
51
|
},
|
|
46
52
|
);
|
|
53
|
+
if (result.upsertedCount > 0 || result.matchedCount > 0) {
|
|
54
|
+
reportSmartdataCollectionWrite(
|
|
55
|
+
collectionArg,
|
|
56
|
+
optsArg?.session,
|
|
57
|
+
result.upsertedCount > 0 ? 'insert' : 'update',
|
|
58
|
+
optsArg?.observedDocument ? [optsArg.observedDocument] : null,
|
|
59
|
+
filterArg,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
47
63
|
} catch (errorArg) {
|
|
48
64
|
return normalizeOrdinaryPersistenceError(
|
|
49
65
|
errorArg,
|
|
@@ -90,6 +106,25 @@ export const executeAtomicUpsertMany = async <T>(
|
|
|
90
106
|
},
|
|
91
107
|
async (rawSessionArg, deadlineArg) => {
|
|
92
108
|
await deadlineArg.join(collectionArg.init());
|
|
109
|
+
/**
|
|
110
|
+
* Every entry upserts, so an entry that neither inserted nor failed
|
|
111
|
+
* matched its document.
|
|
112
|
+
*/
|
|
113
|
+
const reportEntries = (
|
|
114
|
+
upsertedIdsArg: Record<number, unknown> | undefined,
|
|
115
|
+
failedIndexesArg: ReadonlySet<number>,
|
|
116
|
+
): void => {
|
|
117
|
+
operationsArg.forEach((operationArg, indexArg) => {
|
|
118
|
+
if (failedIndexesArg.has(indexArg)) return;
|
|
119
|
+
reportSmartdataCollectionWrite(
|
|
120
|
+
collectionArg,
|
|
121
|
+
optsArg?.session,
|
|
122
|
+
Object.hasOwn(upsertedIdsArg ?? {}, indexArg) ? 'insert' : 'update',
|
|
123
|
+
null,
|
|
124
|
+
operationArg.filter,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
};
|
|
93
128
|
try {
|
|
94
129
|
const result = await collectionArg.mongoDbCollection.bulkWrite(
|
|
95
130
|
operationsArg.map((operationArg) => ({
|
|
@@ -105,6 +140,9 @@ export const executeAtomicUpsertMany = async <T>(
|
|
|
105
140
|
...deadlineArg.commandOptions(),
|
|
106
141
|
},
|
|
107
142
|
);
|
|
143
|
+
if (result.isOk()) {
|
|
144
|
+
reportEntries(result.upsertedIds, new Set());
|
|
145
|
+
}
|
|
108
146
|
return {
|
|
109
147
|
acknowledged: result.isOk(),
|
|
110
148
|
matchedCount: result.matchedCount,
|
|
@@ -112,6 +150,14 @@ export const executeAtomicUpsertMany = async <T>(
|
|
|
112
150
|
upsertedCount: result.upsertedCount,
|
|
113
151
|
};
|
|
114
152
|
} catch (errorArg) {
|
|
153
|
+
// An unordered batch that failed on write errors attempted every entry.
|
|
154
|
+
const failedIndexes = getMongoBulkWriteErrorIndexes(errorArg);
|
|
155
|
+
if (failedIndexes) {
|
|
156
|
+
reportEntries(
|
|
157
|
+
(errorArg as plugins.mongodb.MongoBulkWriteError).result?.upsertedIds,
|
|
158
|
+
failedIndexes,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
115
161
|
return normalizeOrdinaryPersistenceError(
|
|
116
162
|
errorArg,
|
|
117
163
|
`Atomic upsert-many conflicts with a unique index in collection "${collectionArg.collectionName}".`,
|
|
@@ -151,7 +197,7 @@ export const executeAtomicUpdateMany = async <T>(
|
|
|
151
197
|
async (rawSessionArg, deadlineArg) => {
|
|
152
198
|
await deadlineArg.join(collectionArg.init());
|
|
153
199
|
try {
|
|
154
|
-
|
|
200
|
+
const result = await collectionArg.mongoDbCollection.updateMany(
|
|
155
201
|
filterArg,
|
|
156
202
|
updateArg,
|
|
157
203
|
{
|
|
@@ -159,6 +205,10 @@ export const executeAtomicUpdateMany = async <T>(
|
|
|
159
205
|
...deadlineArg.commandOptions(),
|
|
160
206
|
},
|
|
161
207
|
);
|
|
208
|
+
if (result.matchedCount > 0) {
|
|
209
|
+
reportSmartdataCollectionWrite(collectionArg, optsArg?.session, 'update', null, filterArg);
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
162
212
|
} catch (errorArg) {
|
|
163
213
|
return normalizeOrdinaryPersistenceError(
|
|
164
214
|
errorArg,
|
|
@@ -6,6 +6,7 @@ import { snapshotBoundedBsonDocument } from './classes.bsonsnapshot.js';
|
|
|
6
6
|
import { validateOrdinaryStoredDocument } from './classes.ordinarypersistence.js';
|
|
7
7
|
import { SmartdataPersistenceError, isMongoDuplicateKeyError } from './classes.persistence.js';
|
|
8
8
|
import { readBoundedInspectionCursor } from './classes.namespaceinspection.js';
|
|
9
|
+
import { reportSmartdataWrite } from './classes.session.js';
|
|
9
10
|
import {
|
|
10
11
|
collectionModelSchemaResolverSymbol, collectionTopologyDbInspectionSymbol,
|
|
11
12
|
getExpectedCollectionTopologyForSchema, inspectCollectionTopologySnapshot,
|
|
@@ -96,7 +97,12 @@ export const insertBootstrapRecord = async (
|
|
|
96
97
|
dispatched ? 'Bootstrap insertion outcome is uncertain; inspect its explicit ID before proceeding.'
|
|
97
98
|
: 'Bootstrap insertion could not inspect the current database connection.');
|
|
98
99
|
}
|
|
99
|
-
if (outcome === 'inserted')
|
|
100
|
+
if (outcome === 'inserted') {
|
|
101
|
+
reportSmartdataWrite(dbArg, undefined, {
|
|
102
|
+
collection: schema.collectionName, kind: 'insert', documents: [document],
|
|
103
|
+
});
|
|
104
|
+
return Object.freeze({ acknowledged: true, insertedId: document._id as string });
|
|
105
|
+
}
|
|
100
106
|
const messages = {
|
|
101
107
|
unique_conflict: 'Bootstrap record conflicts with a unique index.',
|
|
102
108
|
invalid_configuration: 'Bootstrap insertion requires absent or exact primary-only collection topology.',
|
|
@@ -24,13 +24,16 @@ import { CollectionFactory } from './classes.collectionfactory.js';
|
|
|
24
24
|
import { logger } from './logging.js';
|
|
25
25
|
import {
|
|
26
26
|
SmartdataPersistenceError,
|
|
27
|
+
getMongoBulkWriteErrorIndexes,
|
|
27
28
|
isMongoIndexConflictError,
|
|
28
29
|
normalizeOrdinaryPersistenceError,
|
|
29
30
|
} from './classes.persistence.js';
|
|
30
31
|
import { notifyCollectionReconnect } from './classes.collectionlifecycle.js';
|
|
31
32
|
import type { SmartdataOperationDeadline } from './classes.operationdeadline.js';
|
|
33
|
+
import { hasSmartdataWriteObservers } from './classes.writeobserver.js';
|
|
32
34
|
import {
|
|
33
35
|
leaseOrdinarySmartdataSession,
|
|
36
|
+
reportSmartdataCollectionWrite,
|
|
34
37
|
runWithOrdinarySmartdataSession,
|
|
35
38
|
type TSmartdataOrdinarySession,
|
|
36
39
|
} from './classes.session.js';
|
|
@@ -2595,10 +2598,14 @@ export class SmartdataCollection<T> {
|
|
|
2595
2598
|
|
|
2596
2599
|
const saveableObject = preparedObject ?? await dbDocArg.createSavableObject();
|
|
2597
2600
|
try {
|
|
2598
|
-
|
|
2601
|
+
const result = await this.mongoDbCollection.insertOne(saveableObject, {
|
|
2599
2602
|
session: rawSessionArg,
|
|
2600
2603
|
...deadlineArg.commandOptions(),
|
|
2601
2604
|
});
|
|
2605
|
+
if (result.acknowledged) {
|
|
2606
|
+
reportSmartdataCollectionWrite(this, opts?.session, 'insert', [saveableObject]);
|
|
2607
|
+
}
|
|
2608
|
+
return result;
|
|
2602
2609
|
} catch (err: any) {
|
|
2603
2610
|
return normalizeOrdinaryPersistenceError(
|
|
2604
2611
|
err,
|
|
@@ -2707,13 +2714,29 @@ export class SmartdataCollection<T> {
|
|
|
2707
2714
|
saveableObjects.push(await dbDocArg.createSavableObject());
|
|
2708
2715
|
}
|
|
2709
2716
|
}
|
|
2717
|
+
const reportInserted = (insertedIdsArg: Record<number, unknown> | undefined): void => {
|
|
2718
|
+
const inserted = Object.keys(insertedIdsArg ?? {})
|
|
2719
|
+
.map((indexArg) => saveableObjects[Number(indexArg)])
|
|
2720
|
+
.filter((documentArg) => documentArg !== undefined);
|
|
2721
|
+
if (inserted.length > 0) {
|
|
2722
|
+
reportSmartdataCollectionWrite(this, opts?.session, 'insert', inserted);
|
|
2723
|
+
}
|
|
2724
|
+
};
|
|
2710
2725
|
try {
|
|
2711
|
-
|
|
2726
|
+
const result = await this.mongoDbCollection.insertMany(saveableObjects, {
|
|
2712
2727
|
session: rawSessionArg,
|
|
2713
2728
|
ordered: opts?.ordered !== false,
|
|
2714
2729
|
...deadlineArg.commandOptions(),
|
|
2715
2730
|
});
|
|
2731
|
+
if (result.acknowledged) {
|
|
2732
|
+
reportInserted(result.insertedIds);
|
|
2733
|
+
}
|
|
2734
|
+
return result;
|
|
2716
2735
|
} catch (err: any) {
|
|
2736
|
+
// A batch that failed on write errors names the documents it still inserted.
|
|
2737
|
+
if (getMongoBulkWriteErrorIndexes(err)) {
|
|
2738
|
+
reportInserted((err as plugins.mongodb.MongoBulkWriteError).result?.insertedIds);
|
|
2739
|
+
}
|
|
2717
2740
|
return normalizeOrdinaryPersistenceError(
|
|
2718
2741
|
err,
|
|
2719
2742
|
`Insert conflicts with a unique index in collection "${this.collectionName}".`,
|
|
@@ -2756,6 +2779,14 @@ export class SmartdataCollection<T> {
|
|
|
2756
2779
|
// seeded body must not repeat the immutable primary key: MongoDB derives
|
|
2757
2780
|
// it from the filter's equality condition on the insert branch.
|
|
2758
2781
|
const documentIdIdentity = this.modelSchema?.identityAsDocumentId === identityFieldArg;
|
|
2782
|
+
const reportUpserted = (upsertedIdsArg: Record<number, unknown> | undefined): void => {
|
|
2783
|
+
const inserted = Object.keys(upsertedIdsArg ?? {})
|
|
2784
|
+
.map((indexArg) => prepared[Number(indexArg)])
|
|
2785
|
+
.filter((documentArg) => documentArg !== undefined);
|
|
2786
|
+
if (inserted.length > 0) {
|
|
2787
|
+
reportSmartdataCollectionWrite(this, optsArg.session, 'insert', inserted);
|
|
2788
|
+
}
|
|
2789
|
+
};
|
|
2759
2790
|
try {
|
|
2760
2791
|
const result = await this.mongoDbCollection.bulkWrite(prepared.map((document) => {
|
|
2761
2792
|
const { _id: storedId, ...body } = document as Record<string, unknown>;
|
|
@@ -2769,12 +2800,16 @@ export class SmartdataCollection<T> {
|
|
|
2769
2800
|
},
|
|
2770
2801
|
};
|
|
2771
2802
|
}), { ordered: false, session: rawSessionArg, ...deadlineArg.commandOptions() });
|
|
2803
|
+
reportUpserted(result.upsertedIds);
|
|
2772
2804
|
if (result.upsertedCount + result.matchedCount !== prepared.length) {
|
|
2773
2805
|
throw new SmartdataPersistenceError('unsupported_operation',
|
|
2774
2806
|
'insertManyIfAbsent requires an acknowledged result for every identity.');
|
|
2775
2807
|
}
|
|
2776
2808
|
return { insertedCount: result.upsertedCount, matchedCount: result.matchedCount };
|
|
2777
2809
|
} catch (errorArg) {
|
|
2810
|
+
if (getMongoBulkWriteErrorIndexes(errorArg)) {
|
|
2811
|
+
reportUpserted((errorArg as plugins.mongodb.MongoBulkWriteError).result?.upsertedIds);
|
|
2812
|
+
}
|
|
2778
2813
|
return normalizeOrdinaryPersistenceError(errorArg,
|
|
2779
2814
|
`Insert conflicts with a unique index in collection "${this.collectionName}".`);
|
|
2780
2815
|
}
|
|
@@ -2825,7 +2860,7 @@ export class SmartdataCollection<T> {
|
|
|
2825
2860
|
updateableObject[key] = saveableObject[key];
|
|
2826
2861
|
}
|
|
2827
2862
|
try {
|
|
2828
|
-
|
|
2863
|
+
const result = await this.mongoDbCollection.updateOne(
|
|
2829
2864
|
identifiableObject,
|
|
2830
2865
|
{
|
|
2831
2866
|
$set: updateableObject,
|
|
@@ -2835,6 +2870,16 @@ export class SmartdataCollection<T> {
|
|
|
2835
2870
|
},
|
|
2836
2871
|
{ upsert: true, session: rawSessionArg, ...deadlineArg.commandOptions() },
|
|
2837
2872
|
);
|
|
2873
|
+
if (result.upsertedCount > 0 || result.matchedCount > 0) {
|
|
2874
|
+
reportSmartdataCollectionWrite(
|
|
2875
|
+
this,
|
|
2876
|
+
opts?.session,
|
|
2877
|
+
result.upsertedCount > 0 ? 'insert' : 'update',
|
|
2878
|
+
[saveableObject],
|
|
2879
|
+
identifiableObject,
|
|
2880
|
+
);
|
|
2881
|
+
}
|
|
2882
|
+
return result;
|
|
2838
2883
|
} catch (errorArg) {
|
|
2839
2884
|
return normalizeOrdinaryPersistenceError(
|
|
2840
2885
|
errorArg,
|
|
@@ -2857,10 +2902,24 @@ export class SmartdataCollection<T> {
|
|
|
2857
2902
|
await this.checkDoc(dbDocArg);
|
|
2858
2903
|
const identifiableObject = await dbDocArg.createIdentifiableObject();
|
|
2859
2904
|
this.assertConstrainingIdentifiableObject(identifiableObject, 'delete');
|
|
2860
|
-
|
|
2905
|
+
// Serialized before the delete, so a failing serializer never fails an acknowledged delete.
|
|
2906
|
+
const observedDocument = hasSmartdataWriteObservers(this.smartdataDb)
|
|
2907
|
+
? await dbDocArg.createSavableObject() as object
|
|
2908
|
+
: undefined;
|
|
2909
|
+
const result = await this.mongoDbCollection.deleteOne(identifiableObject, {
|
|
2861
2910
|
session: rawSessionArg,
|
|
2862
2911
|
...deadlineArg.commandOptions(),
|
|
2863
2912
|
});
|
|
2913
|
+
if (result.deletedCount > 0) {
|
|
2914
|
+
reportSmartdataCollectionWrite(
|
|
2915
|
+
this,
|
|
2916
|
+
opts?.session,
|
|
2917
|
+
'delete',
|
|
2918
|
+
observedDocument ? [observedDocument] : null,
|
|
2919
|
+
identifiableObject,
|
|
2920
|
+
);
|
|
2921
|
+
}
|
|
2922
|
+
return result;
|
|
2864
2923
|
},
|
|
2865
2924
|
);
|
|
2866
2925
|
}
|
package/ts/nosqldb/classes.db.ts
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
type ISmartdataNamespaceInspectionOptions,
|
|
21
21
|
type ISmartdataNamespaceInspection,
|
|
22
22
|
} from './classes.namespaceinspection.js';
|
|
23
|
+
import {
|
|
24
|
+
addSmartdataWriteObserver,
|
|
25
|
+
type ISmartdataWriteObserverOptions,
|
|
26
|
+
} from './classes.writeobserver.js';
|
|
23
27
|
|
|
24
28
|
export { SmartdataSession } from './classes.session.js';
|
|
25
29
|
export type {
|
|
@@ -27,6 +31,13 @@ export type {
|
|
|
27
31
|
TSmartdataOrdinarySession,
|
|
28
32
|
TSmartdataTransactionCommitOutcome,
|
|
29
33
|
} from './classes.session.js';
|
|
34
|
+
export type {
|
|
35
|
+
ISmartdataWriteObserverOptions,
|
|
36
|
+
ISmartdataWriteReport,
|
|
37
|
+
TSmartdataObservedFields,
|
|
38
|
+
TSmartdataObservedValue,
|
|
39
|
+
TSmartdataWriteKind,
|
|
40
|
+
} from './classes.writeobserver.js';
|
|
30
41
|
|
|
31
42
|
import { logger } from './logging.js';
|
|
32
43
|
import { assertDataOptions, SmartdataOperationBudget } from './classes.operationbudget.js';
|
|
@@ -524,6 +535,26 @@ export class SmartdataDb {
|
|
|
524
535
|
return createSmartdataSession(this);
|
|
525
536
|
}
|
|
526
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Observes every acknowledged write that this process makes through the
|
|
540
|
+
* database's models: inserts, saves, deletes, atomic and exact writes. Each
|
|
541
|
+
* write is reported once, after the database acknowledged it; a write inside
|
|
542
|
+
* a `createSession()` transaction is reported after the commit, and not at
|
|
543
|
+
* all when the transaction rolls back or its outcome is unknown. A write
|
|
544
|
+
* that changed nothing (no match, nothing deleted) is not reported. Writes
|
|
545
|
+
* of other processes, raw driver access and migrations are not observed.
|
|
546
|
+
*
|
|
547
|
+
* Reports carry the values of the requested top-level `fields` only, from
|
|
548
|
+
* the written documents and from the selector's equality conditions. While
|
|
549
|
+
* an observer is registered, transactions on raw `startSession()` sessions
|
|
550
|
+
* are refused for model writes, since their outcome is not known here.
|
|
551
|
+
*
|
|
552
|
+
* Returns the function that stops the observer.
|
|
553
|
+
*/
|
|
554
|
+
public observeWrites(optionsArg: ISmartdataWriteObserverOptions): () => void {
|
|
555
|
+
return addSmartdataWriteObserver(this, optionsArg);
|
|
556
|
+
}
|
|
557
|
+
|
|
527
558
|
/**
|
|
528
559
|
* Creates a narrowly scoped MongoDB administration facade bound to this
|
|
529
560
|
* SmartData lifecycle. Applications never need direct driver access.
|
|
@@ -6851,7 +6851,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
|
|
6851
6851
|
$set: setObject,
|
|
6852
6852
|
...(Object.keys(unsetObject).length > 0 ? { $unset: unsetObject } : {}),
|
|
6853
6853
|
},
|
|
6854
|
-
{ session: options.session, timeoutMS: options.timeoutMS },
|
|
6854
|
+
{ session: options.session, timeoutMS: options.timeoutMS, observedDocument: saveableObject },
|
|
6855
6855
|
);
|
|
6856
6856
|
if (result.matchedCount === 0) {
|
|
6857
6857
|
// Nothing was written, so the instance keeps the update timestamp the
|
|
@@ -9,6 +9,7 @@ import { registerCollectionReconnectHandler } from './classes.collectionlifecycl
|
|
|
9
9
|
import type { SmartdataSession } from './classes.session.js';
|
|
10
10
|
import {
|
|
11
11
|
acquireSmartdataSession,
|
|
12
|
+
reportSmartdataCollectionWrite,
|
|
12
13
|
settleSmartdataOperationError,
|
|
13
14
|
resolveSmartdataOperationDeadline,
|
|
14
15
|
} from './classes.session.js';
|
|
@@ -1549,6 +1550,14 @@ export class SmartdataExactCollection<
|
|
|
1549
1550
|
}
|
|
1550
1551
|
}
|
|
1551
1552
|
|
|
1553
|
+
/** A reconciled body is this call's own insert only when it carries this call's `_id`. */
|
|
1554
|
+
const reportReconciledInsert = (storedArg: TStoredDocument<TPersisted>): void => {
|
|
1555
|
+
if (storedArg._id.equals(expectedStoredDocument._id)) {
|
|
1556
|
+
reportSmartdataCollectionWrite(
|
|
1557
|
+
this.collection, optionsArg.session, 'insert', [expectedStoredDocument],
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1552
1561
|
let result: plugins.mongodb.InsertOneResult;
|
|
1553
1562
|
const insertOptions = { session: sessionArg, ...deadlineArg.commandOptions() };
|
|
1554
1563
|
try {
|
|
@@ -1594,6 +1603,7 @@ export class SmartdataExactCollection<
|
|
|
1594
1603
|
}
|
|
1595
1604
|
if (reconciled) {
|
|
1596
1605
|
if (bytesEqual(reconciled.bodyBytes, persistedBytes)) {
|
|
1606
|
+
reportReconciledInsert(reconciled.document);
|
|
1597
1607
|
return { status: 'already_committed', document: reconciled.document };
|
|
1598
1608
|
}
|
|
1599
1609
|
if (isDuplicateKeyError(errorArg)) {
|
|
@@ -1633,6 +1643,7 @@ export class SmartdataExactCollection<
|
|
|
1633
1643
|
);
|
|
1634
1644
|
}
|
|
1635
1645
|
if (reconciled && bytesEqual(reconciled.bodyBytes, persistedBytes)) {
|
|
1646
|
+
reportReconciledInsert(reconciled.document);
|
|
1636
1647
|
return { status: 'already_committed', document: reconciled.document };
|
|
1637
1648
|
}
|
|
1638
1649
|
throw this.ambiguousWriteError(
|
|
@@ -1640,6 +1651,9 @@ export class SmartdataExactCollection<
|
|
|
1640
1651
|
result,
|
|
1641
1652
|
);
|
|
1642
1653
|
}
|
|
1654
|
+
reportSmartdataCollectionWrite(
|
|
1655
|
+
this.collection, optionsArg.session, 'insert', [expectedStoredDocument],
|
|
1656
|
+
);
|
|
1643
1657
|
return { status: 'inserted', document: expectedStoredDocument };
|
|
1644
1658
|
});
|
|
1645
1659
|
}
|
|
@@ -1827,6 +1841,11 @@ export class SmartdataExactCollection<
|
|
|
1827
1841
|
? { _id: current._id, _smartdataRevision: current._smartdataRevision }
|
|
1828
1842
|
: { _id: current._id, _smartdataRevision: { $exists: false } };
|
|
1829
1843
|
|
|
1844
|
+
const reportTransition = (): void => {
|
|
1845
|
+
reportSmartdataCollectionWrite(
|
|
1846
|
+
this.collection, sessionOptionsArg.session, 'replace', [current, expectedStoredDocument], selector,
|
|
1847
|
+
);
|
|
1848
|
+
};
|
|
1830
1849
|
const replaceOptions = {
|
|
1831
1850
|
returnDocument: 'after' as const,
|
|
1832
1851
|
includeResultMetadata: false as const,
|
|
@@ -1851,6 +1870,7 @@ export class SmartdataExactCollection<
|
|
|
1851
1870
|
'Exact transition returned a postimage different from the requested state.',
|
|
1852
1871
|
);
|
|
1853
1872
|
}
|
|
1873
|
+
reportTransition();
|
|
1854
1874
|
return { status: 'transitioned', document: expectedStoredDocument };
|
|
1855
1875
|
} catch (errorArg) {
|
|
1856
1876
|
if (isTransientTransactionError(errorArg, sessionArg)) {
|
|
@@ -1875,6 +1895,7 @@ export class SmartdataExactCollection<
|
|
|
1875
1895
|
}, { session: sessionArg, ...deadlineArg.commandOptions() });
|
|
1876
1896
|
if (reconciledRaw) {
|
|
1877
1897
|
if (this.encodesAs(reconciledRaw, expectedBytes)) {
|
|
1898
|
+
reportTransition();
|
|
1878
1899
|
return { status: 'transitioned', document: expectedStoredDocument };
|
|
1879
1900
|
}
|
|
1880
1901
|
// A malformed document fails the reconciliation itself.
|
|
@@ -1936,6 +1957,9 @@ export class SmartdataExactCollection<
|
|
|
1936
1957
|
result.acknowledged === true &&
|
|
1937
1958
|
result.deletedCount === 1
|
|
1938
1959
|
) {
|
|
1960
|
+
reportSmartdataCollectionWrite(
|
|
1961
|
+
this.collection, sessionOptionsArg.session, 'delete', [current], selector,
|
|
1962
|
+
);
|
|
1939
1963
|
return { status: 'deleted', deletedCount: 1 };
|
|
1940
1964
|
}
|
|
1941
1965
|
if (
|
|
@@ -78,6 +78,22 @@ export const isMongoTransientTransactionError = (errorArg: unknown): boolean =>
|
|
|
78
78
|
errorArg instanceof plugins.mongodb.MongoError
|
|
79
79
|
&& errorArg.hasErrorLabel(plugins.mongodb.MongoErrorLabel.TransientTransactionError);
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* The failed operations of a bulk write that failed on write errors. Only then
|
|
83
|
+
* does the error's result state which operations applied: after a driver or
|
|
84
|
+
* write-concern error the outcome is unknown and this returns `undefined`.
|
|
85
|
+
*/
|
|
86
|
+
export const getMongoBulkWriteErrorIndexes = (
|
|
87
|
+
errorArg: unknown,
|
|
88
|
+
): ReadonlySet<number> | undefined => {
|
|
89
|
+
if (!(errorArg instanceof plugins.mongodb.MongoBulkWriteError)) return undefined;
|
|
90
|
+
if (errorArg.result?.getWriteConcernError() !== undefined) return undefined;
|
|
91
|
+
const writeErrors = [errorArg.writeErrors].flat();
|
|
92
|
+
return writeErrors.length > 0
|
|
93
|
+
? new Set(writeErrors.map((writeErrorArg) => writeErrorArg.index))
|
|
94
|
+
: undefined;
|
|
95
|
+
};
|
|
96
|
+
|
|
81
97
|
export const isMongoDuplicateKeyError = (
|
|
82
98
|
errorArg: unknown,
|
|
83
99
|
): errorArg is plugins.mongodb.MongoServerError => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
|
|
3
|
+
import type { SmartdataCollection } from './classes.collection.js';
|
|
3
4
|
import type { SmartdataDb } from './classes.db.js';
|
|
4
5
|
import {
|
|
5
6
|
SmartdataOperationDeadline,
|
|
@@ -15,6 +16,12 @@ import {
|
|
|
15
16
|
normalizeDeadlineError,
|
|
16
17
|
normalizeOrdinaryPersistenceError,
|
|
17
18
|
} from './classes.persistence.js';
|
|
19
|
+
import {
|
|
20
|
+
captureSmartdataWrite,
|
|
21
|
+
hasSmartdataWriteObservers,
|
|
22
|
+
type ISmartdataWrite,
|
|
23
|
+
type TSmartdataWriteKind,
|
|
24
|
+
} from './classes.writeobserver.js';
|
|
18
25
|
|
|
19
26
|
interface ISmartdataSessionBinding {
|
|
20
27
|
session: SmartdataSession;
|
|
@@ -36,6 +43,8 @@ interface ISmartdataSessionBinding {
|
|
|
36
43
|
transactionCommitReached: boolean;
|
|
37
44
|
/** Set when the session ended while a transaction ran, stating how far the transaction had got. */
|
|
38
45
|
transactionEnded?: TSmartdataTransactionEnd;
|
|
46
|
+
/** The write reports of the current attempt, delivered once the transaction committed. */
|
|
47
|
+
transactionWrites: Array<() => void>;
|
|
39
48
|
operationSettled?: Promise<void>;
|
|
40
49
|
resolveOperationSettled?: () => void;
|
|
41
50
|
invalidated: boolean;
|
|
@@ -307,6 +316,7 @@ export class SmartdataSession {
|
|
|
307
316
|
binding.transactionUsesOrdinaryWrite = false;
|
|
308
317
|
binding.transactionFailure = undefined;
|
|
309
318
|
binding.transactionCommitReached = false;
|
|
319
|
+
binding.transactionWrites = [];
|
|
310
320
|
callbackError = undefined;
|
|
311
321
|
attemptInterruption = undefined;
|
|
312
322
|
try {
|
|
@@ -435,11 +445,13 @@ export class SmartdataSession {
|
|
|
435
445
|
};
|
|
436
446
|
let result: TResult | undefined;
|
|
437
447
|
let transactionEnded: TSmartdataTransactionEnd | undefined;
|
|
448
|
+
let committedWrites: Array<() => void> = [];
|
|
438
449
|
try {
|
|
439
450
|
result = timeoutMS === undefined
|
|
440
451
|
? await binding.rawSession.withTransaction(runAttempt)
|
|
441
452
|
: await binding.rawSession.withTransaction(runAttempt, { timeoutMS });
|
|
442
453
|
transactionEnded = binding.transactionEnded;
|
|
454
|
+
committedWrites = binding.transactionWrites;
|
|
443
455
|
} catch (errorArg) {
|
|
444
456
|
const { error, outcome } = settleTransactionRejection(errorArg);
|
|
445
457
|
throw rejectTransaction(error, outcome);
|
|
@@ -451,6 +463,7 @@ export class SmartdataSession {
|
|
|
451
463
|
binding.transactionFailure = undefined;
|
|
452
464
|
binding.transactionCommitReached = false;
|
|
453
465
|
binding.transactionEnded = undefined;
|
|
466
|
+
binding.transactionWrites = [];
|
|
454
467
|
}
|
|
455
468
|
if (transactionEnded) {
|
|
456
469
|
throw rejectTransaction(
|
|
@@ -458,6 +471,9 @@ export class SmartdataSession {
|
|
|
458
471
|
commitOutcomeOfTransactionEnd(transactionEnded),
|
|
459
472
|
);
|
|
460
473
|
}
|
|
474
|
+
for (const deliverWrite of committedWrites) {
|
|
475
|
+
deliverWrite();
|
|
476
|
+
}
|
|
461
477
|
return result;
|
|
462
478
|
}
|
|
463
479
|
|
|
@@ -495,6 +511,7 @@ export const createSmartdataSession = (ownerArg: SmartdataDb): SmartdataSession
|
|
|
495
511
|
transactionCallbackOpen: false,
|
|
496
512
|
transactionUsesOrdinaryWrite: false,
|
|
497
513
|
transactionCommitReached: false,
|
|
514
|
+
transactionWrites: [],
|
|
498
515
|
invalidated: false,
|
|
499
516
|
};
|
|
500
517
|
bindings.set(session, binding);
|
|
@@ -656,6 +673,17 @@ export const leaseOrdinarySmartdataSession = (
|
|
|
656
673
|
if (!optionsArg.prepared && sessionArg?.inTransaction()) {
|
|
657
674
|
refuseUnpreparedCollection(optionsArg, 'inside a transaction');
|
|
658
675
|
}
|
|
676
|
+
if (
|
|
677
|
+
optionsArg.ordinaryWrite
|
|
678
|
+
&& sessionArg?.inTransaction()
|
|
679
|
+
&& hasSmartdataWriteObservers(ownerArg)
|
|
680
|
+
) {
|
|
681
|
+
// Only an owned transaction tells SmartData whether it committed.
|
|
682
|
+
throw new SmartdataPersistenceError(
|
|
683
|
+
'unsupported_operation',
|
|
684
|
+
'Writes of an observed database run in transactions of db.createSession(), not raw driver sessions.',
|
|
685
|
+
);
|
|
686
|
+
}
|
|
659
687
|
return { rawSession: sessionArg, release: () => {} };
|
|
660
688
|
}
|
|
661
689
|
if (!optionsArg.prepared) {
|
|
@@ -753,6 +781,51 @@ export const runWithOrdinarySmartdataSession = async <TResult>(
|
|
|
753
781
|
}
|
|
754
782
|
};
|
|
755
783
|
|
|
784
|
+
/**
|
|
785
|
+
* Reports one acknowledged write to the owner's write observers: at once, or,
|
|
786
|
+
* inside an owned transaction, after it committed. A transaction that rolls
|
|
787
|
+
* back, or whose commit outcome is unknown, reports nothing, and a retried
|
|
788
|
+
* attempt reports only its own writes.
|
|
789
|
+
*/
|
|
790
|
+
export const reportSmartdataWrite = (
|
|
791
|
+
ownerArg: SmartdataDb,
|
|
792
|
+
sessionArg: TSmartdataOrdinarySession | undefined,
|
|
793
|
+
writeArg: ISmartdataWrite,
|
|
794
|
+
): void => {
|
|
795
|
+
// A raw driver transaction passed its lease while nobody observed, and
|
|
796
|
+
// whether it commits is never known here.
|
|
797
|
+
if (!isSmartdataSession(sessionArg) && sessionArg?.inTransaction()) return;
|
|
798
|
+
const deliver = captureSmartdataWrite(ownerArg, writeArg);
|
|
799
|
+
if (!deliver) return;
|
|
800
|
+
const binding = isSmartdataSession(sessionArg) ? bindings.get(sessionArg) : undefined;
|
|
801
|
+
if (binding?.transactionActive) {
|
|
802
|
+
// A write that settles after its attempt ended rolls back with that attempt.
|
|
803
|
+
const attempt = transactionContext.getStore()?.attempt;
|
|
804
|
+
if (attempt !== undefined && attempt === binding.transactionAttempt) {
|
|
805
|
+
binding.transactionWrites.push(deliver);
|
|
806
|
+
}
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
deliver();
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
/** Reports one acknowledged write of a model collection; see `reportSmartdataWrite`. */
|
|
813
|
+
export const reportSmartdataCollectionWrite = (
|
|
814
|
+
collectionArg: SmartdataCollection<any>,
|
|
815
|
+
sessionArg: TSmartdataOrdinarySession | undefined,
|
|
816
|
+
kindArg: TSmartdataWriteKind,
|
|
817
|
+
documentsArg: readonly object[] | null,
|
|
818
|
+
selectorArg?: unknown,
|
|
819
|
+
): void => {
|
|
820
|
+
reportSmartdataWrite(collectionArg.smartdataDb, sessionArg, {
|
|
821
|
+
collection: collectionArg.collectionName,
|
|
822
|
+
kind: kindArg,
|
|
823
|
+
documents: documentsArg,
|
|
824
|
+
selector: selectorArg,
|
|
825
|
+
identityDocumentIdField: collectionArg.getBoundModelSchema()?.identityAsDocumentId,
|
|
826
|
+
});
|
|
827
|
+
};
|
|
828
|
+
|
|
756
829
|
/**
|
|
757
830
|
* Ends every session of the owner. `endOptionsArg.timeoutMS` is the driver's
|
|
758
831
|
* deadline for aborting a transaction a session still holds.
|