@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
|
@@ -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.
|
|
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.
|
|
85
|
-
"@git.zone/tsrun": "3.0.
|
|
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.
|
|
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.
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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 {
|
|
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
|
-
|
|
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);
|