@lossless.org/client 1.18.1 → 1.18.3
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.bsonassertion.d.ts +6 -2
- package/dist_ts/nosqldb/classes.bsonassertion.js +9 -7
- package/dist_ts/nosqldb/classes.collection.js +25 -6
- package/dist_ts/nosqldb/classes.exactpersistence.d.ts +41 -5
- package/dist_ts/nosqldb/classes.exactpersistence.js +133 -90
- package/dist_ts/nosqldb/classes.ordinarypersistence.js +2 -2
- package/package.json +4 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.bsonassertion.ts +9 -6
- package/ts/nosqldb/classes.collection.ts +48 -15
- package/ts/nosqldb/classes.exactpersistence.ts +183 -144
- package/ts/nosqldb/classes.ordinarypersistence.ts +2 -1
|
@@ -261,22 +261,19 @@ interface IExactModelConstructor<
|
|
|
261
261
|
prototype: TModel;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
/** A proven stored row: a fresh canonical decoding and its body's canonical bytes. */
|
|
265
|
+
interface IExactStoredRow<TPersisted extends object> {
|
|
266
|
+
document: TStoredDocument<TPersisted>;
|
|
267
|
+
bodyBytes: Uint8Array;
|
|
268
|
+
}
|
|
269
|
+
|
|
264
270
|
const exactCollectionCache = new WeakMap<
|
|
265
271
|
object,
|
|
266
272
|
WeakMap<object, SmartdataExactCollection<any, any>>
|
|
267
273
|
>();
|
|
268
274
|
|
|
269
|
-
const bytesEqual = (leftArg: Uint8Array, rightArg: Uint8Array): boolean =>
|
|
270
|
-
|
|
271
|
-
return false;
|
|
272
|
-
}
|
|
273
|
-
for (let index = 0; index < leftArg.byteLength; index++) {
|
|
274
|
-
if (leftArg[index] !== rightArg[index]) {
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return true;
|
|
279
|
-
};
|
|
275
|
+
const bytesEqual = (leftArg: Uint8Array, rightArg: Uint8Array): boolean =>
|
|
276
|
+
Buffer.compare(leftArg, rightArg) === 0;
|
|
280
277
|
|
|
281
278
|
const hasOwn = (documentArg: object, keyArg: PropertyKey): boolean =>
|
|
282
279
|
Object.prototype.hasOwnProperty.call(documentArg, keyArg);
|
|
@@ -1150,21 +1147,39 @@ export class SmartdataExactCollection<
|
|
|
1150
1147
|
);
|
|
1151
1148
|
}
|
|
1152
1149
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1150
|
+
/** The collection's one BSON encoder: every encoding of a row passes here. */
|
|
1151
|
+
private encodeBson(documentArg: object): Uint8Array {
|
|
1152
|
+
return plugins.mongodb.BSON.serialize(documentArg);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/** The collection's one BSON decoder: every decoding of a row passes here. */
|
|
1156
|
+
private decodeBson(bytesArg: Uint8Array): plugins.mongodb.Document {
|
|
1157
|
+
return plugins.mongodb.BSON.deserialize(bytesArg);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/** Decodes proven canonical bytes into a fresh canonical document. */
|
|
1161
|
+
private decodeCanonical<TDocument extends object>(
|
|
1162
|
+
bytesArg: Uint8Array,
|
|
1156
1163
|
): TDocument {
|
|
1157
|
-
|
|
1164
|
+
return canonicalizeValue(this.decodeBson(bytesArg) as TDocument);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* The exactness proof: the document's sorted encoding must decode and
|
|
1169
|
+
* encode again to the same bytes. Returns those canonical bytes and the
|
|
1170
|
+
* decoding the proof produced, which no caller has seen yet.
|
|
1171
|
+
*/
|
|
1172
|
+
private canonicalizeDocument(
|
|
1173
|
+
documentArg: object,
|
|
1174
|
+
labelArg: string,
|
|
1175
|
+
): { document: Record<string, unknown>; bytes: Uint8Array } {
|
|
1158
1176
|
let serialized: Uint8Array;
|
|
1159
|
-
let canonical:
|
|
1177
|
+
let canonical: Record<string, unknown>;
|
|
1160
1178
|
let reserialized: Uint8Array;
|
|
1161
1179
|
try {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
plugins.mongodb.BSON.deserialize(serialized) as TDocument,
|
|
1166
|
-
);
|
|
1167
|
-
reserialized = plugins.mongodb.BSON.serialize(canonical);
|
|
1180
|
+
serialized = this.encodeBson(canonicalizeValue(documentArg));
|
|
1181
|
+
canonical = canonicalizeValue(this.decodeBson(serialized));
|
|
1182
|
+
reserialized = this.encodeBson(canonical);
|
|
1168
1183
|
} catch (errorArg) {
|
|
1169
1184
|
throw new SmartdataExactPersistenceError(
|
|
1170
1185
|
'invalid_document',
|
|
@@ -1178,7 +1193,23 @@ export class SmartdataExactCollection<
|
|
|
1178
1193
|
`${labelArg} is not byte-stable across a BSON roundtrip.`,
|
|
1179
1194
|
);
|
|
1180
1195
|
}
|
|
1181
|
-
return canonical;
|
|
1196
|
+
return { document: canonical, bytes: serialized };
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Whether a document encodes canonically to bytes an earlier proof
|
|
1201
|
+
* produced. Equal bytes carry that proof and its assertion over, so the
|
|
1202
|
+
* caller needs neither again; on a mismatch the caller runs the full proof
|
|
1203
|
+
* to report what is wrong.
|
|
1204
|
+
*/
|
|
1205
|
+
private encodesAs(documentArg: object, provenBytesArg: Uint8Array): boolean {
|
|
1206
|
+
let bytes: Uint8Array;
|
|
1207
|
+
try {
|
|
1208
|
+
bytes = this.encodeBson(canonicalizeValue(documentArg));
|
|
1209
|
+
} catch {
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
return bytesEqual(bytes, provenBytesArg);
|
|
1182
1213
|
}
|
|
1183
1214
|
|
|
1184
1215
|
private assertAllowedTopLevelKeys(
|
|
@@ -1209,29 +1240,47 @@ export class SmartdataExactCollection<
|
|
|
1209
1240
|
}
|
|
1210
1241
|
}
|
|
1211
1242
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1243
|
+
/**
|
|
1244
|
+
* Proves an application body canonical and runs the assertion once, on the
|
|
1245
|
+
* proof's own decoding, which is never handed out. Returns the canonical
|
|
1246
|
+
* bytes of the body.
|
|
1247
|
+
*/
|
|
1248
|
+
private provePersistedDocument(documentArg: object): Uint8Array {
|
|
1215
1249
|
this.assertAllowedTopLevelKeys(documentArg, [], 'Persisted document');
|
|
1216
|
-
const
|
|
1250
|
+
const { document, bytes } = this.canonicalizeDocument(
|
|
1217
1251
|
documentArg,
|
|
1218
1252
|
'Persisted document',
|
|
1219
1253
|
);
|
|
1220
|
-
this.
|
|
1221
|
-
|
|
1254
|
+
this.assertAllowedTopLevelKeys(document, [], 'Persisted document');
|
|
1255
|
+
this.assertWithoutMutation(document, bytes, 'Persisted document');
|
|
1256
|
+
return bytes;
|
|
1222
1257
|
}
|
|
1223
1258
|
|
|
1224
|
-
|
|
1259
|
+
/**
|
|
1260
|
+
* Proves a stored envelope canonical, checks its storage metadata and runs
|
|
1261
|
+
* the assertion once on its body. The body is the proof's own decoding with
|
|
1262
|
+
* the metadata removed, so the assertion never sees a document a caller
|
|
1263
|
+
* receives. Returns the canonical bytes of the envelope and of its body.
|
|
1264
|
+
*/
|
|
1265
|
+
private proveStoredDocument(
|
|
1225
1266
|
documentArg: object,
|
|
1226
|
-
):
|
|
1267
|
+
): { bytes: Uint8Array; bodyBytes: Uint8Array } {
|
|
1227
1268
|
this.assertAllowedTopLevelKeys(
|
|
1228
1269
|
documentArg,
|
|
1229
1270
|
storageMetadataKeys,
|
|
1230
1271
|
'Stored document',
|
|
1231
1272
|
);
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
|
|
1273
|
+
const { document, bytes } = this.canonicalizeDocument(
|
|
1274
|
+
documentArg,
|
|
1275
|
+
'Stored document',
|
|
1276
|
+
);
|
|
1277
|
+
this.assertAllowedTopLevelKeys(
|
|
1278
|
+
document,
|
|
1279
|
+
storageMetadataKeys,
|
|
1280
|
+
'Stored document',
|
|
1281
|
+
);
|
|
1282
|
+
const revision = document._smartdataRevision;
|
|
1283
|
+
if (!isMongoObjectId(document._id)) {
|
|
1235
1284
|
throw new SmartdataExactPersistenceError(
|
|
1236
1285
|
'invalid_document',
|
|
1237
1286
|
'Stored document _id must be a MongoDB ObjectId.',
|
|
@@ -1246,15 +1295,55 @@ export class SmartdataExactCollection<
|
|
|
1246
1295
|
'Stored document _smartdataRevision must be a nonempty string when present.',
|
|
1247
1296
|
);
|
|
1248
1297
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1298
|
+
for (const key of storageMetadataKeys) {
|
|
1299
|
+
delete document[key];
|
|
1300
|
+
}
|
|
1301
|
+
const bodyBytes = this.encodeBson(document);
|
|
1302
|
+
this.assertWithoutMutation(document, bodyBytes, 'Persisted document');
|
|
1303
|
+
return { bytes, bodyBytes };
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/** Proves a stored envelope and returns a fresh canonical decoding of it. */
|
|
1307
|
+
private readStoredDocument(documentArg: object): IExactStoredRow<TPersisted> {
|
|
1308
|
+
const { bytes, bodyBytes } = this.proveStoredDocument(documentArg);
|
|
1309
|
+
return {
|
|
1310
|
+
document: this.decodeCanonical<TStoredDocument<TPersisted>>(bytes),
|
|
1311
|
+
bodyBytes,
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* The canonical envelope of a proven body. BSON encodes every element on
|
|
1317
|
+
* its own, so a proven body with inert metadata is canonical as a whole;
|
|
1318
|
+
* only the canonical key order has to be restored.
|
|
1319
|
+
*/
|
|
1320
|
+
private composeStoredDocument(
|
|
1321
|
+
bodyArg: TExactPersistedDocument<TPersisted>,
|
|
1322
|
+
metadataArg: { _id: plugins.mongodb.ObjectId; _smartdataRevision?: string },
|
|
1323
|
+
): TStoredDocument<TPersisted> {
|
|
1324
|
+
const fields = new Map<string, unknown>(Object.entries(bodyArg));
|
|
1325
|
+
fields.set('_id', metadataArg._id);
|
|
1326
|
+
if (metadataArg._smartdataRevision !== undefined) {
|
|
1327
|
+
fields.set('_smartdataRevision', metadataArg._smartdataRevision);
|
|
1328
|
+
}
|
|
1329
|
+
const envelope = Object.create(null) as Record<string, unknown>;
|
|
1330
|
+
for (const key of [...fields.keys()].sort()) {
|
|
1331
|
+
envelope[key] = fields.get(key);
|
|
1332
|
+
}
|
|
1333
|
+
return envelope as TStoredDocument<TPersisted>;
|
|
1251
1334
|
}
|
|
1252
1335
|
|
|
1253
1336
|
private assertWithoutMutation(
|
|
1254
1337
|
documentArg: object,
|
|
1338
|
+
beforeArg: Uint8Array,
|
|
1255
1339
|
labelArg: string,
|
|
1256
1340
|
): void {
|
|
1257
|
-
assertBsonWithoutMutation(
|
|
1341
|
+
assertBsonWithoutMutation(
|
|
1342
|
+
documentArg,
|
|
1343
|
+
beforeArg,
|
|
1344
|
+
(documentToEncodeArg) => this.encodeBson(documentToEncodeArg),
|
|
1345
|
+
this.options.assertDocument,
|
|
1346
|
+
(reasonArg, causeArg) =>
|
|
1258
1347
|
new SmartdataExactPersistenceError('invalid_document', `${labelArg} ${
|
|
1259
1348
|
reasonArg === 'assertion' ? 'assertion failed.' :
|
|
1260
1349
|
reasonArg === 'bson' ? 'validator produced a non-BSON document.' :
|
|
@@ -1262,46 +1351,8 @@ export class SmartdataExactCollection<
|
|
|
1262
1351
|
}`, causeArg === undefined ? undefined : { cause: causeArg }));
|
|
1263
1352
|
}
|
|
1264
1353
|
|
|
1265
|
-
private getPersistedBody(
|
|
1266
|
-
storedDocumentArg: TStoredDocument<TPersisted>,
|
|
1267
|
-
): TExactPersistedDocument<TPersisted> {
|
|
1268
|
-
const body = Object.create(null) as Record<string, unknown>;
|
|
1269
|
-
for (const key of Object.keys(storedDocumentArg)) {
|
|
1270
|
-
if (!reservedKeys.includes(key as TReservedKey)) {
|
|
1271
|
-
body[key] = (storedDocumentArg as Record<string, unknown>)[key];
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
return this.canonicalizePersistedDocument(body);
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
private persistedDocumentsEqual(
|
|
1278
|
-
leftArg: TExactPersistedDocument<TPersisted>,
|
|
1279
|
-
rightArg: TExactPersistedDocument<TPersisted>,
|
|
1280
|
-
): boolean {
|
|
1281
|
-
return bytesEqual(
|
|
1282
|
-
plugins.mongodb.BSON.serialize(leftArg),
|
|
1283
|
-
plugins.mongodb.BSON.serialize(rightArg),
|
|
1284
|
-
);
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
private storedDocumentsEqual(
|
|
1288
|
-
leftArg: TStoredDocument<TPersisted>,
|
|
1289
|
-
rightArg: TStoredDocument<TPersisted>,
|
|
1290
|
-
): boolean {
|
|
1291
|
-
if (!leftArg._id.equals(rightArg._id)) {
|
|
1292
|
-
return false;
|
|
1293
|
-
}
|
|
1294
|
-
if (leftArg._smartdataRevision !== rightArg._smartdataRevision) {
|
|
1295
|
-
return false;
|
|
1296
|
-
}
|
|
1297
|
-
return this.persistedDocumentsEqual(
|
|
1298
|
-
this.getPersistedBody(leftArg),
|
|
1299
|
-
this.getPersistedBody(rightArg),
|
|
1300
|
-
);
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
1354
|
private validateReconcileIdentity(
|
|
1304
|
-
documentArg: TExactPersistedDocument<TPersisted>,
|
|
1355
|
+
documentArg: TExactPersistedDocument<TPersisted> | TStoredDocument<TPersisted>,
|
|
1305
1356
|
): string {
|
|
1306
1357
|
const value = (documentArg as Record<string, unknown>)[this.reconcileBy];
|
|
1307
1358
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
@@ -1313,9 +1364,10 @@ export class SmartdataExactCollection<
|
|
|
1313
1364
|
return value;
|
|
1314
1365
|
}
|
|
1315
1366
|
|
|
1367
|
+
/** The persisted body a model serializes to, before any proof. */
|
|
1316
1368
|
private async serializeModel(
|
|
1317
1369
|
modelArg: TModel,
|
|
1318
|
-
): Promise<
|
|
1370
|
+
): Promise<Record<string, unknown>> {
|
|
1319
1371
|
const serialized = (await modelArg.createSavableObject()) as Record<
|
|
1320
1372
|
string,
|
|
1321
1373
|
unknown
|
|
@@ -1353,12 +1405,13 @@ export class SmartdataExactCollection<
|
|
|
1353
1405
|
body[key] = serialized[key];
|
|
1354
1406
|
}
|
|
1355
1407
|
}
|
|
1356
|
-
return
|
|
1408
|
+
return body;
|
|
1357
1409
|
}
|
|
1358
1410
|
|
|
1411
|
+
/** Proves an insert input and returns the canonical bytes of its body. */
|
|
1359
1412
|
private async createPersistedDocument(
|
|
1360
1413
|
documentArg: TModel | TExactPersistedDocument<TPersisted>,
|
|
1361
|
-
): Promise<
|
|
1414
|
+
): Promise<Uint8Array> {
|
|
1362
1415
|
if (typeof documentArg !== 'object' || documentArg === null) {
|
|
1363
1416
|
throw new SmartdataExactPersistenceError(
|
|
1364
1417
|
'invalid_document',
|
|
@@ -1366,9 +1419,9 @@ export class SmartdataExactCollection<
|
|
|
1366
1419
|
);
|
|
1367
1420
|
}
|
|
1368
1421
|
if (documentArg instanceof this.modelConstructor) {
|
|
1369
|
-
return this.serializeModel(documentArg);
|
|
1422
|
+
return this.provePersistedDocument(await this.serializeModel(documentArg));
|
|
1370
1423
|
}
|
|
1371
|
-
return this.
|
|
1424
|
+
return this.provePersistedDocument(documentArg);
|
|
1372
1425
|
}
|
|
1373
1426
|
|
|
1374
1427
|
private hydrateStoredDocument(
|
|
@@ -1383,12 +1436,12 @@ export class SmartdataExactCollection<
|
|
|
1383
1436
|
identityArg: string,
|
|
1384
1437
|
sessionArg: plugins.mongodb.ClientSession | undefined,
|
|
1385
1438
|
deadlineArg: SmartdataOperationDeadline,
|
|
1386
|
-
): Promise<
|
|
1439
|
+
): Promise<IExactStoredRow<TPersisted> | null> {
|
|
1387
1440
|
const rawDocument = await this.collection.mongoDbCollection.findOne({
|
|
1388
1441
|
[this.reconcileBy]: identityArg,
|
|
1389
1442
|
}, { session: sessionArg, ...deadlineArg.commandOptions() });
|
|
1390
1443
|
return rawDocument
|
|
1391
|
-
? this.
|
|
1444
|
+
? this.readStoredDocument(rawDocument)
|
|
1392
1445
|
: null;
|
|
1393
1446
|
}
|
|
1394
1447
|
|
|
@@ -1438,12 +1491,10 @@ export class SmartdataExactCollection<
|
|
|
1438
1491
|
currentDescriptor.value,
|
|
1439
1492
|
'Exact delete current',
|
|
1440
1493
|
);
|
|
1441
|
-
const current = this.
|
|
1494
|
+
const { document: current } = this.readStoredDocument(
|
|
1442
1495
|
currentSnapshot as object,
|
|
1443
1496
|
);
|
|
1444
|
-
const identity = this.validateReconcileIdentity(
|
|
1445
|
-
this.getPersistedBody(current),
|
|
1446
|
-
);
|
|
1497
|
+
const identity = this.validateReconcileIdentity(current);
|
|
1447
1498
|
return { current, identity };
|
|
1448
1499
|
}
|
|
1449
1500
|
|
|
@@ -1454,7 +1505,7 @@ export class SmartdataExactCollection<
|
|
|
1454
1505
|
sessionArg: plugins.mongodb.ClientSession | undefined,
|
|
1455
1506
|
deadlineArg: SmartdataOperationDeadline,
|
|
1456
1507
|
): Promise<TExactDeleteResult> {
|
|
1457
|
-
let reconciled:
|
|
1508
|
+
let reconciled: IExactStoredRow<TPersisted> | null;
|
|
1458
1509
|
try {
|
|
1459
1510
|
reconciled = await this.findByReconcileIdentity(identityArg, sessionArg, deadlineArg);
|
|
1460
1511
|
} catch (errorArg) {
|
|
@@ -1466,7 +1517,7 @@ export class SmartdataExactCollection<
|
|
|
1466
1517
|
causeArg,
|
|
1467
1518
|
);
|
|
1468
1519
|
}
|
|
1469
|
-
if (reconciled && reconciled._id.equals(currentArg._id)) {
|
|
1520
|
+
if (reconciled && reconciled.document._id.equals(currentArg._id)) {
|
|
1470
1521
|
return { status: 'concurrent_change', deletedCount: 0 };
|
|
1471
1522
|
}
|
|
1472
1523
|
throw this.ambiguousWriteError(
|
|
@@ -1480,21 +1531,21 @@ export class SmartdataExactCollection<
|
|
|
1480
1531
|
optionsArg: IExactBoundedOptions = {},
|
|
1481
1532
|
): Promise<TExactInsertResult<TPersisted>> {
|
|
1482
1533
|
return this.runOperation(optionsArg, async (sessionArg, deadlineArg) => {
|
|
1483
|
-
const
|
|
1534
|
+
const persistedBytes = await this.createPersistedDocument(documentArg);
|
|
1535
|
+
const persistedDocument = this.decodeCanonical<
|
|
1536
|
+
TExactPersistedDocument<TPersisted>
|
|
1537
|
+
>(persistedBytes);
|
|
1484
1538
|
const identity = this.validateReconcileIdentity(persistedDocument);
|
|
1485
|
-
const expectedStoredDocument = this.
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1539
|
+
const expectedStoredDocument = this.composeStoredDocument(
|
|
1540
|
+
persistedDocument,
|
|
1541
|
+
{ _id: new plugins.mongodb.ObjectId() },
|
|
1542
|
+
);
|
|
1489
1543
|
if (hasActiveTransaction(sessionArg)) {
|
|
1490
1544
|
const existing = await this.findByReconcileIdentity(identity, sessionArg, deadlineArg);
|
|
1491
1545
|
if (existing) {
|
|
1492
|
-
return
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
)
|
|
1496
|
-
? { status: 'already_committed', document: existing }
|
|
1497
|
-
: { status: 'conflict', document: existing };
|
|
1546
|
+
return bytesEqual(existing.bodyBytes, persistedBytes)
|
|
1547
|
+
? { status: 'already_committed', document: existing.document }
|
|
1548
|
+
: { status: 'conflict', document: existing.document };
|
|
1498
1549
|
}
|
|
1499
1550
|
}
|
|
1500
1551
|
|
|
@@ -1522,7 +1573,7 @@ export class SmartdataExactCollection<
|
|
|
1522
1573
|
if (deadlineArg.interrupted(errorArg)) {
|
|
1523
1574
|
throw this.interruptedWriteError('Exact insert', errorArg);
|
|
1524
1575
|
}
|
|
1525
|
-
let reconciled:
|
|
1576
|
+
let reconciled: IExactStoredRow<TPersisted> | null;
|
|
1526
1577
|
try {
|
|
1527
1578
|
reconciled = await this.findByReconcileIdentity(identity, sessionArg, deadlineArg);
|
|
1528
1579
|
} catch (reconciliationError) {
|
|
@@ -1542,16 +1593,11 @@ export class SmartdataExactCollection<
|
|
|
1542
1593
|
);
|
|
1543
1594
|
}
|
|
1544
1595
|
if (reconciled) {
|
|
1545
|
-
if (
|
|
1546
|
-
|
|
1547
|
-
this.getPersistedBody(reconciled),
|
|
1548
|
-
persistedDocument,
|
|
1549
|
-
)
|
|
1550
|
-
) {
|
|
1551
|
-
return { status: 'already_committed', document: reconciled };
|
|
1596
|
+
if (bytesEqual(reconciled.bodyBytes, persistedBytes)) {
|
|
1597
|
+
return { status: 'already_committed', document: reconciled.document };
|
|
1552
1598
|
}
|
|
1553
1599
|
if (isDuplicateKeyError(errorArg)) {
|
|
1554
|
-
return { status: 'conflict', document: reconciled };
|
|
1600
|
+
return { status: 'conflict', document: reconciled.document };
|
|
1555
1601
|
}
|
|
1556
1602
|
}
|
|
1557
1603
|
if (isDuplicateKeyError(errorArg)) {
|
|
@@ -1574,7 +1620,7 @@ export class SmartdataExactCollection<
|
|
|
1574
1620
|
result,
|
|
1575
1621
|
);
|
|
1576
1622
|
}
|
|
1577
|
-
let reconciled:
|
|
1623
|
+
let reconciled: IExactStoredRow<TPersisted> | null;
|
|
1578
1624
|
try {
|
|
1579
1625
|
reconciled = await this.findByReconcileIdentity(identity, sessionArg, deadlineArg);
|
|
1580
1626
|
} catch (errorArg) {
|
|
@@ -1586,14 +1632,8 @@ export class SmartdataExactCollection<
|
|
|
1586
1632
|
result,
|
|
1587
1633
|
);
|
|
1588
1634
|
}
|
|
1589
|
-
if (
|
|
1590
|
-
reconciled
|
|
1591
|
-
this.persistedDocumentsEqual(
|
|
1592
|
-
this.getPersistedBody(reconciled),
|
|
1593
|
-
persistedDocument,
|
|
1594
|
-
)
|
|
1595
|
-
) {
|
|
1596
|
-
return { status: 'already_committed', document: reconciled };
|
|
1635
|
+
if (reconciled && bytesEqual(reconciled.bodyBytes, persistedBytes)) {
|
|
1636
|
+
return { status: 'already_committed', document: reconciled.document };
|
|
1597
1637
|
}
|
|
1598
1638
|
throw this.ambiguousWriteError(
|
|
1599
1639
|
'Exact insert was not acknowledged and could not be reconciled.',
|
|
@@ -1607,19 +1647,15 @@ export class SmartdataExactCollection<
|
|
|
1607
1647
|
public toPersisted(
|
|
1608
1648
|
storedDocumentArg: TStoredDocument<TPersisted>,
|
|
1609
1649
|
): TExactPersistedDocument<TPersisted> {
|
|
1610
|
-
const
|
|
1611
|
-
|
|
1612
|
-
return
|
|
1613
|
-
plugins.mongodb.BSON.serialize(persistedDocument),
|
|
1614
|
-
) as TExactPersistedDocument<TPersisted>;
|
|
1650
|
+
const { bodyBytes } = this.proveStoredDocument(storedDocumentArg);
|
|
1651
|
+
// A plain decoding: ordinary application objects, not canonical ones.
|
|
1652
|
+
return this.decodeBson(bodyBytes) as TExactPersistedDocument<TPersisted>;
|
|
1615
1653
|
}
|
|
1616
1654
|
|
|
1617
1655
|
public canonicalBytes(
|
|
1618
1656
|
persistedDocumentArg: TExactPersistedDocument<TPersisted>,
|
|
1619
1657
|
): Uint8Array {
|
|
1620
|
-
return
|
|
1621
|
-
this.canonicalizePersistedDocument(persistedDocumentArg),
|
|
1622
|
-
);
|
|
1658
|
+
return this.provePersistedDocument(persistedDocumentArg);
|
|
1623
1659
|
}
|
|
1624
1660
|
|
|
1625
1661
|
public async findStoredOne(
|
|
@@ -1645,7 +1681,7 @@ export class SmartdataExactCollection<
|
|
|
1645
1681
|
try {
|
|
1646
1682
|
const rawDocument = await cursor.next();
|
|
1647
1683
|
storedDocument = rawDocument
|
|
1648
|
-
? this.
|
|
1684
|
+
? this.readStoredDocument(rawDocument).document
|
|
1649
1685
|
: null;
|
|
1650
1686
|
} catch (errorArg) {
|
|
1651
1687
|
try {
|
|
@@ -1689,7 +1725,7 @@ export class SmartdataExactCollection<
|
|
|
1689
1725
|
}
|
|
1690
1726
|
const rawDocuments = await cursor.toArray();
|
|
1691
1727
|
storedDocuments = rawDocuments.map((documentArg) =>
|
|
1692
|
-
this.
|
|
1728
|
+
this.readStoredDocument(documentArg).document,
|
|
1693
1729
|
);
|
|
1694
1730
|
} catch (errorArg) {
|
|
1695
1731
|
try {
|
|
@@ -1744,12 +1780,14 @@ export class SmartdataExactCollection<
|
|
|
1744
1780
|
sessionOptionsArg: IExactBoundedOptions = {},
|
|
1745
1781
|
): Promise<TExactTransitionResult<TPersisted>> {
|
|
1746
1782
|
return this.runOperation(sessionOptionsArg, async (sessionArg, deadlineArg) => {
|
|
1747
|
-
const current =
|
|
1748
|
-
|
|
1749
|
-
const currentIdentity = this.validateReconcileIdentity(
|
|
1783
|
+
const { document: current, bodyBytes: currentBodyBytes } =
|
|
1784
|
+
this.readStoredDocument(optionsArg.current);
|
|
1785
|
+
const currentIdentity = this.validateReconcileIdentity(current);
|
|
1750
1786
|
const model = this.hydrateStoredDocument(current);
|
|
1751
1787
|
const untouchedBody = await this.serializeModel(model);
|
|
1752
|
-
if (!this.
|
|
1788
|
+
if (!this.encodesAs(untouchedBody, currentBodyBytes)) {
|
|
1789
|
+
// A body that is not even canonical reports its own proof failure.
|
|
1790
|
+
this.provePersistedDocument(untouchedBody);
|
|
1753
1791
|
throw new SmartdataExactPersistenceError(
|
|
1754
1792
|
'noncanonical_document',
|
|
1755
1793
|
'Stored document does not survive an untouched model serialization roundtrip.',
|
|
@@ -1769,7 +1807,9 @@ export class SmartdataExactCollection<
|
|
|
1769
1807
|
);
|
|
1770
1808
|
}
|
|
1771
1809
|
|
|
1772
|
-
const candidateBody =
|
|
1810
|
+
const candidateBody = this.decodeCanonical<
|
|
1811
|
+
TExactPersistedDocument<TPersisted>
|
|
1812
|
+
>(this.provePersistedDocument(await this.serializeModel(model)));
|
|
1773
1813
|
const candidateIdentity = this.validateReconcileIdentity(candidateBody);
|
|
1774
1814
|
if (candidateIdentity !== currentIdentity) {
|
|
1775
1815
|
throw new SmartdataExactPersistenceError(
|
|
@@ -1778,11 +1818,11 @@ export class SmartdataExactCollection<
|
|
|
1778
1818
|
);
|
|
1779
1819
|
}
|
|
1780
1820
|
const newRevision = globalThis.crypto.randomUUID();
|
|
1781
|
-
const expectedStoredDocument = this.
|
|
1821
|
+
const expectedStoredDocument = this.composeStoredDocument(candidateBody, {
|
|
1782
1822
|
_id: current._id,
|
|
1783
|
-
...candidateBody,
|
|
1784
1823
|
_smartdataRevision: newRevision,
|
|
1785
1824
|
});
|
|
1825
|
+
const expectedBytes = this.encodeBson(expectedStoredDocument);
|
|
1786
1826
|
const selector = current._smartdataRevision !== undefined
|
|
1787
1827
|
? { _id: current._id, _smartdataRevision: current._smartdataRevision }
|
|
1788
1828
|
: { _id: current._id, _smartdataRevision: { $exists: false } };
|
|
@@ -1803,14 +1843,15 @@ export class SmartdataExactCollection<
|
|
|
1803
1843
|
if (!postimage) {
|
|
1804
1844
|
return { status: 'concurrent_change' };
|
|
1805
1845
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1846
|
+
if (!this.encodesAs(postimage, expectedBytes)) {
|
|
1847
|
+
// A postimage that is not even a valid stored document reports why.
|
|
1848
|
+
this.proveStoredDocument(postimage);
|
|
1808
1849
|
throw new SmartdataExactPersistenceError(
|
|
1809
1850
|
'ambiguous_write',
|
|
1810
1851
|
'Exact transition returned a postimage different from the requested state.',
|
|
1811
1852
|
);
|
|
1812
1853
|
}
|
|
1813
|
-
return { status: 'transitioned', document:
|
|
1854
|
+
return { status: 'transitioned', document: expectedStoredDocument };
|
|
1814
1855
|
} catch (errorArg) {
|
|
1815
1856
|
if (isTransientTransactionError(errorArg, sessionArg)) {
|
|
1816
1857
|
throw errorArg;
|
|
@@ -1833,13 +1874,11 @@ export class SmartdataExactCollection<
|
|
|
1833
1874
|
_id: current._id,
|
|
1834
1875
|
}, { session: sessionArg, ...deadlineArg.commandOptions() });
|
|
1835
1876
|
if (reconciledRaw) {
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
reconciled._smartdataRevision === newRevision &&
|
|
1839
|
-
this.storedDocumentsEqual(reconciled, expectedStoredDocument)
|
|
1840
|
-
) {
|
|
1841
|
-
return { status: 'transitioned', document: reconciled };
|
|
1877
|
+
if (this.encodesAs(reconciledRaw, expectedBytes)) {
|
|
1878
|
+
return { status: 'transitioned', document: expectedStoredDocument };
|
|
1842
1879
|
}
|
|
1880
|
+
// A malformed document fails the reconciliation itself.
|
|
1881
|
+
this.proveStoredDocument(reconciledRaw);
|
|
1843
1882
|
}
|
|
1844
1883
|
} catch (reconciliationError) {
|
|
1845
1884
|
if (isTransientTransactionError(reconciliationError, sessionArg)) {
|
|
@@ -94,7 +94,8 @@ export const validateOrdinaryStoredDocument = (
|
|
|
94
94
|
: !(clone._id instanceof plugins.mongodb.ObjectId)) throw invalid();
|
|
95
95
|
if (policyArg.timestamps === 'managed' &&
|
|
96
96
|
(typeof clone._createdAt !== 'string' || typeof clone._updatedAt !== 'string')) throw invalid();
|
|
97
|
-
assertBsonWithoutMutation(clone,
|
|
97
|
+
assertBsonWithoutMutation(clone, plugins.mongodb.BSON.serialize(clone),
|
|
98
|
+
(documentArg) => plugins.mongodb.BSON.serialize(documentArg), policyArg.assertStoredDocument, invalid);
|
|
98
99
|
// The assertion must not retain authority to mutate the returned document.
|
|
99
100
|
return plugins.mongodb.BSON.deserialize(plugins.mongodb.BSON.serialize(clone));
|
|
100
101
|
};
|