@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.
Files changed (43) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/nosqldb/classes.atomicdelete.js +8 -4
  3. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +19 -4
  4. package/dist_ts/nosqldb/classes.atomicupdate.js +16 -5
  5. package/dist_ts/nosqldb/classes.bootstraprecord.js +31 -5
  6. package/dist_ts/nosqldb/classes.collection.js +17 -14
  7. package/dist_ts/nosqldb/classes.connection.d.ts +3 -0
  8. package/dist_ts/nosqldb/classes.connection.js +3 -2
  9. package/dist_ts/nosqldb/classes.db.d.ts +27 -2
  10. package/dist_ts/nosqldb/classes.db.js +42 -3
  11. package/dist_ts/nosqldb/classes.doc.d.ts +3 -1
  12. package/dist_ts/nosqldb/classes.doc.js +15 -9
  13. package/dist_ts/nosqldb/classes.exactpersistence.d.ts +13 -2
  14. package/dist_ts/nosqldb/classes.exactpersistence.js +64 -17
  15. package/dist_ts/nosqldb/classes.operationdeadline.d.ts +5 -0
  16. package/dist_ts/nosqldb/classes.operationdeadline.js +8 -1
  17. package/dist_ts/nosqldb/classes.readtracker.d.ts +26 -0
  18. package/dist_ts/nosqldb/classes.readtracker.js +124 -1
  19. package/dist_ts/nosqldb/classes.session.d.ts +34 -5
  20. package/dist_ts/nosqldb/classes.session.js +134 -8
  21. package/dist_ts/nosqldb/classes.storedinspection.js +7 -4
  22. package/dist_ts/nosqldb/classes.writeobserver.d.ts +5 -0
  23. package/dist_ts/nosqldb/classes.writeobserver.js +1 -1
  24. package/dist_ts/nosqldb/classes.writerevisions.d.ts +81 -0
  25. package/dist_ts/nosqldb/classes.writerevisions.js +246 -0
  26. package/package.json +4 -4
  27. package/readme.md +1 -0
  28. package/ts/00_commitinfo_data.ts +1 -1
  29. package/ts/nosqldb/classes.atomicdelete.ts +11 -2
  30. package/ts/nosqldb/classes.atomicfindoneandupdate.ts +25 -2
  31. package/ts/nosqldb/classes.atomicupdate.ts +25 -3
  32. package/ts/nosqldb/classes.bootstraprecord.ts +29 -4
  33. package/ts/nosqldb/classes.collection.ts +20 -12
  34. package/ts/nosqldb/classes.connection.ts +5 -1
  35. package/ts/nosqldb/classes.db.ts +65 -2
  36. package/ts/nosqldb/classes.doc.ts +16 -8
  37. package/ts/nosqldb/classes.exactpersistence.ts +83 -21
  38. package/ts/nosqldb/classes.operationdeadline.ts +8 -0
  39. package/ts/nosqldb/classes.readtracker.ts +139 -0
  40. package/ts/nosqldb/classes.session.ts +191 -8
  41. package/ts/nosqldb/classes.storedinspection.ts +6 -3
  42. package/ts/nosqldb/classes.writeobserver.ts +5 -0
  43. package/ts/nosqldb/classes.writerevisions.ts +328 -0
@@ -0,0 +1,246 @@
1
+ import * as plugins from './plugins.js';
2
+ import { assertDataOptions } from './classes.operationbudget.js';
3
+ import { SmartdataPersistenceError, isMongoDuplicateKeyError } from './classes.persistence.js';
4
+ /** The collection that holds the counters. */
5
+ export const writeRevisionsCollectionName = '__smartdata_write_revisions';
6
+ /** The counter every write revision belongs to; a new generation restarts all counters. */
7
+ const epochId = '$epoch';
8
+ const maximumPartitionLength = 512;
9
+ const maximumPinnedPartitions = 64;
10
+ /** Beyond this many documents, a write by selector bumps the shared counter instead. */
11
+ export const maximumPreReadDocuments = 1024;
12
+ const snapshotTimeoutMs = 5_000;
13
+ const partitionFields = new WeakMap();
14
+ /** @internal Validates and records the option; called once by the database's constructor. */
15
+ export const configureSmartdataWriteRevisions = (dbArg, optionsArg) => {
16
+ if (optionsArg === undefined)
17
+ return;
18
+ const { partitionField } = assertDataOptions(optionsArg, ['partitionField']);
19
+ if (typeof partitionField !== 'string'
20
+ || partitionField.length === 0
21
+ || !partitionField.isWellFormed()
22
+ || partitionField.includes('.')
23
+ || partitionField.startsWith('$')
24
+ || partitionField === '_id') {
25
+ throw new SmartdataPersistenceError('invalid_argument', 'Write revisions require a top-level partition field other than _id: no "." and no leading "$".');
26
+ }
27
+ partitionFields.set(dbArg, partitionField);
28
+ };
29
+ /** @internal The configured partition field, whether or not the engine can carry revisions. */
30
+ export const writeRevisionsPartitionField = (dbArg) => partitionFields.get(dbArg);
31
+ /** @internal Write revisions are configured and the engine runs transactions. */
32
+ export const isSmartdataWriteRevisionsActive = (dbArg) => partitionFields.has(dbArg) && dbArg.capabilities?.transactions === 'available';
33
+ /** A string the counters can name, or `undefined`. */
34
+ const partitionOf = (valueArg) => typeof valueArg === 'string' && valueArg.length <= maximumPartitionLength && valueArg.isWellFormed()
35
+ ? valueArg
36
+ : undefined;
37
+ /** @internal The counter of one partition of a collection. */
38
+ export const partitionRevisionId = (collectionArg, partitionArg) => `${collectionArg}$s${partitionArg}`;
39
+ /** @internal The counter of a collection's writes that no partition can place. */
40
+ export const sharedRevisionId = (collectionArg) => `${collectionArg}$*`;
41
+ /**
42
+ * @internal The counter a stored value of the partition field belongs to:
43
+ * its partition's for a string the counters can name, the shared one otherwise.
44
+ */
45
+ export const revisionIdOfValue = (collectionArg, valueArg) => {
46
+ const partition = partitionOf(valueArg);
47
+ return partition === undefined
48
+ ? sharedRevisionId(collectionArg)
49
+ : partitionRevisionId(collectionArg, partition);
50
+ };
51
+ const isPlainRecord = (valueArg) => {
52
+ if (typeof valueArg !== 'object' || valueArg === null || Array.isArray(valueArg))
53
+ return false;
54
+ const prototype = Object.getPrototypeOf(valueArg);
55
+ return prototype === Object.prototype || prototype === null;
56
+ };
57
+ /**
58
+ * The partitions a filter pins the field to: a string, `{ $eq: string }` or
59
+ * `{ $in: [...strings] }` beside other operators, at the top level or inside
60
+ * `$and`. The first pin found wins.
61
+ */
62
+ const pinnedPartitions = (filterArg, fieldArg) => {
63
+ if (!isPlainRecord(filterArg))
64
+ return undefined;
65
+ if (Object.hasOwn(filterArg, fieldArg)) {
66
+ const condition = filterArg[fieldArg];
67
+ if (typeof condition === 'string') {
68
+ const partition = partitionOf(condition);
69
+ if (partition !== undefined)
70
+ return [partition];
71
+ }
72
+ else if (isPlainRecord(condition)
73
+ && Object.keys(condition).length > 0
74
+ && Object.keys(condition).every((keyArg) => keyArg.startsWith('$'))) {
75
+ if (Object.hasOwn(condition, '$eq')) {
76
+ const partition = partitionOf(condition.$eq);
77
+ if (partition !== undefined)
78
+ return [partition];
79
+ }
80
+ const values = condition.$in;
81
+ if (Array.isArray(values)
82
+ && values.length > 0
83
+ && values.length <= maximumPinnedPartitions
84
+ && values.every((valueArg) => partitionOf(valueArg) !== undefined)) {
85
+ return [...new Set(values)];
86
+ }
87
+ }
88
+ }
89
+ if (Array.isArray(filterArg.$and)) {
90
+ for (const conjunct of filterArg.$and) {
91
+ const partitions = pinnedPartitions(conjunct, fieldArg);
92
+ if (partitions)
93
+ return partitions;
94
+ }
95
+ }
96
+ return undefined;
97
+ };
98
+ /*
99
+ * Snapshot keys: `e` for the epoch, `c<id>` for one counter and `r<collection>`
100
+ * for the sum of every counter of a collection.
101
+ */
102
+ const epochKey = 'e';
103
+ const counterKey = (idArg) => `c${idArg}`;
104
+ const rangeKey = (collectionArg) => `r${collectionArg}`;
105
+ /**
106
+ * @internal The snapshot keys one read depends on. A read the field pins
107
+ * depends on its partitions and the shared counter, which holds every
108
+ * document whose field holds no single string; any other read depends on the
109
+ * whole collection.
110
+ */
111
+ export const writeRevisionKeysOfRead = (dbArg, collectionArg, kindArg, filterArg) => {
112
+ const field = partitionFields.get(dbArg);
113
+ const partitions = field === undefined || kindArg === 'collection'
114
+ ? undefined
115
+ : pinnedPartitions(filterArg, field);
116
+ if (!partitions)
117
+ return [epochKey, rangeKey(collectionArg)];
118
+ return [
119
+ epochKey,
120
+ counterKey(sharedRevisionId(collectionArg)),
121
+ ...partitions.map((partitionArg) => counterKey(partitionRevisionId(collectionArg, partitionArg))),
122
+ ];
123
+ };
124
+ const revisionsCollection = (mongoDbArg) => mongoDbArg.collection(writeRevisionsCollectionName);
125
+ const connectedDb = (dbArg) => {
126
+ if (dbArg.status !== 'connected' || !dbArg.mongoDb) {
127
+ throw new SmartdataPersistenceError('unsupported_operation', 'Write revisions require a connected SmartdataDb.');
128
+ }
129
+ return dbArg.mongoDb;
130
+ };
131
+ /**
132
+ * @internal The current value of each snapshot key, or `undefined` for a key
133
+ * whose value cannot be known: a missing epoch means the counters restarted.
134
+ * Bounded by its own deadline.
135
+ */
136
+ export const readWriteRevisionSnapshot = async (dbArg, keysArg) => {
137
+ const collection = revisionsCollection(connectedDb(dbArg));
138
+ const signal = AbortSignal.timeout(snapshotTimeoutMs);
139
+ const expiresAt = performance.now() + snapshotTimeoutMs;
140
+ const options = () => ({
141
+ timeoutMS: Math.max(1, Math.ceil(expiresAt - performance.now())),
142
+ timeoutMode: 'cursorLifetime',
143
+ signal,
144
+ });
145
+ const counterIds = keysArg.flatMap((keyArg) => keyArg.startsWith('c') ? [keyArg.slice(1)] : []);
146
+ const ranges = keysArg.flatMap((keyArg) => keyArg.startsWith('r') ? [keyArg.slice(1)] : []);
147
+ const [counters, ...sums] = await Promise.all([
148
+ collection.find({ _id: { $in: [epochId, ...counterIds] } }, { projection: { n: 1, generation: 1 }, ...options() }).toArray(),
149
+ ...ranges.map((collectionArg) => collection.aggregate([
150
+ { $match: { _id: { $gte: `${collectionArg}$`, $lt: `${collectionArg}%` } } },
151
+ { $group: { _id: null, n: { $sum: '$n' } } },
152
+ ], options()).toArray()),
153
+ ]);
154
+ const byId = new Map(counters.map((documentArg) => [documentArg._id, documentArg]));
155
+ const snapshot = new Map();
156
+ const epoch = byId.get(epochId);
157
+ snapshot.set(epochKey, typeof epoch?.generation === 'string' && Number.isSafeInteger(epoch.n)
158
+ ? `${epoch.generation}:${epoch.n}`
159
+ : undefined);
160
+ for (const id of counterIds) {
161
+ const counter = byId.get(id);
162
+ snapshot.set(counterKey(id), counter === undefined
163
+ ? '0'
164
+ : Number.isSafeInteger(counter.n) ? String(counter.n) : undefined);
165
+ }
166
+ for (const [index, collectionArg] of ranges.entries()) {
167
+ const sum = sums[index].length === 0 ? 0 : sums[index][0].n;
168
+ snapshot.set(rangeKey(collectionArg), typeof sum === 'number' && Number.isSafeInteger(sum)
169
+ ? String(sum)
170
+ : undefined);
171
+ }
172
+ return snapshot;
173
+ };
174
+ /**
175
+ * @internal Creates the epoch of a database that has none, before `init()`
176
+ * publishes the connection. Another process creating it at the same time is
177
+ * fine.
178
+ */
179
+ export const ensureSmartdataWriteRevisions = async (mongoDbArg, optionsArg = {}) => {
180
+ try {
181
+ await revisionsCollection(mongoDbArg).updateOne({ _id: epochId }, { $setOnInsert: { n: 0, generation: plugins.nodeCrypto.randomUUID() } }, { upsert: true, ...optionsArg });
182
+ }
183
+ catch (errorArg) {
184
+ if (!isMongoDuplicateKeyError(errorArg))
185
+ throw errorArg;
186
+ }
187
+ };
188
+ /**
189
+ * @internal Bumps the epoch, which every read set depends on, so every read
190
+ * set of every process stops being current. An epoch that went missing is
191
+ * recreated with a new generation.
192
+ */
193
+ export const bumpSmartdataWriteRevisionEpoch = async (dbArg, optionsArg) => {
194
+ const collection = revisionsCollection(connectedDb(dbArg));
195
+ const bump = () => collection.updateOne({ _id: epochId }, { $inc: { n: 1 }, $setOnInsert: { generation: plugins.nodeCrypto.randomUUID() } }, { upsert: true, ...optionsArg });
196
+ try {
197
+ await bump();
198
+ }
199
+ catch (errorArg) {
200
+ // A concurrent upsert created the epoch first; the update now matches it.
201
+ if (!isMongoDuplicateKeyError(errorArg))
202
+ throw errorArg;
203
+ await bump();
204
+ }
205
+ };
206
+ /**
207
+ * @internal Bumps the counters a transaction's writes touched, inside that
208
+ * transaction, right before its commit. Sorted, so concurrent transactions
209
+ * take the counters in one order.
210
+ */
211
+ export const flushSmartdataWriteRevisions = async (mongoDbArg, rawSessionArg, idsArg, signalArg) => {
212
+ if (idsArg.size === 0)
213
+ return;
214
+ await revisionsCollection(mongoDbArg).bulkWrite([...idsArg].sort().map((idArg) => ({
215
+ updateOne: { filter: { _id: idArg }, update: { $inc: { n: 1 } }, upsert: true },
216
+ })), {
217
+ ordered: true,
218
+ session: rawSessionArg,
219
+ ...(signalArg === undefined ? {} : { signal: signalArg }),
220
+ });
221
+ };
222
+ /**
223
+ * @internal The counters of the documents a write by selector is about to
224
+ * change, read in its transaction before it runs. Snapshot isolation makes
225
+ * them exactly the documents the write sees. More documents than the bound,
226
+ * or a value the counters cannot name, bump the shared counter.
227
+ */
228
+ export const readPreviousRevisionIds = async (dbArg, collectionArg, rawSessionArg, selectorArg, deadlineArg) => {
229
+ const field = partitionFields.get(dbArg);
230
+ const documents = await collectionArg.find(selectorArg, {
231
+ projection: { [field]: 1, _id: 0 },
232
+ limit: maximumPreReadDocuments + 1,
233
+ session: rawSessionArg,
234
+ ...deadlineArg.cursorOptions(),
235
+ }).toArray();
236
+ const ids = new Set();
237
+ if (documents.length > maximumPreReadDocuments) {
238
+ ids.add(sharedRevisionId(collectionArg.collectionName));
239
+ return ids;
240
+ }
241
+ for (const document of documents) {
242
+ ids.add(revisionIdOfValue(collectionArg.collectionName, Object.hasOwn(document, field) ? document[field] : undefined));
243
+ }
244
+ return ids;
245
+ };
246
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy53cml0ZXJldmlzaW9ucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL25vc3FsZGIvY2xhc3Nlcy53cml0ZXJldmlzaW9ucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUd4QyxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSw4QkFBOEIsQ0FBQztBQUVqRSxPQUFPLEVBQUUseUJBQXlCLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQWtCL0YsOENBQThDO0FBQzlDLE1BQU0sQ0FBQyxNQUFNLDRCQUE0QixHQUFHLDZCQUE2QixDQUFDO0FBUTFFLDJGQUEyRjtBQUMzRixNQUFNLE9BQU8sR0FBRyxRQUFRLENBQUM7QUFDekIsTUFBTSxzQkFBc0IsR0FBRyxHQUFHLENBQUM7QUFDbkMsTUFBTSx1QkFBdUIsR0FBRyxFQUFFLENBQUM7QUFDbkMsd0ZBQXdGO0FBQ3hGLE1BQU0sQ0FBQyxNQUFNLHVCQUF1QixHQUFHLElBQUksQ0FBQztBQUM1QyxNQUFNLGlCQUFpQixHQUFHLEtBQUssQ0FBQztBQUVoQyxNQUFNLGVBQWUsR0FBRyxJQUFJLE9BQU8sRUFBdUIsQ0FBQztBQUUzRCw2RkFBNkY7QUFDN0YsTUFBTSxDQUFDLE1BQU0sZ0NBQWdDLEdBQUcsQ0FBQyxLQUFrQixFQUFFLFVBQW1CLEVBQVEsRUFBRTtJQUNoRyxJQUFJLFVBQVUsS0FBSyxTQUFTO1FBQUUsT0FBTztJQUNyQyxNQUFNLEVBQUUsY0FBYyxFQUFFLEdBQUcsaUJBQWlCLENBQUMsVUFBVSxFQUFFLENBQUMsZ0JBQWdCLENBQUMsQ0FBQyxDQUFDO0lBQzdFLElBQ0UsT0FBTyxjQUFjLEtBQUssUUFBUTtXQUMvQixjQUFjLENBQUMsTUFBTSxLQUFLLENBQUM7V0FDM0IsQ0FBQyxjQUFjLENBQUMsWUFBWSxFQUFFO1dBQzlCLGNBQWMsQ0FBQyxRQUFRLENBQUMsR0FBRyxD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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lossless.org/client",
3
- "version": "1.21.1",
3
+ "version": "1.22.0",
4
4
  "private": false,
5
5
  "description": "One typed client for NoSQLDB, MongoDB, SQLDB, MariaDB, ClickHouse and S3 object storage.",
6
6
  "exports": {
@@ -81,15 +81,15 @@
81
81
  "@aws-sdk/client-s3": "^3.1039.0",
82
82
  "@clickhouse/client": "1.23.1",
83
83
  "@git.zone/tsbuild": "6.0.2",
84
- "@git.zone/tsbundle": "3.1.4",
85
- "@git.zone/tsrun": "3.0.2",
84
+ "@git.zone/tsbundle": "3.2.0",
85
+ "@git.zone/tsrun": "3.0.3",
86
86
  "@git.zone/tstest": "7.2.0",
87
87
  "@lossless.org/nosqldb": "8.0.2",
88
88
  "@lossless.org/nosqldb-qualified": "npm:@lossless.org/nosqldb@10.7.2",
89
89
  "@lossless.org/objectstorage": "10.3.1",
90
90
  "@lossless.org/sqldb": "0.2.3",
91
91
  "@push.rocks/qenv": "^6.1.3",
92
- "@types/node": "26.6.3",
92
+ "@types/node": "26.6.4",
93
93
  "mariadb": "3.5.4",
94
94
  "mongodb-memory-server": "11.1.0"
95
95
  },
package/readme.md CHANGED
@@ -118,6 +118,7 @@ Family imports preserve the established SmartData and SmartBucket names and cons
118
118
  | Subclasses need their own binding | a subclass of a bound model that declares decorators or a persistence policy of its own and no `@Collection()`, `@managed()` or `defineCollectionModel()` binding is refused with `invalid_configuration` on every read and write, so it never writes through the parent's schema; an undecorated subclass keeps the inherited binding |
119
119
  | Observing this process's writes | `db.observeWrites({ fields, listener })` — one frozen report per acknowledged model write, `{ collection, kind, documents, anchors }` with the requested top-level fields of the written documents and of the selector's equality anchors; writes inside a `db.createSession()` transaction are reported after the commit, never after a rollback; works on NoSQLDB, where `watch()` is unsupported |
120
120
  | Following this process's reads | `db.trackReads(fn)` resolves `{ result, reads }`; `reads.changed` turns true once a write this process makes through the database's models could have changed what a model read of `fn` returned, judged by the read's query and the ids it returned, and a write it cannot place counts as one that did, so a cache keyed by `reads` rebuilds only what a write touched; works on NoSQLDB |
121
+ | Following every process's writes | with `new SmartdataDb({ ..., writeRevisions: { partitionField: 'organizationId' } })` in every writer, the awaited `reads.isCurrent()` is true while no model write of any process could have changed the reads, counted per collection and partition value in the write's transaction, so one organization's writes keep another's sets current; `db.invalidateTrackedReads()` states raw writes and migrations; needs an engine identified as NoSQLDB, elsewhere `isCurrent()` is always false |
121
122
  | Disposable test databases on a unix socket | `SmartdataTestDatabase.create({ connection: { socketPath } })` from `@lossless.org/client/testsupport` reaches an engine that listens only on a unix-domain socket, fault helpers included; the loopback `host`/`port` form is unchanged |
122
123
 
123
124
  `docs/source/smartdata/readme.md` documents each one with its refusals.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@lossless.org/client',
6
- version: '1.21.1',
6
+ version: '1.22.0',
7
7
  description: 'One typed client for NoSQLDB, MongoDB, SQLDB, MariaDB, ClickHouse and S3 object storage.'
8
8
  }
@@ -1,6 +1,7 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import type { SmartdataCollection } from './classes.collection.js';
3
3
  import {
4
+ readPreviousWriteRevisions,
4
5
  reportSmartdataCollectionWrite,
5
6
  runWithOrdinarySmartdataSession,
6
7
  type TSmartdataOrdinarySession,
@@ -27,11 +28,15 @@ export const executeAtomicDelete = async <T>(
27
28
  {
28
29
  ordinaryWrite: true,
29
30
  prepared: collectionArg.isInitializedForCurrentDatabase(),
31
+ prepare: () => collectionArg.init(),
30
32
  collectionName: collectionArg.collectionName,
31
33
  timeoutMS: optsArg?.timeoutMS,
32
34
  },
33
35
  async (rawSessionArg, deadlineArg) => {
34
36
  await deadlineArg.join(collectionArg.init());
37
+ const previousPartitionsRead = await readPreviousWriteRevisions(
38
+ collectionArg.smartdataDb, collectionArg.mongoDbCollection, rawSessionArg, filterArg, deadlineArg,
39
+ );
35
40
  const result = await collectionArg.mongoDbCollection.deleteOne(
36
41
  filterArg,
37
42
  {
@@ -46,7 +51,7 @@ export const executeAtomicDelete = async <T>(
46
51
  'delete',
47
52
  null,
48
53
  filterArg,
49
- { images: 'none' },
54
+ { images: 'none', ...(previousPartitionsRead ? { previousPartitionsRead } : {}) },
50
55
  );
51
56
  }
52
57
  return result;
@@ -77,12 +82,16 @@ export const executeAtomicDeleteMany = async <T>(
77
82
  {
78
83
  ordinaryWrite: true,
79
84
  prepared: collectionArg.isInitializedForCurrentDatabase(),
85
+ prepare: () => collectionArg.init(),
80
86
  collectionName: collectionArg.collectionName,
81
87
  timeoutMS: optsArg?.timeoutMS,
82
88
  signal: optsArg?.signal,
83
89
  },
84
90
  async (rawSessionArg, deadlineArg) => {
85
91
  await deadlineArg.join(collectionArg.init());
92
+ const previousPartitionsRead = await readPreviousWriteRevisions(
93
+ collectionArg.smartdataDb, collectionArg.mongoDbCollection, rawSessionArg, filterArg, deadlineArg,
94
+ );
86
95
  const result = await collectionArg.mongoDbCollection.deleteMany(
87
96
  filterArg,
88
97
  {
@@ -97,7 +106,7 @@ export const executeAtomicDeleteMany = async <T>(
97
106
  'delete',
98
107
  null,
99
108
  filterArg,
100
- { images: 'none' },
109
+ { images: 'none', ...(previousPartitionsRead ? { previousPartitionsRead } : {}) },
101
110
  );
102
111
  }
103
112
  return result;
@@ -5,8 +5,10 @@ import {
5
5
  SmartdataPersistenceError,
6
6
  } from './classes.persistence.js';
7
7
  import {
8
+ readPreviousWriteRevisions,
8
9
  reportSmartdataCollectionWrite,
9
10
  runWithOrdinarySmartdataSession,
11
+ isOrdinarySessionInTransaction,
10
12
  isSmartdataSession,
11
13
  failSmartdataTransaction,
12
14
  type SmartdataSession,
@@ -100,7 +102,13 @@ export const executeAtomicFindOneAndUpdate = async <T>(
100
102
  },
101
103
  ): Promise<TAtomicFindOneAndUpdateExecutionResult> => {
102
104
  const policy = collectionArg.getBoundModelSchema()?.ordinaryPersistence;
103
- if (policy && (!isSmartdataSession(optionsArg.session) || optionsArg.returnDocument !== 'after')) {
105
+ // The caller's own transaction: one SmartData starts for write revisions
106
+ // would let a validated update run where it is otherwise refused.
107
+ if (policy && (
108
+ !isSmartdataSession(optionsArg.session)
109
+ || !isOrdinarySessionInTransaction(optionsArg.session)
110
+ || optionsArg.returnDocument !== 'after'
111
+ )) {
104
112
  throw new SmartdataPersistenceError('unsupported_operation',
105
113
  'Validated ordinary updates require an owned transaction and an after image.');
106
114
  }
@@ -110,6 +118,7 @@ export const executeAtomicFindOneAndUpdate = async <T>(
110
118
  {
111
119
  ordinaryWrite: true,
112
120
  prepared: collectionArg.isInitializedForCurrentDatabase(),
121
+ prepare: () => collectionArg.init(),
113
122
  collectionName: collectionArg.collectionName,
114
123
  timeoutMS: optionsArg.timeoutMS,
115
124
  },
@@ -120,6 +129,11 @@ export const executeAtomicFindOneAndUpdate = async <T>(
120
129
  }
121
130
  await deadlineArg.join(collectionArg.init());
122
131
  try {
132
+ // A before image states the document's previous partition itself.
133
+ const previousPartitionsRead = optionsArg.returnDocument === 'after'
134
+ && await readPreviousWriteRevisions(
135
+ collectionArg.smartdataDb, collectionArg.mongoDbCollection, rawSessionArg, filterArg, deadlineArg,
136
+ );
123
137
  const result = await collectionArg.mongoDbCollection.findOneAndUpdate(
124
138
  filterArg,
125
139
  updateArg,
@@ -149,6 +163,7 @@ export const executeAtomicFindOneAndUpdate = async <T>(
149
163
  {
150
164
  images: classified.document ? optionsArg.returnDocument : 'none',
151
165
  update: updateArg,
166
+ ...(previousPartitionsRead ? { previousPartitionsRead } : {}),
152
167
  },
153
168
  );
154
169
  }
@@ -202,12 +217,17 @@ export const executeAtomicFindOneAndReplace = async <T>(
202
217
  {
203
218
  ordinaryWrite: true,
204
219
  prepared: collectionArg.isInitializedForCurrentDatabase(),
220
+ prepare: () => collectionArg.init(),
205
221
  collectionName: collectionArg.collectionName,
206
222
  timeoutMS: optionsArg.timeoutMS,
207
223
  },
208
224
  async (rawSessionArg, deadlineArg) => {
209
225
  await deadlineArg.join(collectionArg.init());
210
226
  try {
227
+ const previousPartitionsRead = optionsArg.returnDocument === 'after'
228
+ && await readPreviousWriteRevisions(
229
+ collectionArg.smartdataDb, collectionArg.mongoDbCollection, rawSessionArg, filterArg, deadlineArg,
230
+ );
211
231
  const result = await collectionArg.mongoDbCollection.findOneAndReplace(
212
232
  filterArg,
213
233
  replacementArg,
@@ -237,7 +257,10 @@ export const executeAtomicFindOneAndReplace = async <T>(
237
257
  ? [classified.document]
238
258
  : [classified.document, replacementArg],
239
259
  filterArg,
240
- { images: optionsArg.returnDocument === 'after' ? 'after' : 'beforeAndAfter' },
260
+ {
261
+ images: optionsArg.returnDocument === 'after' ? 'after' : 'beforeAndAfter',
262
+ ...(previousPartitionsRead ? { previousPartitionsRead } : {}),
263
+ },
241
264
  );
242
265
  }
243
266
  return classified;
@@ -5,6 +5,7 @@ import {
5
5
  normalizeOrdinaryPersistenceError,
6
6
  } from './classes.persistence.js';
7
7
  import {
8
+ readPreviousWriteRevisions,
8
9
  reportSmartdataCollectionWrite,
9
10
  runWithOrdinarySmartdataSession,
10
11
  type TSmartdataOrdinarySession,
@@ -35,12 +36,16 @@ export const executeAtomicUpdate = async <T>(
35
36
  {
36
37
  ordinaryWrite: true,
37
38
  prepared: collectionArg.isInitializedForCurrentDatabase(),
39
+ prepare: () => collectionArg.init(),
38
40
  collectionName: collectionArg.collectionName,
39
41
  timeoutMS: optsArg?.timeoutMS,
40
42
  },
41
43
  async (rawSessionArg, deadlineArg) => {
42
44
  await deadlineArg.join(collectionArg.init());
43
45
  try {
46
+ const previousPartitionsRead = await readPreviousWriteRevisions(
47
+ collectionArg.smartdataDb, collectionArg.mongoDbCollection, rawSessionArg, filterArg, deadlineArg,
48
+ );
44
49
  const result = await collectionArg.mongoDbCollection.updateOne(
45
50
  filterArg,
46
51
  updateArg,
@@ -57,7 +62,7 @@ export const executeAtomicUpdate = async <T>(
57
62
  result.upsertedCount > 0 ? 'insert' : 'update',
58
63
  optsArg?.observedDocument ? [optsArg.observedDocument] : null,
59
64
  filterArg,
60
- { images: 'none', update: updateArg },
65
+ { images: 'none', update: updateArg, ...(previousPartitionsRead ? { previousPartitionsRead } : {}) },
61
66
  );
62
67
  }
63
68
  return result;
@@ -102,11 +107,13 @@ export const executeAtomicUpsertMany = async <T>(
102
107
  {
103
108
  ordinaryWrite: true,
104
109
  prepared: collectionArg.isInitializedForCurrentDatabase(),
110
+ prepare: () => collectionArg.init(),
105
111
  collectionName: collectionArg.collectionName,
106
112
  timeoutMS: optsArg?.timeoutMS,
107
113
  },
108
114
  async (rawSessionArg, deadlineArg) => {
109
115
  await deadlineArg.join(collectionArg.init());
116
+ let previousPartitionsRead = false;
110
117
  /**
111
118
  * Every entry upserts, so an entry that neither inserted nor failed
112
119
  * matched its document.
@@ -123,11 +130,22 @@ export const executeAtomicUpsertMany = async <T>(
123
130
  Object.hasOwn(upsertedIdsArg ?? {}, indexArg) ? 'insert' : 'update',
124
131
  null,
125
132
  operationArg.filter,
126
- { images: 'none', update: operationArg.update },
133
+ {
134
+ images: 'none',
135
+ update: operationArg.update,
136
+ ...(previousPartitionsRead ? { previousPartitionsRead } : {}),
137
+ },
127
138
  );
128
139
  });
129
140
  };
130
141
  try {
142
+ previousPartitionsRead = await readPreviousWriteRevisions(
143
+ collectionArg.smartdataDb,
144
+ collectionArg.mongoDbCollection,
145
+ rawSessionArg,
146
+ { $or: operationsArg.map((operationArg) => operationArg.filter) },
147
+ deadlineArg,
148
+ );
131
149
  const result = await collectionArg.mongoDbCollection.bulkWrite(
132
150
  operationsArg.map((operationArg) => ({
133
151
  updateOne: {
@@ -193,12 +211,16 @@ export const executeAtomicUpdateMany = async <T>(
193
211
  {
194
212
  ordinaryWrite: true,
195
213
  prepared: collectionArg.isInitializedForCurrentDatabase(),
214
+ prepare: () => collectionArg.init(),
196
215
  collectionName: collectionArg.collectionName,
197
216
  timeoutMS: optsArg?.timeoutMS,
198
217
  },
199
218
  async (rawSessionArg, deadlineArg) => {
200
219
  await deadlineArg.join(collectionArg.init());
201
220
  try {
221
+ const previousPartitionsRead = await readPreviousWriteRevisions(
222
+ collectionArg.smartdataDb, collectionArg.mongoDbCollection, rawSessionArg, filterArg, deadlineArg,
223
+ );
202
224
  const result = await collectionArg.mongoDbCollection.updateMany(
203
225
  filterArg,
204
226
  updateArg,
@@ -214,7 +236,7 @@ export const executeAtomicUpdateMany = async <T>(
214
236
  'update',
215
237
  null,
216
238
  filterArg,
217
- { images: 'none', update: updateArg },
239
+ { images: 'none', update: updateArg, ...(previousPartitionsRead ? { previousPartitionsRead } : {}) },
218
240
  );
219
241
  }
220
242
  return result;
@@ -6,8 +6,11 @@ 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, reportSmartdataWriteFailure } from './classes.session.js';
9
+ import {
10
+ reportSmartdataWrite, reportSmartdataWriteFailure, runWithOrdinarySmartdataSession,
11
+ } from './classes.session.js';
10
12
  import { runSmartdataWrite } from './classes.readtracker.js';
13
+ import { isSmartdataWriteRevisionsActive } from './classes.writerevisions.js';
11
14
  import {
12
15
  collectionModelSchemaResolverSymbol, collectionTopologyDbInspectionSymbol,
13
16
  getExpectedCollectionTopologyForSchema, inspectCollectionTopologySnapshot,
@@ -60,7 +63,11 @@ export const insertBootstrapRecord = async (
60
63
  )), schema.ordinaryPersistence, schema.persistedFields,
61
64
  );
62
65
  const expected = getExpectedCollectionTopologyForSchema(schema);
66
+ const reportInsert = (): void => reportSmartdataWrite(dbArg, undefined, {
67
+ collection: schema.collectionName, kind: 'insert', documents: [document], images: 'after',
68
+ });
63
69
  let dispatched = false;
70
+ let reported = false;
64
71
  let outcome: TBootstrapOutcome;
65
72
  try {
66
73
  // Explicit preparation uses this same connection-generation guard.
@@ -85,6 +92,26 @@ export const insertBootstrapRecord = async (
85
92
  if (topology.status !== 'absent' && topology.status !== 'exact') return 'invalid_configuration';
86
93
  const insertOptions = budget.operationOptions();
87
94
  dispatched = true;
95
+ if (isSmartdataWriteRevisionsActive(dbArg)) {
96
+ // The insert runs in a transaction of its own that commits its write revisions with it.
97
+ try {
98
+ const inserted = await runWithOrdinarySmartdataSession(undefined, dbArg, {
99
+ ordinaryWrite: true, prepared: true, ...insertOptions,
100
+ }, async (rawSessionArg, deadlineArg) => {
101
+ const result = await collection.insertOne(document, {
102
+ session: rawSessionArg, ...deadlineArg.commandOptions(),
103
+ });
104
+ if (!result.acknowledged || result.insertedId !== document._id) return false;
105
+ reportInsert();
106
+ return true;
107
+ });
108
+ reported = inserted;
109
+ budget.check();
110
+ return inserted ? 'inserted' : 'ambiguous_write';
111
+ } catch (errorArg) {
112
+ return isMongoDuplicateKeyError(errorArg) ? 'unique_conflict' : 'ambiguous_write';
113
+ }
114
+ }
88
115
  try {
89
116
  const result = await collection.insertOne(document, insertOptions);
90
117
  budget.check();
@@ -100,9 +127,7 @@ export const insertBootstrapRecord = async (
100
127
  : 'Bootstrap insertion could not inspect the current database connection.');
101
128
  }
102
129
  if (outcome === 'inserted') {
103
- reportSmartdataWrite(dbArg, undefined, {
104
- collection: schema.collectionName, kind: 'insert', documents: [document], images: 'after',
105
- });
130
+ if (!reported) reportInsert();
106
131
  return Object.freeze({ acknowledged: true, insertedId: document._id as string });
107
132
  }
108
133
  if (outcome === 'ambiguous_write') reportSmartdataWriteFailure(dbArg, undefined, schema.collectionName);