@lossless.org/client 1.6.0 → 1.8.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.d.ts +1 -0
- package/dist_ts/nosqldb/classes.atomicdelete.js +10 -7
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +2 -0
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +9 -5
- package/dist_ts/nosqldb/classes.atomicupdate.d.ts +2 -0
- package/dist_ts/nosqldb/classes.atomicupdate.js +13 -8
- package/dist_ts/nosqldb/classes.collection.d.ts +13 -4
- package/dist_ts/nosqldb/classes.collection.js +57 -64
- package/dist_ts/nosqldb/classes.db.d.ts +1 -1
- package/dist_ts/nosqldb/classes.doc.d.ts +60 -3
- package/dist_ts/nosqldb/classes.doc.js +68 -26
- package/dist_ts/nosqldb/classes.exactpersistence.d.ts +36 -7
- package/dist_ts/nosqldb/classes.exactpersistence.js +92 -81
- package/dist_ts/nosqldb/classes.operationdeadline.d.ts +61 -0
- package/dist_ts/nosqldb/classes.operationdeadline.js +149 -0
- package/dist_ts/nosqldb/classes.persistence.d.ts +16 -1
- package/dist_ts/nosqldb/classes.persistence.js +20 -1
- package/dist_ts/nosqldb/classes.session.d.ts +73 -2
- package/dist_ts/nosqldb/classes.session.js +164 -38
- package/dist_ts/objectstorage/classes.bucket.d.ts +61 -3
- package/dist_ts/objectstorage/classes.bucket.js +243 -184
- package/dist_ts/objectstorage/classes.directory.js +17 -28
- package/dist_ts/objectstorage/classes.smartbucket.d.ts +13 -6
- package/dist_ts/objectstorage/classes.smartbucket.js +18 -12
- package/dist_ts/objectstorage/classes.watcher.js +1 -2
- package/dist_ts/objectstorage/interfaces.d.ts +8 -0
- package/dist_ts/objectstorage/internal.bucketrequests.d.ts +14 -0
- package/dist_ts/objectstorage/internal.bucketrequests.js +53 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.d.ts +12 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.js +46 -17
- package/package.json +3 -3
- package/readme.md +13 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.atomicdelete.ts +10 -6
- package/ts/nosqldb/classes.atomicfindoneandupdate.ts +10 -4
- package/ts/nosqldb/classes.atomicupdate.ts +14 -7
- package/ts/nosqldb/classes.collection.ts +82 -73
- package/ts/nosqldb/classes.db.ts +1 -1
- package/ts/nosqldb/classes.doc.ts +162 -33
- package/ts/nosqldb/classes.exactpersistence.ts +131 -93
- package/ts/nosqldb/classes.operationdeadline.ts +179 -0
- package/ts/nosqldb/classes.persistence.ts +36 -1
- package/ts/nosqldb/classes.session.ts +221 -39
- package/ts/objectstorage/classes.bucket.ts +350 -218
- package/ts/objectstorage/classes.directory.ts +18 -27
- package/ts/objectstorage/classes.smartbucket.ts +21 -11
- package/ts/objectstorage/classes.watcher.ts +0 -1
- package/ts/objectstorage/interfaces.ts +9 -0
- package/ts/objectstorage/internal.bucketrequests.ts +70 -0
- package/ts/objectstorage/internal.exactupload.capability.ts +105 -24
|
@@ -7,7 +7,17 @@ import type { SmartdataCollection } from './classes.collection.js';
|
|
|
7
7
|
import { getIdentityIndexName } from './classes.collectiontopology.js';
|
|
8
8
|
import { registerCollectionReconnectHandler } from './classes.collectionlifecycle.js';
|
|
9
9
|
import type { SmartdataSession } from './classes.session.js';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
acquireSmartdataSession,
|
|
12
|
+
settleSmartdataOperationError,
|
|
13
|
+
resolveSmartdataOperationDeadline,
|
|
14
|
+
} from './classes.session.js';
|
|
15
|
+
import {
|
|
16
|
+
type SmartdataOperationDeadline,
|
|
17
|
+
requireOperationSignal,
|
|
18
|
+
requireOperationTimeoutMS,
|
|
19
|
+
} from './classes.operationdeadline.js';
|
|
20
|
+
import { normalizeDeadlineError } from './classes.persistence.js';
|
|
11
21
|
import {
|
|
12
22
|
convertFilterForMongoDb,
|
|
13
23
|
type MongoFilter,
|
|
@@ -217,7 +227,22 @@ export interface IExactReadOptions extends IExactSessionOptions {
|
|
|
217
227
|
signal?: AbortSignal;
|
|
218
228
|
}
|
|
219
229
|
|
|
220
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Options of every exact call: `insert()`, `transition()`, `delete()`,
|
|
232
|
+
* `count()`, `distinct()` and `findStoredOne()`, and through
|
|
233
|
+
* `IFindStoredOptions` `findStored()`.
|
|
234
|
+
*/
|
|
235
|
+
export interface IExactBoundedOptions extends IExactReadOptions {
|
|
236
|
+
/**
|
|
237
|
+
* Client deadline (1..120000 ms), handed to the driver as its client-side
|
|
238
|
+
* operation timeout for every command of the call, including the wait for
|
|
239
|
+
* the lazy first-touch preparation. Inside a transaction with a deadline it
|
|
240
|
+
* is refused; the transaction's deadline bounds the call.
|
|
241
|
+
*/
|
|
242
|
+
timeoutMS?: number;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface IFindStoredOptions<TPersisted extends object> extends IExactBoundedOptions {
|
|
221
246
|
filter?: MongoFilter<TStoredDocument<TPersisted>>;
|
|
222
247
|
sort?: Partial<
|
|
223
248
|
Record<TExactDocumentPath<TStoredDocument<TPersisted>>, 1 | -1>
|
|
@@ -253,36 +278,6 @@ const bytesEqual = (leftArg: Uint8Array, rightArg: Uint8Array): boolean => {
|
|
|
253
278
|
return true;
|
|
254
279
|
};
|
|
255
280
|
|
|
256
|
-
const waitWithSignal = async <T>(
|
|
257
|
-
promiseArg: Promise<T>,
|
|
258
|
-
signalArg?: AbortSignal,
|
|
259
|
-
): Promise<T> => {
|
|
260
|
-
if (!signalArg) return promiseArg;
|
|
261
|
-
signalArg.throwIfAborted();
|
|
262
|
-
return new Promise<T>((resolve, reject) => {
|
|
263
|
-
let settled = false;
|
|
264
|
-
const beginSettlement = (): boolean => {
|
|
265
|
-
if (settled) return false;
|
|
266
|
-
settled = true;
|
|
267
|
-
signalArg.removeEventListener('abort', onAbort);
|
|
268
|
-
return true;
|
|
269
|
-
};
|
|
270
|
-
const onAbort = () => {
|
|
271
|
-
if (beginSettlement()) reject(signalArg.reason);
|
|
272
|
-
};
|
|
273
|
-
signalArg.addEventListener('abort', onAbort, { once: true });
|
|
274
|
-
if (signalArg.aborted) onAbort();
|
|
275
|
-
void promiseArg.then(
|
|
276
|
-
(valueArg) => {
|
|
277
|
-
if (beginSettlement()) resolve(valueArg);
|
|
278
|
-
},
|
|
279
|
-
(errorArg) => {
|
|
280
|
-
if (beginSettlement()) reject(errorArg);
|
|
281
|
-
},
|
|
282
|
-
);
|
|
283
|
-
});
|
|
284
|
-
};
|
|
285
|
-
|
|
286
281
|
const hasOwn = (documentArg: object, keyArg: PropertyKey): boolean =>
|
|
287
282
|
Object.prototype.hasOwnProperty.call(documentArg, keyArg);
|
|
288
283
|
|
|
@@ -1073,17 +1068,17 @@ export class SmartdataExactCollection<
|
|
|
1073
1068
|
}
|
|
1074
1069
|
|
|
1075
1070
|
private async beginOperation(
|
|
1076
|
-
|
|
1077
|
-
|
|
1071
|
+
sessionArg: SmartdataSession | undefined,
|
|
1072
|
+
boundsArg: { timeoutMS?: number; signal?: AbortSignal },
|
|
1078
1073
|
): Promise<{
|
|
1079
1074
|
session?: plugins.mongodb.ClientSession;
|
|
1075
|
+
deadline: SmartdataOperationDeadline;
|
|
1080
1076
|
release: () => void;
|
|
1081
1077
|
}> {
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
await
|
|
1085
|
-
|
|
1086
|
-
return { release: () => {} };
|
|
1078
|
+
if (!sessionArg) {
|
|
1079
|
+
const deadline = resolveSmartdataOperationDeadline(undefined, boundsArg);
|
|
1080
|
+
await deadline.join(this.initialize());
|
|
1081
|
+
return { deadline, release: () => {} };
|
|
1087
1082
|
}
|
|
1088
1083
|
if (
|
|
1089
1084
|
!this.collection.isInitializedForCurrentDatabase()
|
|
@@ -1095,35 +1090,66 @@ export class SmartdataExactCollection<
|
|
|
1095
1090
|
);
|
|
1096
1091
|
}
|
|
1097
1092
|
const lease = acquireSmartdataSession(
|
|
1098
|
-
|
|
1093
|
+
sessionArg,
|
|
1099
1094
|
this.collection.smartdataDb,
|
|
1100
1095
|
);
|
|
1101
|
-
|
|
1096
|
+
try {
|
|
1097
|
+
const deadline = resolveSmartdataOperationDeadline(sessionArg, boundsArg);
|
|
1098
|
+
deadline.throwIfAborted();
|
|
1099
|
+
return {
|
|
1100
|
+
session: lease.rawSession,
|
|
1101
|
+
deadline,
|
|
1102
|
+
release: lease.release,
|
|
1103
|
+
};
|
|
1104
|
+
} catch (errorArg) {
|
|
1102
1105
|
lease.release();
|
|
1103
|
-
|
|
1106
|
+
throw errorArg;
|
|
1104
1107
|
}
|
|
1105
|
-
return {
|
|
1106
|
-
session: lease.rawSession,
|
|
1107
|
-
release: lease.release,
|
|
1108
|
-
};
|
|
1109
1108
|
}
|
|
1110
1109
|
|
|
1110
|
+
/**
|
|
1111
|
+
* Runs one exact call on its session lease under its client deadline and
|
|
1112
|
+
* signal. A signal that is already aborted rejects with its reason before
|
|
1113
|
+
* anything is sent. An expired driver deadline surfaces as the `timeout`
|
|
1114
|
+
* persistence error; a write the deadline or signal interrupts in flight
|
|
1115
|
+
* is reported by the write itself as `ambiguous_write`, and inside a
|
|
1116
|
+
* transaction any interruption fails the attempt.
|
|
1117
|
+
*/
|
|
1111
1118
|
private async runOperation<TResult>(
|
|
1112
|
-
optionsArg:
|
|
1119
|
+
optionsArg: IExactBoundedOptions,
|
|
1113
1120
|
operationArg: (
|
|
1114
|
-
sessionArg
|
|
1121
|
+
sessionArg: plugins.mongodb.ClientSession | undefined,
|
|
1122
|
+
deadlineArg: SmartdataOperationDeadline,
|
|
1115
1123
|
) => Promise<TResult>,
|
|
1116
|
-
signalArg?: AbortSignal,
|
|
1117
1124
|
): Promise<TResult> {
|
|
1118
|
-
const
|
|
1125
|
+
const bounds = {
|
|
1126
|
+
timeoutMS: requireOperationTimeoutMS(optionsArg.timeoutMS, 'Exact operation timeoutMS'),
|
|
1127
|
+
signal: requireOperationSignal(optionsArg.signal, 'Exact operation signal'),
|
|
1128
|
+
};
|
|
1129
|
+
bounds.signal?.throwIfAborted();
|
|
1130
|
+
const operation = await this.beginOperation(optionsArg.session, bounds);
|
|
1119
1131
|
try {
|
|
1120
|
-
|
|
1121
|
-
|
|
1132
|
+
return await operationArg(operation.session, operation.deadline);
|
|
1133
|
+
} catch (errorArg) {
|
|
1134
|
+
throw settleSmartdataOperationError(optionsArg.session, operation.deadline, errorArg);
|
|
1122
1135
|
} finally {
|
|
1123
1136
|
operation.release();
|
|
1124
1137
|
}
|
|
1125
1138
|
}
|
|
1126
1139
|
|
|
1140
|
+
/**
|
|
1141
|
+
* The error of a non-transactional write that its client deadline or
|
|
1142
|
+
* signal interrupted after the write was handed to the driver: the command
|
|
1143
|
+
* may have reached the database, so its outcome is unknown and no
|
|
1144
|
+
* reconciliation read runs on the interrupted call's budget.
|
|
1145
|
+
*/
|
|
1146
|
+
private interruptedWriteError(labelArg: string, causeArg: unknown): Error {
|
|
1147
|
+
return this.ambiguousWriteError(
|
|
1148
|
+
`${labelArg} was interrupted by its client deadline or signal in flight; its outcome is unknown.`,
|
|
1149
|
+
normalizeDeadlineError(causeArg),
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1127
1153
|
private canonicalizeDocument<TDocument extends object>(
|
|
1128
1154
|
documentArg: TDocument,
|
|
1129
1155
|
labelArg: string,
|
|
@@ -1355,11 +1381,12 @@ export class SmartdataExactCollection<
|
|
|
1355
1381
|
|
|
1356
1382
|
private async findByReconcileIdentity(
|
|
1357
1383
|
identityArg: string,
|
|
1358
|
-
sessionArg
|
|
1384
|
+
sessionArg: plugins.mongodb.ClientSession | undefined,
|
|
1385
|
+
deadlineArg: SmartdataOperationDeadline,
|
|
1359
1386
|
): Promise<TStoredDocument<TPersisted> | null> {
|
|
1360
1387
|
const rawDocument = await this.collection.mongoDbCollection.findOne({
|
|
1361
1388
|
[this.reconcileBy]: identityArg,
|
|
1362
|
-
}, { session: sessionArg });
|
|
1389
|
+
}, { session: sessionArg, ...deadlineArg.commandOptions() });
|
|
1363
1390
|
return rawDocument
|
|
1364
1391
|
? this.canonicalizeStoredDocument(rawDocument)
|
|
1365
1392
|
: null;
|
|
@@ -1424,11 +1451,12 @@ export class SmartdataExactCollection<
|
|
|
1424
1451
|
currentArg: TStoredDocument<TPersisted>,
|
|
1425
1452
|
identityArg: string,
|
|
1426
1453
|
causeArg: unknown,
|
|
1427
|
-
sessionArg
|
|
1454
|
+
sessionArg: plugins.mongodb.ClientSession | undefined,
|
|
1455
|
+
deadlineArg: SmartdataOperationDeadline,
|
|
1428
1456
|
): Promise<TExactDeleteResult> {
|
|
1429
1457
|
let reconciled: TStoredDocument<TPersisted> | null;
|
|
1430
1458
|
try {
|
|
1431
|
-
reconciled = await this.findByReconcileIdentity(identityArg, sessionArg);
|
|
1459
|
+
reconciled = await this.findByReconcileIdentity(identityArg, sessionArg, deadlineArg);
|
|
1432
1460
|
} catch (errorArg) {
|
|
1433
1461
|
if (isTransientTransactionError(errorArg, sessionArg)) {
|
|
1434
1462
|
throw errorArg;
|
|
@@ -1449,9 +1477,9 @@ export class SmartdataExactCollection<
|
|
|
1449
1477
|
|
|
1450
1478
|
public async insert(
|
|
1451
1479
|
documentArg: TModel | TExactPersistedDocument<TPersisted>,
|
|
1452
|
-
optionsArg:
|
|
1480
|
+
optionsArg: IExactBoundedOptions = {},
|
|
1453
1481
|
): Promise<TExactInsertResult<TPersisted>> {
|
|
1454
|
-
return this.runOperation(optionsArg, async (sessionArg) => {
|
|
1482
|
+
return this.runOperation(optionsArg, async (sessionArg, deadlineArg) => {
|
|
1455
1483
|
const persistedDocument = await this.createPersistedDocument(documentArg);
|
|
1456
1484
|
const identity = this.validateReconcileIdentity(persistedDocument);
|
|
1457
1485
|
const expectedStoredDocument = this.canonicalizeStoredDocument({
|
|
@@ -1459,7 +1487,7 @@ export class SmartdataExactCollection<
|
|
|
1459
1487
|
...persistedDocument,
|
|
1460
1488
|
});
|
|
1461
1489
|
if (hasActiveTransaction(sessionArg)) {
|
|
1462
|
-
const existing = await this.findByReconcileIdentity(identity, sessionArg);
|
|
1490
|
+
const existing = await this.findByReconcileIdentity(identity, sessionArg, deadlineArg);
|
|
1463
1491
|
if (existing) {
|
|
1464
1492
|
return this.persistedDocumentsEqual(
|
|
1465
1493
|
this.getPersistedBody(existing),
|
|
@@ -1471,10 +1499,11 @@ export class SmartdataExactCollection<
|
|
|
1471
1499
|
}
|
|
1472
1500
|
|
|
1473
1501
|
let result: plugins.mongodb.InsertOneResult;
|
|
1502
|
+
const insertOptions = { session: sessionArg, ...deadlineArg.commandOptions() };
|
|
1474
1503
|
try {
|
|
1475
1504
|
result = await this.collection.mongoDbCollection.insertOne(
|
|
1476
1505
|
expectedStoredDocument,
|
|
1477
|
-
|
|
1506
|
+
insertOptions,
|
|
1478
1507
|
);
|
|
1479
1508
|
} catch (errorArg) {
|
|
1480
1509
|
if (isTransientTransactionError(errorArg, sessionArg)) {
|
|
@@ -1490,9 +1519,12 @@ export class SmartdataExactCollection<
|
|
|
1490
1519
|
}
|
|
1491
1520
|
throw errorArg;
|
|
1492
1521
|
}
|
|
1522
|
+
if (deadlineArg.interrupted(errorArg)) {
|
|
1523
|
+
throw this.interruptedWriteError('Exact insert', errorArg);
|
|
1524
|
+
}
|
|
1493
1525
|
let reconciled: TStoredDocument<TPersisted> | null;
|
|
1494
1526
|
try {
|
|
1495
|
-
reconciled = await this.findByReconcileIdentity(identity, sessionArg);
|
|
1527
|
+
reconciled = await this.findByReconcileIdentity(identity, sessionArg, deadlineArg);
|
|
1496
1528
|
} catch (reconciliationError) {
|
|
1497
1529
|
if (isTransientTransactionError(reconciliationError, sessionArg)) {
|
|
1498
1530
|
throw reconciliationError;
|
|
@@ -1544,7 +1576,7 @@ export class SmartdataExactCollection<
|
|
|
1544
1576
|
}
|
|
1545
1577
|
let reconciled: TStoredDocument<TPersisted> | null;
|
|
1546
1578
|
try {
|
|
1547
|
-
reconciled = await this.findByReconcileIdentity(identity, sessionArg);
|
|
1579
|
+
reconciled = await this.findByReconcileIdentity(identity, sessionArg, deadlineArg);
|
|
1548
1580
|
} catch (errorArg) {
|
|
1549
1581
|
if (isTransientTransactionError(errorArg, sessionArg)) {
|
|
1550
1582
|
throw errorArg;
|
|
@@ -1592,15 +1624,15 @@ export class SmartdataExactCollection<
|
|
|
1592
1624
|
|
|
1593
1625
|
public async findStoredOne(
|
|
1594
1626
|
filterArg: MongoFilter<TStoredDocument<TPersisted>>,
|
|
1595
|
-
optionsArg:
|
|
1627
|
+
optionsArg: IExactBoundedOptions = {},
|
|
1596
1628
|
): Promise<TStoredDocument<TPersisted> | null> {
|
|
1597
|
-
return this.runOperation(optionsArg, async (sessionArg) => {
|
|
1629
|
+
return this.runOperation(optionsArg, async (sessionArg, deadlineArg) => {
|
|
1598
1630
|
const cursor = this.collection.mongoDbCollection.find(
|
|
1599
1631
|
convertFilterForMongoDb(filterArg),
|
|
1600
1632
|
{
|
|
1601
1633
|
session: sessionArg,
|
|
1602
|
-
signal: optionsArg.signal,
|
|
1603
1634
|
singleBatch: true,
|
|
1635
|
+
...deadlineArg.cursorOptions(),
|
|
1604
1636
|
},
|
|
1605
1637
|
).limit(1);
|
|
1606
1638
|
const nativeCursorClose = cursor.close.bind(cursor);
|
|
@@ -1625,17 +1657,16 @@ export class SmartdataExactCollection<
|
|
|
1625
1657
|
}
|
|
1626
1658
|
await cursor.close();
|
|
1627
1659
|
return storedDocument;
|
|
1628
|
-
}
|
|
1660
|
+
});
|
|
1629
1661
|
}
|
|
1630
1662
|
|
|
1631
1663
|
public async findStored(
|
|
1632
1664
|
optionsArg: IFindStoredOptions<TPersisted> = {},
|
|
1633
1665
|
): Promise<Array<TStoredDocument<TPersisted>>> {
|
|
1634
|
-
|
|
1635
|
-
return this.runOperation(optionsArg, async (sessionArg) => {
|
|
1666
|
+
return this.runOperation(optionsArg, async (sessionArg, deadlineArg) => {
|
|
1636
1667
|
let cursor = this.collection.mongoDbCollection.find(
|
|
1637
1668
|
convertFilterForMongoDb(optionsArg.filter || {}),
|
|
1638
|
-
{ session: sessionArg,
|
|
1669
|
+
{ session: sessionArg, ...deadlineArg.cursorOptions() },
|
|
1639
1670
|
);
|
|
1640
1671
|
// The driver abort listener does not await close, so every close must join
|
|
1641
1672
|
// the same native cleanup before runOperation releases its session lease.
|
|
@@ -1670,17 +1701,17 @@ export class SmartdataExactCollection<
|
|
|
1670
1701
|
}
|
|
1671
1702
|
await cursor.close();
|
|
1672
1703
|
return storedDocuments;
|
|
1673
|
-
}
|
|
1704
|
+
});
|
|
1674
1705
|
}
|
|
1675
1706
|
|
|
1676
1707
|
public async count(
|
|
1677
1708
|
filterArg: MongoFilter<TStoredDocument<TPersisted>> = {},
|
|
1678
|
-
optionsArg:
|
|
1709
|
+
optionsArg: IExactBoundedOptions = {},
|
|
1679
1710
|
): Promise<number> {
|
|
1680
|
-
return this.runOperation(optionsArg, async (sessionArg) =>
|
|
1711
|
+
return this.runOperation(optionsArg, async (sessionArg, deadlineArg) =>
|
|
1681
1712
|
this.collection.mongoDbCollection.countDocuments(
|
|
1682
1713
|
convertFilterForMongoDb(filterArg),
|
|
1683
|
-
{ session: sessionArg },
|
|
1714
|
+
{ session: sessionArg, ...deadlineArg.commandOptions() },
|
|
1684
1715
|
)
|
|
1685
1716
|
);
|
|
1686
1717
|
}
|
|
@@ -1690,15 +1721,15 @@ export class SmartdataExactCollection<
|
|
|
1690
1721
|
>(
|
|
1691
1722
|
fieldArg: TField,
|
|
1692
1723
|
filterArg: MongoFilter<TStoredDocument<TPersisted>> = {},
|
|
1693
|
-
optionsArg:
|
|
1724
|
+
optionsArg: IExactBoundedOptions = {},
|
|
1694
1725
|
): Promise<Array<TExactDistinctValue<
|
|
1695
1726
|
TExactDocumentPathValue<TStoredDocument<TPersisted>, TField>
|
|
1696
1727
|
>>> {
|
|
1697
|
-
return this.runOperation(optionsArg, async (sessionArg) =>
|
|
1728
|
+
return this.runOperation(optionsArg, async (sessionArg, deadlineArg) =>
|
|
1698
1729
|
(await this.collection.mongoDbCollection.distinct(
|
|
1699
1730
|
fieldArg as string,
|
|
1700
1731
|
convertFilterForMongoDb(filterArg),
|
|
1701
|
-
{ session: sessionArg },
|
|
1732
|
+
{ session: sessionArg, ...deadlineArg.commandOptions() },
|
|
1702
1733
|
)) as Array<TExactDistinctValue<
|
|
1703
1734
|
TExactDocumentPathValue<TStoredDocument<TPersisted>, TField>
|
|
1704
1735
|
>>
|
|
@@ -1710,9 +1741,9 @@ export class SmartdataExactCollection<
|
|
|
1710
1741
|
current: TStoredDocument<TPersisted>;
|
|
1711
1742
|
change: (modelArg: TModel) => void;
|
|
1712
1743
|
},
|
|
1713
|
-
sessionOptionsArg:
|
|
1744
|
+
sessionOptionsArg: IExactBoundedOptions = {},
|
|
1714
1745
|
): Promise<TExactTransitionResult<TPersisted>> {
|
|
1715
|
-
return this.runOperation(sessionOptionsArg, async (sessionArg) => {
|
|
1746
|
+
return this.runOperation(sessionOptionsArg, async (sessionArg, deadlineArg) => {
|
|
1716
1747
|
const current = this.canonicalizeStoredDocument(optionsArg.current);
|
|
1717
1748
|
const currentBody = this.getPersistedBody(current);
|
|
1718
1749
|
const currentIdentity = this.validateReconcileIdentity(currentBody);
|
|
@@ -1756,16 +1787,18 @@ export class SmartdataExactCollection<
|
|
|
1756
1787
|
? { _id: current._id, _smartdataRevision: current._smartdataRevision }
|
|
1757
1788
|
: { _id: current._id, _smartdataRevision: { $exists: false } };
|
|
1758
1789
|
|
|
1790
|
+
const replaceOptions = {
|
|
1791
|
+
returnDocument: 'after' as const,
|
|
1792
|
+
includeResultMetadata: false as const,
|
|
1793
|
+
upsert: false,
|
|
1794
|
+
session: sessionArg,
|
|
1795
|
+
...deadlineArg.commandOptions(),
|
|
1796
|
+
};
|
|
1759
1797
|
try {
|
|
1760
1798
|
const postimage = await this.collection.mongoDbCollection.findOneAndReplace(
|
|
1761
1799
|
selector,
|
|
1762
1800
|
expectedStoredDocument,
|
|
1763
|
-
|
|
1764
|
-
returnDocument: 'after',
|
|
1765
|
-
includeResultMetadata: false,
|
|
1766
|
-
upsert: false,
|
|
1767
|
-
session: sessionArg,
|
|
1768
|
-
},
|
|
1801
|
+
replaceOptions,
|
|
1769
1802
|
);
|
|
1770
1803
|
if (!postimage) {
|
|
1771
1804
|
return { status: 'concurrent_change' };
|
|
@@ -1792,10 +1825,13 @@ export class SmartdataExactCollection<
|
|
|
1792
1825
|
if (hasActiveTransaction(sessionArg)) {
|
|
1793
1826
|
throw errorArg;
|
|
1794
1827
|
}
|
|
1828
|
+
if (deadlineArg.interrupted(errorArg)) {
|
|
1829
|
+
throw this.interruptedWriteError('Exact transition', errorArg);
|
|
1830
|
+
}
|
|
1795
1831
|
try {
|
|
1796
1832
|
const reconciledRaw = await this.collection.mongoDbCollection.findOne({
|
|
1797
1833
|
_id: current._id,
|
|
1798
|
-
}, { session: sessionArg });
|
|
1834
|
+
}, { session: sessionArg, ...deadlineArg.commandOptions() });
|
|
1799
1835
|
if (reconciledRaw) {
|
|
1800
1836
|
const reconciled = this.canonicalizeStoredDocument(reconciledRaw);
|
|
1801
1837
|
if (
|
|
@@ -1826,10 +1862,10 @@ export class SmartdataExactCollection<
|
|
|
1826
1862
|
optionsArg: {
|
|
1827
1863
|
current: TStoredDocument<TPersisted>;
|
|
1828
1864
|
},
|
|
1829
|
-
sessionOptionsArg:
|
|
1865
|
+
sessionOptionsArg: IExactBoundedOptions = {},
|
|
1830
1866
|
): Promise<TExactDeleteResult> {
|
|
1831
1867
|
const { current, identity } = this.canonicalizeDeleteCurrent(optionsArg);
|
|
1832
|
-
return this.runOperation(sessionOptionsArg, async (sessionArg) => {
|
|
1868
|
+
return this.runOperation(sessionOptionsArg, async (sessionArg, deadlineArg) => {
|
|
1833
1869
|
const selector = current._smartdataRevision !== undefined
|
|
1834
1870
|
? {
|
|
1835
1871
|
_id: current._id,
|
|
@@ -1842,10 +1878,9 @@ export class SmartdataExactCollection<
|
|
|
1842
1878
|
_smartdataRevision: { $exists: false },
|
|
1843
1879
|
};
|
|
1844
1880
|
let result: plugins.mongodb.DeleteResult;
|
|
1881
|
+
const deleteOptions = { session: sessionArg, ...deadlineArg.commandOptions() };
|
|
1845
1882
|
try {
|
|
1846
|
-
result = await this.collection.mongoDbCollection.deleteOne(selector,
|
|
1847
|
-
session: sessionArg,
|
|
1848
|
-
});
|
|
1883
|
+
result = await this.collection.mongoDbCollection.deleteOne(selector, deleteOptions);
|
|
1849
1884
|
} catch (errorArg) {
|
|
1850
1885
|
if (isTransientTransactionError(errorArg, sessionArg)) {
|
|
1851
1886
|
throw errorArg;
|
|
@@ -1853,7 +1888,10 @@ export class SmartdataExactCollection<
|
|
|
1853
1888
|
if (hasActiveTransaction(sessionArg)) {
|
|
1854
1889
|
throw errorArg;
|
|
1855
1890
|
}
|
|
1856
|
-
|
|
1891
|
+
if (deadlineArg.interrupted(errorArg)) {
|
|
1892
|
+
throw this.interruptedWriteError('Exact delete', errorArg);
|
|
1893
|
+
}
|
|
1894
|
+
return this.reconcileDeleteOutcome(current, identity, errorArg, sessionArg, deadlineArg);
|
|
1857
1895
|
}
|
|
1858
1896
|
if (
|
|
1859
1897
|
result.acknowledged === true &&
|
|
@@ -1874,11 +1912,11 @@ export class SmartdataExactCollection<
|
|
|
1874
1912
|
result,
|
|
1875
1913
|
);
|
|
1876
1914
|
}
|
|
1877
|
-
return this.reconcileDeleteOutcome(current, identity, result, sessionArg);
|
|
1915
|
+
return this.reconcileDeleteOutcome(current, identity, result, sessionArg, deadlineArg);
|
|
1878
1916
|
}
|
|
1879
1917
|
|
|
1880
1918
|
try {
|
|
1881
|
-
await this.findByReconcileIdentity(identity, sessionArg);
|
|
1919
|
+
await this.findByReconcileIdentity(identity, sessionArg, deadlineArg);
|
|
1882
1920
|
} catch (errorArg) {
|
|
1883
1921
|
if (isTransientTransactionError(errorArg, sessionArg)) {
|
|
1884
1922
|
throw errorArg;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
SmartdataPersistenceError,
|
|
4
|
+
createSmartdataTimeoutError,
|
|
5
|
+
isMongoDeadlineError,
|
|
6
|
+
} from './classes.persistence.js';
|
|
7
|
+
|
|
8
|
+
const deadlineExceededMessage = 'SmartData operation exceeded its client deadline.';
|
|
9
|
+
|
|
10
|
+
/** Validates a caller's client deadline: a positive safe integer of at most 120000 ms. */
|
|
11
|
+
export const requireOperationTimeoutMS = (
|
|
12
|
+
valueArg: unknown,
|
|
13
|
+
labelArg: string,
|
|
14
|
+
): number | undefined => {
|
|
15
|
+
if (valueArg === undefined) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (
|
|
19
|
+
typeof valueArg !== 'number'
|
|
20
|
+
|| !Number.isSafeInteger(valueArg)
|
|
21
|
+
|| valueArg < 1
|
|
22
|
+
|| valueArg > 120_000
|
|
23
|
+
) {
|
|
24
|
+
throw new SmartdataPersistenceError(
|
|
25
|
+
'invalid_argument',
|
|
26
|
+
`${labelArg} must be a positive safe integer no greater than 120000.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return valueArg;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Validates a caller's cancellation signal: a genuine `AbortSignal`. */
|
|
33
|
+
export const requireOperationSignal = (
|
|
34
|
+
valueArg: unknown,
|
|
35
|
+
labelArg: string,
|
|
36
|
+
): AbortSignal | undefined => {
|
|
37
|
+
if (valueArg === undefined) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
if (plugins.nodeUtil.types.isProxy(valueArg) || !(valueArg instanceof AbortSignal)) {
|
|
41
|
+
throw new SmartdataPersistenceError('invalid_argument', `${labelArg} must be an AbortSignal.`);
|
|
42
|
+
}
|
|
43
|
+
return valueArg;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @internal The client-side deadline and cancellation of one model API call.
|
|
48
|
+
*
|
|
49
|
+
* Every database command of the call receives the time that is left as the
|
|
50
|
+
* driver's `timeoutMS`, so server selection, connection checkout, the round
|
|
51
|
+
* trip and the server-side `maxTimeMS` the driver derives all run on one
|
|
52
|
+
* clock, and the driver closes a connection whose reply did not arrive in
|
|
53
|
+
* time. An unbounded deadline hands the driver nothing, which leaves an
|
|
54
|
+
* enclosing transaction deadline or the connection's own settings in charge.
|
|
55
|
+
*
|
|
56
|
+
* The call's `signal` — its own, an enclosing transaction's, or both — goes
|
|
57
|
+
* to the driver with every command, which stops waiting for server
|
|
58
|
+
* selection or a pooled connection and closes a connection whose command is
|
|
59
|
+
* in flight, rejecting with the signal's reason.
|
|
60
|
+
*/
|
|
61
|
+
export class SmartdataOperationDeadline {
|
|
62
|
+
public static readonly unbounded = new SmartdataOperationDeadline(undefined);
|
|
63
|
+
|
|
64
|
+
private readonly expiresAt: number | undefined;
|
|
65
|
+
|
|
66
|
+
constructor(
|
|
67
|
+
timeoutMSArg: number | undefined,
|
|
68
|
+
public readonly signal?: AbortSignal,
|
|
69
|
+
) {
|
|
70
|
+
this.expiresAt = timeoutMSArg === undefined
|
|
71
|
+
? undefined
|
|
72
|
+
: performance.now() + timeoutMSArg;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public get bounded(): boolean {
|
|
76
|
+
return this.expiresAt !== undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Throws the signal's reason once the call was aborted. */
|
|
80
|
+
public throwIfAborted(): void {
|
|
81
|
+
this.signal?.throwIfAborted();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Driver options for one command. Throws the abort reason once the call was
|
|
86
|
+
* aborted and `timeout` once the deadline has passed, so a command these
|
|
87
|
+
* options were built for was never handed to the driver.
|
|
88
|
+
*/
|
|
89
|
+
public commandOptions(): { timeoutMS?: number } & plugins.mongodb.Abortable {
|
|
90
|
+
this.throwIfAborted();
|
|
91
|
+
return {
|
|
92
|
+
...(this.expiresAt === undefined ? {} : { timeoutMS: this.remainingMS() }),
|
|
93
|
+
...(this.signal === undefined ? {} : { signal: this.signal }),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Driver options for a cursor the deadline bounds from its first command to
|
|
99
|
+
* its last reply. When a `getMore` times out, the driver's own cleanup runs
|
|
100
|
+
* the cursor's `killCursors` on a fresh deadline of the full `timeoutMS`
|
|
101
|
+
* before it rethrows, and no driver option shortens that, so a stalled
|
|
102
|
+
* database settles a multi-batch read within about twice the deadline.
|
|
103
|
+
*/
|
|
104
|
+
public cursorOptions(): {
|
|
105
|
+
timeoutMS?: number;
|
|
106
|
+
timeoutMode?: 'cursorLifetime';
|
|
107
|
+
} & plugins.mongodb.Abortable {
|
|
108
|
+
this.throwIfAborted();
|
|
109
|
+
return {
|
|
110
|
+
...(this.expiresAt === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { timeoutMS: this.remainingMS(), timeoutMode: 'cursorLifetime' as const }),
|
|
113
|
+
...(this.signal === undefined ? {} : { signal: this.signal }),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Whether `errorArg` is this call's interruption: its signal's abort reason
|
|
119
|
+
* or the driver's deadline expiry.
|
|
120
|
+
*/
|
|
121
|
+
public interrupted(errorArg: unknown): boolean {
|
|
122
|
+
return (this.signal?.aborted === true && errorArg === this.signal.reason)
|
|
123
|
+
|| isMongoDeadlineError(errorArg);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Waits within the deadline for work another owner drives, such as the
|
|
128
|
+
* collection initialization every model call shares. Expiry or an abort
|
|
129
|
+
* ends only this call's wait: the owner keeps the work, its outcome and its
|
|
130
|
+
* cleanup, and the next caller joins the same work again.
|
|
131
|
+
*/
|
|
132
|
+
public async join(workArg: Promise<void>): Promise<void> {
|
|
133
|
+
this.throwIfAborted();
|
|
134
|
+
if (this.expiresAt === undefined && this.signal === undefined) {
|
|
135
|
+
await workArg;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const remainingMS = this.expiresAt === undefined ? undefined : this.remainingMS();
|
|
139
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
140
|
+
let onAbort: (() => void) | undefined;
|
|
141
|
+
let expired = false;
|
|
142
|
+
const interruption = new Promise<void>((resolveArg) => {
|
|
143
|
+
if (remainingMS !== undefined) {
|
|
144
|
+
timer = setTimeout(() => {
|
|
145
|
+
expired = true;
|
|
146
|
+
resolveArg();
|
|
147
|
+
}, remainingMS);
|
|
148
|
+
}
|
|
149
|
+
if (this.signal !== undefined) {
|
|
150
|
+
onAbort = () => resolveArg();
|
|
151
|
+
this.signal.addEventListener('abort', onAbort, { once: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
// The owner observes the work's failure itself; this wait must not turn a
|
|
155
|
+
// failure it stopped waiting for into an unhandled rejection.
|
|
156
|
+
const settled = workArg.then(() => undefined, () => undefined);
|
|
157
|
+
try {
|
|
158
|
+
await Promise.race([settled, interruption]);
|
|
159
|
+
} finally {
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
if (onAbort !== undefined) {
|
|
162
|
+
this.signal?.removeEventListener('abort', onAbort);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
this.throwIfAborted();
|
|
166
|
+
if (expired) {
|
|
167
|
+
throw createSmartdataTimeoutError(deadlineExceededMessage);
|
|
168
|
+
}
|
|
169
|
+
await workArg;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private remainingMS(): number {
|
|
173
|
+
const remainingMS = this.expiresAt! - performance.now();
|
|
174
|
+
if (remainingMS <= 0) {
|
|
175
|
+
throw createSmartdataTimeoutError(deadlineExceededMessage);
|
|
176
|
+
}
|
|
177
|
+
return Math.ceil(remainingMS);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -6,7 +6,8 @@ export type TSmartdataPersistenceErrorCode =
|
|
|
6
6
|
| 'invalid_document'
|
|
7
7
|
| 'unique_conflict'
|
|
8
8
|
| 'ambiguous_write'
|
|
9
|
-
| 'unsupported_operation'
|
|
9
|
+
| 'unsupported_operation'
|
|
10
|
+
| 'timeout';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Stable errors for ordinary SmartData model configuration and persistence.
|
|
@@ -27,6 +28,40 @@ export class SmartdataPersistenceError extends Error {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
/**
|
|
32
|
+
* The client-side deadline of a model call or transaction expired. The
|
|
33
|
+
* driver's own `MongoOperationTimeoutError` stays available as `cause` when
|
|
34
|
+
* the driver enforced it. A write or commit that expired has an unknown
|
|
35
|
+
* outcome: it may have been applied before the reply was lost.
|
|
36
|
+
*/
|
|
37
|
+
export const createSmartdataTimeoutError = (
|
|
38
|
+
messageArg: string,
|
|
39
|
+
causeArg?: unknown,
|
|
40
|
+
): SmartdataPersistenceError =>
|
|
41
|
+
new SmartdataPersistenceError(
|
|
42
|
+
'timeout',
|
|
43
|
+
messageArg,
|
|
44
|
+
causeArg === undefined ? undefined : { cause: causeArg },
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The driver enforced a client deadline. A bulk write reports a driver error
|
|
49
|
+
* wrapped in `MongoBulkWriteError`, whose `errorResponse` is the original
|
|
50
|
+
* error and whose result still states what earlier batches wrote.
|
|
51
|
+
*/
|
|
52
|
+
export const isMongoDeadlineError = (errorArg: unknown): boolean =>
|
|
53
|
+
errorArg instanceof plugins.mongodb.MongoOperationTimeoutError
|
|
54
|
+
|| (
|
|
55
|
+
errorArg instanceof plugins.mongodb.MongoBulkWriteError
|
|
56
|
+
&& errorArg.errorResponse instanceof plugins.mongodb.MongoOperationTimeoutError
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
/** Maps an expired driver deadline to `timeout`; every other error passes through unchanged. */
|
|
60
|
+
export const normalizeDeadlineError = (errorArg: unknown): unknown =>
|
|
61
|
+
isMongoDeadlineError(errorArg)
|
|
62
|
+
? createSmartdataTimeoutError('SmartData operation exceeded its client deadline.', errorArg)
|
|
63
|
+
: errorArg;
|
|
64
|
+
|
|
30
65
|
export const isMongoTransactionRetryError = (errorArg: unknown): boolean =>
|
|
31
66
|
errorArg instanceof plugins.mongodb.MongoServerError &&
|
|
32
67
|
(
|