@lossless.org/client 1.20.2 → 1.21.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/nosqldb/classes.atomicdelete.js +3 -3
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +6 -3
- package/dist_ts/nosqldb/classes.atomicupdate.js +4 -4
- package/dist_ts/nosqldb/classes.bootstraprecord.js +10 -5
- package/dist_ts/nosqldb/classes.collection.js +53 -16
- package/dist_ts/nosqldb/classes.collectiontopology.d.ts +8 -1
- package/dist_ts/nosqldb/classes.collectiontopology.js +10 -2
- package/dist_ts/nosqldb/classes.cursor.d.ts +4 -1
- package/dist_ts/nosqldb/classes.cursor.js +14 -3
- package/dist_ts/nosqldb/classes.db.d.ts +22 -0
- package/dist_ts/nosqldb/classes.db.js +25 -1
- package/dist_ts/nosqldb/classes.doc.js +35 -5
- package/dist_ts/nosqldb/classes.exactpersistence.d.ts +6 -0
- package/dist_ts/nosqldb/classes.exactpersistence.js +70 -13
- package/dist_ts/nosqldb/classes.readtracker.d.ts +89 -0
- package/dist_ts/nosqldb/classes.readtracker.js +876 -0
- package/dist_ts/nosqldb/classes.session.d.ts +30 -7
- package/dist_ts/nosqldb/classes.session.js +119 -26
- package/dist_ts/nosqldb/classes.storedinspection.js +12 -4
- package/dist_ts/nosqldb/classes.testdatabase.js +16 -9
- package/dist_ts/nosqldb/classes.writeobserver.d.ts +11 -0
- package/dist_ts/nosqldb/classes.writeobserver.js +1 -1
- package/package.json +1 -1
- package/readme.md +1 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.atomicdelete.ts +16 -2
- package/ts/nosqldb/classes.atomicfindoneandupdate.ts +5 -0
- package/ts/nosqldb/classes.atomicupdate.ts +10 -1
- package/ts/nosqldb/classes.bootstraprecord.ts +7 -4
- package/ts/nosqldb/classes.collection.ts +56 -12
- package/ts/nosqldb/classes.collectiontopology.ts +15 -1
- package/ts/nosqldb/classes.cursor.ts +14 -1
- package/ts/nosqldb/classes.db.ts +32 -0
- package/ts/nosqldb/classes.doc.ts +32 -2
- package/ts/nosqldb/classes.exactpersistence.ts +93 -19
- package/ts/nosqldb/classes.readtracker.ts +1031 -0
- package/ts/nosqldb/classes.session.ts +166 -30
- package/ts/nosqldb/classes.storedinspection.ts +15 -3
- package/ts/nosqldb/classes.testdatabase.ts +14 -8
- package/ts/nosqldb/classes.writeobserver.ts +11 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
|
|
3
3
|
import type { SmartdataCollection } from './classes.collection.js';
|
|
4
|
+
import { getOrderingIdentityFields } from './classes.collectiontopology.js';
|
|
4
5
|
import type { SmartdataDb } from './classes.db.js';
|
|
5
6
|
import {
|
|
6
7
|
SmartdataOperationDeadline,
|
|
@@ -22,6 +23,16 @@ import {
|
|
|
22
23
|
type ISmartdataWrite,
|
|
23
24
|
type TSmartdataWriteKind,
|
|
24
25
|
} from './classes.writeobserver.js';
|
|
26
|
+
import {
|
|
27
|
+
captureSmartdataCollectionChange,
|
|
28
|
+
captureSmartdataReadChange,
|
|
29
|
+
hasSmartdataReadScope,
|
|
30
|
+
hasSmartdataReadSets,
|
|
31
|
+
observeSmartdataRead,
|
|
32
|
+
runSmartdataWrite,
|
|
33
|
+
type ISmartdataReadRecorder,
|
|
34
|
+
type ISmartdataReadSpec,
|
|
35
|
+
} from './classes.readtracker.js';
|
|
25
36
|
|
|
26
37
|
interface ISmartdataSessionBinding {
|
|
27
38
|
session: SmartdataSession;
|
|
@@ -43,14 +54,28 @@ interface ISmartdataSessionBinding {
|
|
|
43
54
|
transactionCommitReached: boolean;
|
|
44
55
|
/** Set when the session ended while a transaction ran, stating how far the transaction had got. */
|
|
45
56
|
transactionEnded?: TSmartdataTransactionEnd;
|
|
46
|
-
/**
|
|
47
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The writes of the current attempt: their reports, delivered once the
|
|
59
|
+
* transaction committed, and their changes to open read sets, also applied
|
|
60
|
+
* when the commit outcome is unknown.
|
|
61
|
+
*/
|
|
62
|
+
transactionWrites: ISmartdataTransactionWrite[];
|
|
48
63
|
operationSettled?: Promise<void>;
|
|
49
64
|
resolveOperationSettled?: () => void;
|
|
50
65
|
invalidated: boolean;
|
|
51
66
|
closing?: Promise<void>;
|
|
52
67
|
}
|
|
53
68
|
|
|
69
|
+
interface ISmartdataTransactionWrite {
|
|
70
|
+
deliver?: () => void;
|
|
71
|
+
changeReads?: () => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A transaction whose commit outcome is unknown may have applied its writes. */
|
|
75
|
+
const changeReadsOfTransaction = (writesArg: readonly ISmartdataTransactionWrite[]): void => {
|
|
76
|
+
for (const write of writesArg) write.changeReads?.();
|
|
77
|
+
};
|
|
78
|
+
|
|
54
79
|
/** How far a transaction had got when its session ended outside the callback's control. */
|
|
55
80
|
type TSmartdataTransactionEnd = 'before_commit' | 'during_commit';
|
|
56
81
|
|
|
@@ -445,7 +470,7 @@ export class SmartdataSession {
|
|
|
445
470
|
};
|
|
446
471
|
let result: TResult | undefined;
|
|
447
472
|
let transactionEnded: TSmartdataTransactionEnd | undefined;
|
|
448
|
-
let committedWrites:
|
|
473
|
+
let committedWrites: ISmartdataTransactionWrite[] = [];
|
|
449
474
|
try {
|
|
450
475
|
result = timeoutMS === undefined
|
|
451
476
|
? await binding.rawSession.withTransaction(runAttempt)
|
|
@@ -454,6 +479,7 @@ export class SmartdataSession {
|
|
|
454
479
|
committedWrites = binding.transactionWrites;
|
|
455
480
|
} catch (errorArg) {
|
|
456
481
|
const { error, outcome } = settleTransactionRejection(errorArg);
|
|
482
|
+
if (outcome === 'unknown') changeReadsOfTransaction(binding.transactionWrites);
|
|
457
483
|
throw rejectTransaction(error, outcome);
|
|
458
484
|
} finally {
|
|
459
485
|
binding.transactionActive = false;
|
|
@@ -466,13 +492,13 @@ export class SmartdataSession {
|
|
|
466
492
|
binding.transactionWrites = [];
|
|
467
493
|
}
|
|
468
494
|
if (transactionEnded) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
);
|
|
495
|
+
const outcome = commitOutcomeOfTransactionEnd(transactionEnded);
|
|
496
|
+
if (outcome === 'unknown') changeReadsOfTransaction(committedWrites);
|
|
497
|
+
throw rejectTransaction(createTransactionSessionEndedError(transactionEnded), outcome);
|
|
473
498
|
}
|
|
474
|
-
|
|
475
|
-
|
|
499
|
+
changeReadsOfTransaction(committedWrites);
|
|
500
|
+
for (const write of committedWrites) {
|
|
501
|
+
write.deliver?.();
|
|
476
502
|
}
|
|
477
503
|
return result;
|
|
478
504
|
}
|
|
@@ -676,12 +702,14 @@ export const leaseOrdinarySmartdataSession = (
|
|
|
676
702
|
if (
|
|
677
703
|
optionsArg.ordinaryWrite
|
|
678
704
|
&& sessionArg?.inTransaction()
|
|
679
|
-
&& hasSmartdataWriteObservers(ownerArg)
|
|
705
|
+
&& (hasSmartdataWriteObservers(ownerArg) || hasSmartdataReadSets(ownerArg))
|
|
680
706
|
) {
|
|
681
707
|
// Only an owned transaction tells SmartData whether it committed.
|
|
682
708
|
throw new SmartdataPersistenceError(
|
|
683
709
|
'unsupported_operation',
|
|
684
|
-
|
|
710
|
+
hasSmartdataWriteObservers(ownerArg)
|
|
711
|
+
? 'Writes of an observed database run in transactions of db.createSession(), not raw driver sessions.'
|
|
712
|
+
: 'Writes of a database with open read sets run in transactions of db.createSession(), not raw driver sessions.',
|
|
685
713
|
);
|
|
686
714
|
}
|
|
687
715
|
return { rawSession: sessionArg, release: () => {} };
|
|
@@ -755,7 +783,9 @@ export const settleSmartdataOperationError = (
|
|
|
755
783
|
/**
|
|
756
784
|
* Runs one model operation on the ordinary session boundary: the owned
|
|
757
785
|
* session lease, the operation's client deadline and signal, and the mapping
|
|
758
|
-
* of an expired driver deadline to the `timeout` persistence error.
|
|
786
|
+
* of an expired driver deadline to the `timeout` persistence error. A write
|
|
787
|
+
* that fails once started may still have applied, so it changes every open
|
|
788
|
+
* read set that read its collection.
|
|
759
789
|
*/
|
|
760
790
|
export const runWithOrdinarySmartdataSession = async <TResult>(
|
|
761
791
|
sessionArg: TSmartdataOrdinarySession | undefined,
|
|
@@ -769,10 +799,19 @@ export const runWithOrdinarySmartdataSession = async <TResult>(
|
|
|
769
799
|
optionsArg.signal?.throwIfAborted();
|
|
770
800
|
const lease = leaseOrdinarySmartdataSession(sessionArg, ownerArg, optionsArg);
|
|
771
801
|
let deadline: SmartdataOperationDeadline | undefined;
|
|
802
|
+
let writeStarted = false;
|
|
772
803
|
try {
|
|
773
804
|
deadline = resolveSmartdataOperationDeadline(sessionArg, optionsArg);
|
|
774
|
-
|
|
805
|
+
if (!optionsArg.ordinaryWrite) {
|
|
806
|
+
return await operationArg(lease.rawSession, deadline);
|
|
807
|
+
}
|
|
808
|
+
const writeDeadline = deadline;
|
|
809
|
+
writeStarted = true;
|
|
810
|
+
return await runSmartdataWrite(ownerArg, () => operationArg(lease.rawSession, writeDeadline));
|
|
775
811
|
} catch (errorArg) {
|
|
812
|
+
if (writeStarted && optionsArg.collectionName) {
|
|
813
|
+
reportSmartdataWriteFailure(ownerArg, sessionArg, optionsArg.collectionName);
|
|
814
|
+
}
|
|
776
815
|
throw deadline
|
|
777
816
|
? settleSmartdataOperationError(sessionArg, deadline, errorArg)
|
|
778
817
|
: normalizeDeadlineError(errorArg);
|
|
@@ -782,50 +821,147 @@ export const runWithOrdinarySmartdataSession = async <TResult>(
|
|
|
782
821
|
};
|
|
783
822
|
|
|
784
823
|
/**
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
|
|
788
|
-
|
|
824
|
+
* Buffers one write of an owned transaction's current attempt. A write that
|
|
825
|
+
* settles after its attempt ended rolls back with that attempt.
|
|
826
|
+
*/
|
|
827
|
+
const bufferTransactionWrite = (
|
|
828
|
+
bindingArg: ISmartdataSessionBinding,
|
|
829
|
+
writeArg: ISmartdataTransactionWrite,
|
|
830
|
+
): void => {
|
|
831
|
+
const attempt = transactionContext.getStore()?.attempt;
|
|
832
|
+
if (attempt !== undefined && attempt === bindingArg.transactionAttempt) {
|
|
833
|
+
bindingArg.transactionWrites.push(writeArg);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Reports one acknowledged write to the owner's write observers and open read
|
|
839
|
+
* sets: at once, or, inside an owned transaction, after it committed. A
|
|
840
|
+
* transaction that rolls back reports nothing, one whose commit outcome is
|
|
841
|
+
* unknown changes the read sets only, and a retried attempt reports only its
|
|
842
|
+
* own writes. Read sets change before observers hear of the write.
|
|
789
843
|
*/
|
|
790
844
|
export const reportSmartdataWrite = (
|
|
791
845
|
ownerArg: SmartdataDb,
|
|
792
846
|
sessionArg: TSmartdataOrdinarySession | undefined,
|
|
793
847
|
writeArg: ISmartdataWrite,
|
|
794
848
|
): void => {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
849
|
+
if (!isSmartdataSession(sessionArg) && sessionArg?.inTransaction()) {
|
|
850
|
+
// A raw driver transaction passed its lease while nobody observed or
|
|
851
|
+
// tracked, and whether it commits is never known here.
|
|
852
|
+
captureSmartdataCollectionChange(ownerArg, writeArg.collection, false)?.();
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const binding = isSmartdataSession(sessionArg) ? bindings.get(sessionArg) : undefined;
|
|
856
|
+
const buffered = binding?.transactionActive === true;
|
|
857
|
+
const changeReads = captureSmartdataReadChange(ownerArg, writeArg, buffered);
|
|
798
858
|
const deliver = captureSmartdataWrite(ownerArg, writeArg);
|
|
799
|
-
if (!deliver) return;
|
|
859
|
+
if (!changeReads && !deliver) return;
|
|
860
|
+
if (binding && buffered) {
|
|
861
|
+
bufferTransactionWrite(binding, { deliver, changeReads });
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
changeReads?.();
|
|
865
|
+
deliver?.();
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Reports a write to the collection that failed after it was sent: it may
|
|
870
|
+
* still have applied, so every open read set that read the collection
|
|
871
|
+
* changes, inside an owned transaction once it commits.
|
|
872
|
+
*/
|
|
873
|
+
export const reportSmartdataWriteFailure = (
|
|
874
|
+
ownerArg: SmartdataDb,
|
|
875
|
+
sessionArg: TSmartdataOrdinarySession | undefined,
|
|
876
|
+
collectionNameArg: string,
|
|
877
|
+
): void => {
|
|
800
878
|
const binding = isSmartdataSession(sessionArg) ? bindings.get(sessionArg) : undefined;
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}
|
|
879
|
+
const buffered = binding?.transactionActive === true;
|
|
880
|
+
const changeReads = captureSmartdataCollectionChange(ownerArg, collectionNameArg, buffered);
|
|
881
|
+
if (!changeReads) return;
|
|
882
|
+
if (binding && buffered) {
|
|
883
|
+
bufferTransactionWrite(binding, { changeReads });
|
|
807
884
|
return;
|
|
808
885
|
}
|
|
809
|
-
|
|
886
|
+
changeReads();
|
|
810
887
|
};
|
|
811
888
|
|
|
812
|
-
/**
|
|
889
|
+
/**
|
|
890
|
+
* Reports one acknowledged write of a model collection; see
|
|
891
|
+
* `reportSmartdataWrite`. `readImagesArg` states which document states the
|
|
892
|
+
* documents are and the update applied; without it, read sets count the
|
|
893
|
+
* write as changing every read of the collection.
|
|
894
|
+
*/
|
|
813
895
|
export const reportSmartdataCollectionWrite = (
|
|
814
896
|
collectionArg: SmartdataCollection<any>,
|
|
815
897
|
sessionArg: TSmartdataOrdinarySession | undefined,
|
|
816
898
|
kindArg: TSmartdataWriteKind,
|
|
817
899
|
documentsArg: readonly object[] | null,
|
|
818
900
|
selectorArg?: unknown,
|
|
901
|
+
readImagesArg?: Pick<ISmartdataWrite, 'images' | 'update'>,
|
|
819
902
|
): void => {
|
|
903
|
+
const schema = collectionArg.getBoundModelSchema();
|
|
820
904
|
reportSmartdataWrite(collectionArg.smartdataDb, sessionArg, {
|
|
821
905
|
collection: collectionArg.collectionName,
|
|
822
906
|
kind: kindArg,
|
|
823
907
|
documents: documentsArg,
|
|
824
908
|
selector: selectorArg,
|
|
825
|
-
identityDocumentIdField:
|
|
909
|
+
identityDocumentIdField: schema?.identityAsDocumentId,
|
|
910
|
+
identityFields: identityFieldsOfSchema(schema),
|
|
911
|
+
...readImagesArg,
|
|
826
912
|
});
|
|
827
913
|
};
|
|
828
914
|
|
|
915
|
+
/** The identity fields a read or write names a document by besides `_id`. */
|
|
916
|
+
const identityFieldsOfSchema = (
|
|
917
|
+
schemaArg: ReturnType<SmartdataCollection<any>['getBoundModelSchema']>,
|
|
918
|
+
): readonly string[] | undefined =>
|
|
919
|
+
schemaArg && !schemaArg.identityAsDocumentId && schemaArg.identityFields.length > 0
|
|
920
|
+
? schemaArg.identityFields
|
|
921
|
+
: undefined;
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Registers one read of a model collection with the open read sets of the
|
|
925
|
+
* current async context, right before it is issued; `undefined` when none is
|
|
926
|
+
* open. See `observeSmartdataRead`.
|
|
927
|
+
*/
|
|
928
|
+
export const trackSmartdataRead = (
|
|
929
|
+
collectionArg: SmartdataCollection<any>,
|
|
930
|
+
sessionArg: TSmartdataOrdinarySession | undefined,
|
|
931
|
+
specArg: Omit<ISmartdataReadSpec, 'collection' | 'identityFields' | 'orderingIdentityFields'>,
|
|
932
|
+
): ISmartdataReadRecorder | undefined => trackSmartdataModelRead(
|
|
933
|
+
collectionArg.smartdataDb,
|
|
934
|
+
collectionArg.collectionName,
|
|
935
|
+
collectionArg.getBoundModelSchema(),
|
|
936
|
+
sessionArg,
|
|
937
|
+
specArg,
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
/** `trackSmartdataRead` for a read that names its model by schema rather than collection. */
|
|
941
|
+
export const trackSmartdataModelRead = (
|
|
942
|
+
dbArg: SmartdataDb,
|
|
943
|
+
collectionNameArg: string,
|
|
944
|
+
schemaArg: ReturnType<SmartdataCollection<any>['getBoundModelSchema']>,
|
|
945
|
+
sessionArg: TSmartdataOrdinarySession | undefined,
|
|
946
|
+
specArg: Omit<ISmartdataReadSpec, 'collection' | 'identityFields' | 'orderingIdentityFields'>,
|
|
947
|
+
): ISmartdataReadRecorder | undefined => {
|
|
948
|
+
if (!hasSmartdataReadScope()) return undefined;
|
|
949
|
+
let inTransaction: boolean;
|
|
950
|
+
try {
|
|
951
|
+
inTransaction = isOrdinarySessionInTransaction(sessionArg);
|
|
952
|
+
} catch {
|
|
953
|
+
// An ended session's read fails; a set must not count it as followed.
|
|
954
|
+
inTransaction = true;
|
|
955
|
+
}
|
|
956
|
+
const identityFields = identityFieldsOfSchema(schemaArg);
|
|
957
|
+
return observeSmartdataRead(dbArg, {
|
|
958
|
+
...specArg,
|
|
959
|
+
collection: collectionNameArg,
|
|
960
|
+
identityFields,
|
|
961
|
+
orderingIdentityFields: schemaArg && identityFields ? getOrderingIdentityFields(schemaArg) : undefined,
|
|
962
|
+
}, inTransaction);
|
|
963
|
+
};
|
|
964
|
+
|
|
829
965
|
/**
|
|
830
966
|
* Ends every session of the owner. `endOptionsArg.timeoutMS` is the driver's
|
|
831
967
|
* deadline for aborting a transaction a session still holds.
|
|
@@ -3,7 +3,9 @@ import type { SmartdataDb } from './classes.db.js';
|
|
|
3
3
|
import { convertFilterForMongoDb } from './classes.doc.js';
|
|
4
4
|
import { SmartdataPersistenceError } from './classes.persistence.js';
|
|
5
5
|
import { runCursorOperation } from './classes.cursoroperation.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
acquireSmartdataSession, isSmartdataSession, trackSmartdataModelRead, type SmartdataSession,
|
|
8
|
+
} from './classes.session.js';
|
|
7
9
|
import {
|
|
8
10
|
collectionTopologyDbInspectionSymbol, collectionModelSchemaResolverSymbol,
|
|
9
11
|
} from './classes.collectiontopology.js';
|
|
@@ -79,6 +81,9 @@ const readStoredDocuments = async (
|
|
|
79
81
|
let page: { documents: plugins.mongodb.Document[]; hasMore: boolean };
|
|
80
82
|
try {
|
|
81
83
|
page = await dbArg[collectionTopologyDbInspectionSymbol](async (mongoDbArg) => {
|
|
84
|
+
const tracked = trackSmartdataModelRead(dbArg, schema.collectionName, schema, opts.session, {
|
|
85
|
+
kind: 'documents', filter, sort: { _id: 1 }, matches: skip > 0, limited: true,
|
|
86
|
+
});
|
|
82
87
|
const cursor = mongoDbArg.collection(schema.collectionName).find(filter, {
|
|
83
88
|
sort: { _id: 1 }, skip, limit: limit + 1, batchSize: 1,
|
|
84
89
|
timeoutMS, timeoutMode: 'cursorLifetime', signal: opts.signal, session: lease?.rawSession,
|
|
@@ -88,8 +93,15 @@ const readStoredDocuments = async (
|
|
|
88
93
|
let bytes = 0;
|
|
89
94
|
while (true) {
|
|
90
95
|
const next = await cursorArg.next();
|
|
91
|
-
if (next === null)
|
|
92
|
-
|
|
96
|
+
if (next === null) {
|
|
97
|
+
tracked?.settle();
|
|
98
|
+
return { documents, hasMore: false };
|
|
99
|
+
}
|
|
100
|
+
tracked?.record(next);
|
|
101
|
+
if (documents.length === limit) {
|
|
102
|
+
tracked?.settle();
|
|
103
|
+
return { documents, hasMore: true };
|
|
104
|
+
}
|
|
93
105
|
bytes += plugins.mongodb.BSON.calculateObjectSize(next);
|
|
94
106
|
if (bytes > maxBytes) throw new SmartdataPersistenceError('unsupported_operation',
|
|
95
107
|
'Stored inspection exceeds its byte budget.');
|
|
@@ -3,6 +3,7 @@ import { snapshotBoundedBsonDocument } from './classes.bsonsnapshot.js';
|
|
|
3
3
|
import { SmartdataDb } from './classes.db.js';
|
|
4
4
|
import { SmartDataDbDoc } from './classes.doc.js';
|
|
5
5
|
import { SmartdataPersistenceError } from './classes.persistence.js';
|
|
6
|
+
import { captureSmartdataCollectionChange } from './classes.readtracker.js';
|
|
6
7
|
import { assertDataOptions, SmartdataOperationBudget } from './classes.operationbudget.js';
|
|
7
8
|
|
|
8
9
|
/** A loopback TCP engine endpoint. */
|
|
@@ -255,14 +256,19 @@ export class SmartdataTestDatabase {
|
|
|
255
256
|
await modelArg.prepareCollection({ mode: 'ensureCompatible', timeoutMs: budget.remainingMs(), signal: budget.signal });
|
|
256
257
|
this.assertOwnedBinding();
|
|
257
258
|
const raw = this.db.mongoDb.collection(expected.collectionName);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
259
|
+
try {
|
|
260
|
+
if (replacementArg) {
|
|
261
|
+
const result = await raw.replaceOne({ _id: document._id }, document,
|
|
262
|
+
{ upsert: false, ...budget.operationOptions() });
|
|
263
|
+
if (!result.acknowledged || result.matchedCount !== 1) throw new SmartdataPersistenceError(
|
|
264
|
+
'unsupported_operation', 'Exact fault replacement requires one existing document.');
|
|
265
|
+
} else {
|
|
266
|
+
const result = await raw.insertOne(document, budget.operationOptions());
|
|
267
|
+
if (!result.acknowledged) throw new SmartdataPersistenceError('unsupported_operation', 'Fault insertion was not acknowledged.');
|
|
268
|
+
}
|
|
269
|
+
} finally {
|
|
270
|
+
// A fault writes behind the models, so every open read set that read the collection changes.
|
|
271
|
+
captureSmartdataCollectionChange(this.db, expected.collectionName, false)?.();
|
|
266
272
|
}
|
|
267
273
|
budget.check();
|
|
268
274
|
})();
|
|
@@ -51,6 +51,17 @@ export interface ISmartdataWrite {
|
|
|
51
51
|
selector?: unknown;
|
|
52
52
|
/** The model's `identityAsDocumentId` field, whose value a selector carries as `_id`. */
|
|
53
53
|
identityDocumentIdField?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Which stored states `documents` are, for read sets: the whole document
|
|
56
|
+
* after the write (for a replacement, the last one), before it, both
|
|
57
|
+
* (before, then after), or neither. A write without it changes every read
|
|
58
|
+
* of its collection.
|
|
59
|
+
*/
|
|
60
|
+
images?: 'after' | 'before' | 'beforeAndAfter' | 'none';
|
|
61
|
+
/** The update document a write by selector applied. */
|
|
62
|
+
update?: unknown;
|
|
63
|
+
/** The model's identity fields, by which a selector names a document without its `_id`. */
|
|
64
|
+
identityFields?: readonly string[];
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
interface IWriteObserver {
|