@lossless.org/client 1.11.0 → 1.12.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.db.d.ts +19 -2
- package/dist_ts/nosqldb/classes.db.js +45 -12
- package/dist_ts/nosqldb/classes.doc.d.ts +12 -2
- package/dist_ts/nosqldb/classes.doc.js +1 -1
- package/dist_ts/nosqldb/classes.operationbudget.d.ts +2 -0
- package/dist_ts/nosqldb/classes.operationbudget.js +7 -1
- package/dist_ts/nosqldb/classes.session.d.ts +11 -1
- package/dist_ts/nosqldb/classes.session.js +88 -14
- package/package.json +2 -2
- package/readme.md +10 -7
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.db.ts +63 -11
- package/ts/nosqldb/classes.doc.ts +19 -2
- package/ts/nosqldb/classes.operationbudget.ts +6 -0
- package/ts/nosqldb/classes.session.ts +111 -12
|
@@ -561,7 +561,24 @@ export type TSmartdataOptionalNullPredicate<TValue> =
|
|
|
561
561
|
type InValues<T> = ReadonlyArray<ElementOf<T> | TSmartdataOptionalNullPredicate<T>>;
|
|
562
562
|
|
|
563
563
|
// Type that allows MongoDB operators on leaf values while maintaining nested type safety
|
|
564
|
-
export type MongoFilterCondition<T> =
|
|
564
|
+
export type MongoFilterCondition<T> =
|
|
565
|
+
| T
|
|
566
|
+
| TSmartdataOptionalNullPredicate<T>
|
|
567
|
+
| TSmartdataFilterOperators<T>;
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* The operand of `$elemMatch`: a query on each element, the document form, or
|
|
571
|
+
* an operator document applied to each element as a whole, the value form
|
|
572
|
+
* (`{ $gte: 80, $lt: 90 }`). Elements that are all documents take only the
|
|
573
|
+
* document form; any other element type takes either, so element unions that
|
|
574
|
+
* include documents, nullable documents and `unknown` keep the document form.
|
|
575
|
+
*/
|
|
576
|
+
type TSmartdataElemMatchOperand<TElement> = [TElement] extends [object]
|
|
577
|
+
? MongoFilter<TElement>
|
|
578
|
+
: MongoFilter<TElement> | TSmartdataFilterOperators<TElement>;
|
|
579
|
+
|
|
580
|
+
/** The operator document of a field condition. */
|
|
581
|
+
type TSmartdataFilterOperators<T> = {
|
|
565
582
|
$eq?: T | TSmartdataOptionalNullPredicate<T>;
|
|
566
583
|
$ne?: T | TSmartdataOptionalNullPredicate<T>;
|
|
567
584
|
$gt?: T;
|
|
@@ -575,7 +592,7 @@ export type MongoFilterCondition<T> = T | TSmartdataOptionalNullPredicate<T> | {
|
|
|
575
592
|
$regex?: string | RegExp;
|
|
576
593
|
$options?: string;
|
|
577
594
|
$all?: T extends ReadonlyArray<infer U> ? ReadonlyArray<U> : never;
|
|
578
|
-
$elemMatch?: T extends ReadonlyArray<infer U> ?
|
|
595
|
+
$elemMatch?: T extends ReadonlyArray<infer U> ? TSmartdataElemMatchOperand<U> : never;
|
|
579
596
|
$size?: T extends ReadonlyArray<any> ? number : never;
|
|
580
597
|
$not?: MongoFilterCondition<T>;
|
|
581
598
|
};
|
|
@@ -46,6 +46,12 @@ export class SmartdataOperationBudget {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Whether the deadline itself has passed; a caller's signal does not count. */
|
|
50
|
+
public expired(): boolean {
|
|
51
|
+
if (performance.now() >= this.expiresAt) this.controller.abort();
|
|
52
|
+
return this.controller.signal.aborted;
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
public remainingMs(): number {
|
|
50
56
|
this.check();
|
|
51
57
|
return this.cleanupOptions().timeoutMS;
|
|
@@ -31,12 +31,36 @@ interface ISmartdataSessionBinding {
|
|
|
31
31
|
transactionSignal?: AbortSignal;
|
|
32
32
|
transactionAttempt?: symbol;
|
|
33
33
|
transactionFailure?: { error: unknown };
|
|
34
|
+
/** Set once the current attempt's callback returned and the driver moves on to commit. */
|
|
35
|
+
transactionCommitReached: boolean;
|
|
36
|
+
/** Set when the session ended while a transaction ran, stating how far the transaction had got. */
|
|
37
|
+
transactionEnded?: TSmartdataTransactionEnd;
|
|
34
38
|
operationSettled?: Promise<void>;
|
|
35
39
|
resolveOperationSettled?: () => void;
|
|
36
40
|
invalidated: boolean;
|
|
37
41
|
closing?: Promise<void>;
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
/** How far a transaction had got when its session ended outside the callback's control. */
|
|
45
|
+
type TSmartdataTransactionEnd = 'before_commit' | 'during_commit';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The session of a transaction ended while it ran, for example through the
|
|
49
|
+
* database's `close()`. The driver then returns the callback's result without
|
|
50
|
+
* committing, so the call must reject instead.
|
|
51
|
+
*/
|
|
52
|
+
const createTransactionSessionEndedError = (
|
|
53
|
+
endArg: TSmartdataTransactionEnd,
|
|
54
|
+
causeArg?: unknown,
|
|
55
|
+
): SmartdataPersistenceError =>
|
|
56
|
+
new SmartdataPersistenceError(
|
|
57
|
+
'unsupported_operation',
|
|
58
|
+
endArg === 'during_commit'
|
|
59
|
+
? 'SmartData transaction session ended during commit; the commit outcome is unknown.'
|
|
60
|
+
: 'SmartData transaction session ended before commit; nothing was committed.',
|
|
61
|
+
causeArg === undefined ? undefined : { cause: causeArg },
|
|
62
|
+
);
|
|
63
|
+
|
|
40
64
|
const bindings = new WeakMap<SmartdataSession, ISmartdataSessionBinding>();
|
|
41
65
|
const bindingsByOwner = new WeakMap<SmartdataDb, Set<ISmartdataSessionBinding>>();
|
|
42
66
|
const smartdataSessionInvalidationAggregateBrand = Symbol(
|
|
@@ -162,6 +186,12 @@ export class SmartdataSession {
|
|
|
162
186
|
* Any operation on the session that its deadline or a signal interrupts
|
|
163
187
|
* fails the attempt: the call rolls it back and rejects with that
|
|
164
188
|
* operation's error even when the callback caught it.
|
|
189
|
+
*
|
|
190
|
+
* When the session ends while the call runs — the database's `close()`
|
|
191
|
+
* ends every session — the call never reports a commit. It rejects with
|
|
192
|
+
* `unsupported_operation`: before the callback returned nothing was
|
|
193
|
+
* committed, afterwards the commit outcome is unknown. The callback's
|
|
194
|
+
* error, if it threw, is the `cause`.
|
|
165
195
|
*/
|
|
166
196
|
public async withTransaction<TResult>(
|
|
167
197
|
callbackArg: (sessionArg: SmartdataSession) => Promise<TResult>,
|
|
@@ -185,19 +215,24 @@ export class SmartdataSession {
|
|
|
185
215
|
binding.transactionActive = true;
|
|
186
216
|
binding.transactionTimeoutMS = timeoutMS;
|
|
187
217
|
binding.transactionSignal = signal;
|
|
218
|
+
binding.transactionEnded = undefined;
|
|
188
219
|
let callbackError: unknown;
|
|
189
|
-
|
|
220
|
+
/** The signal or deadline interruption that failed the last attempt. */
|
|
221
|
+
let attemptInterruption: { error: unknown } | undefined;
|
|
222
|
+
let sessionEndedError: SmartdataPersistenceError | undefined;
|
|
190
223
|
const runAttempt = async () => {
|
|
191
224
|
const attempt = Symbol('smartdataTransactionAttempt');
|
|
192
225
|
binding.transactionAttempt = attempt;
|
|
193
226
|
binding.transactionCallbackOpen = true;
|
|
194
227
|
binding.transactionUsesOrdinaryWrite = false;
|
|
195
228
|
binding.transactionFailure = undefined;
|
|
229
|
+
binding.transactionCommitReached = false;
|
|
196
230
|
callbackError = undefined;
|
|
197
|
-
|
|
231
|
+
attemptInterruption = undefined;
|
|
198
232
|
try {
|
|
199
233
|
let result: TResult;
|
|
200
234
|
let callbackReturned = false;
|
|
235
|
+
let callbackThrown: { error: unknown } | undefined;
|
|
201
236
|
try {
|
|
202
237
|
// A retry the driver starts after an abort ends before the callback.
|
|
203
238
|
signal?.throwIfAborted();
|
|
@@ -206,6 +241,9 @@ export class SmartdataSession {
|
|
|
206
241
|
async () => callbackArg(this),
|
|
207
242
|
);
|
|
208
243
|
callbackReturned = true;
|
|
244
|
+
} catch (errorArg) {
|
|
245
|
+
callbackThrown = { error: errorArg };
|
|
246
|
+
throw errorArg;
|
|
209
247
|
} finally {
|
|
210
248
|
const activeOperation = binding.operationActive
|
|
211
249
|
? binding.operationSettled
|
|
@@ -217,7 +255,17 @@ export class SmartdataSession {
|
|
|
217
255
|
if (activeOperation) {
|
|
218
256
|
await activeOperation;
|
|
219
257
|
}
|
|
258
|
+
attemptInterruption = binding.transactionFailure;
|
|
220
259
|
assertTransactionSucceeded(binding);
|
|
260
|
+
if (binding.transactionEnded) {
|
|
261
|
+
// The driver would commit or return the result on a session that is
|
|
262
|
+
// being ended; the attempt fails instead, keeping the callback's error.
|
|
263
|
+
sessionEndedError = createTransactionSessionEndedError(
|
|
264
|
+
binding.transactionEnded,
|
|
265
|
+
callbackThrown?.error,
|
|
266
|
+
);
|
|
267
|
+
throw sessionEndedError;
|
|
268
|
+
}
|
|
221
269
|
if (callbackReturned && activeOperation) {
|
|
222
270
|
throw new SmartdataPersistenceError(
|
|
223
271
|
'unsupported_operation',
|
|
@@ -227,26 +275,49 @@ export class SmartdataSession {
|
|
|
227
275
|
}
|
|
228
276
|
// An abort that arrived while the callback ran rolls the attempt back.
|
|
229
277
|
signal?.throwIfAborted();
|
|
230
|
-
// The driver commits next; a deadline that expires
|
|
231
|
-
// leaves the commit outcome unknown.
|
|
232
|
-
|
|
278
|
+
// The driver commits next; a deadline that expires or a session that
|
|
279
|
+
// ends from here on leaves the commit outcome unknown.
|
|
280
|
+
binding.transactionCommitReached = true;
|
|
233
281
|
return result;
|
|
234
282
|
} catch (errorArg) {
|
|
235
283
|
callbackError = errorArg;
|
|
284
|
+
if (signal?.aborted && errorArg === signal.reason) {
|
|
285
|
+
attemptInterruption ??= { error: errorArg };
|
|
286
|
+
}
|
|
236
287
|
throw errorArg;
|
|
237
288
|
}
|
|
238
289
|
};
|
|
290
|
+
let result: TResult | undefined;
|
|
291
|
+
let transactionEnded: TSmartdataTransactionEnd | undefined;
|
|
239
292
|
try {
|
|
240
|
-
|
|
293
|
+
result = timeoutMS === undefined
|
|
241
294
|
? await binding.rawSession.withTransaction(runAttempt)
|
|
242
295
|
: await binding.rawSession.withTransaction(runAttempt, { timeoutMS });
|
|
296
|
+
transactionEnded = binding.transactionEnded;
|
|
243
297
|
} catch (errorArg) {
|
|
298
|
+
// The driver's closing abort replaces the attempt's error when it finds
|
|
299
|
+
// the transaction already aborted, as a session that close() ended leaves
|
|
300
|
+
// it; the interruption that failed the attempt keeps its rejection.
|
|
301
|
+
if (attemptInterruption) {
|
|
302
|
+
throw normalizeDeadlineError(attemptInterruption.error);
|
|
303
|
+
}
|
|
304
|
+
if (sessionEndedError) {
|
|
305
|
+
throw sessionEndedError;
|
|
306
|
+
}
|
|
307
|
+
if (
|
|
308
|
+
binding.transactionEnded
|
|
309
|
+
&& errorArg !== signal?.reason
|
|
310
|
+
&& !isMongoDeadlineError(errorArg)
|
|
311
|
+
&& !(errorArg instanceof SmartdataPersistenceError && errorArg.code === 'timeout')
|
|
312
|
+
) {
|
|
313
|
+
throw createTransactionSessionEndedError(binding.transactionEnded, errorArg);
|
|
314
|
+
}
|
|
244
315
|
if (errorArg === callbackError) {
|
|
245
316
|
throw normalizeDeadlineError(errorArg);
|
|
246
317
|
}
|
|
247
318
|
if (isMongoDeadlineError(errorArg)) {
|
|
248
319
|
throw createSmartdataTimeoutError(
|
|
249
|
-
|
|
320
|
+
binding.transactionCommitReached
|
|
250
321
|
? 'SmartData transaction exceeded its client deadline during commit; the commit outcome is unknown.'
|
|
251
322
|
: 'SmartData transaction exceeded its client deadline before commit; nothing was committed.',
|
|
252
323
|
errorArg,
|
|
@@ -265,7 +336,13 @@ export class SmartdataSession {
|
|
|
265
336
|
binding.transactionSignal = undefined;
|
|
266
337
|
binding.transactionUsesOrdinaryWrite = false;
|
|
267
338
|
binding.transactionFailure = undefined;
|
|
339
|
+
binding.transactionCommitReached = false;
|
|
340
|
+
binding.transactionEnded = undefined;
|
|
341
|
+
}
|
|
342
|
+
if (transactionEnded) {
|
|
343
|
+
throw createTransactionSessionEndedError(transactionEnded);
|
|
268
344
|
}
|
|
345
|
+
return result;
|
|
269
346
|
}
|
|
270
347
|
|
|
271
348
|
public async close(): Promise<void> {
|
|
@@ -301,6 +378,7 @@ export const createSmartdataSession = (ownerArg: SmartdataDb): SmartdataSession
|
|
|
301
378
|
transactionActive: false,
|
|
302
379
|
transactionCallbackOpen: false,
|
|
303
380
|
transactionUsesOrdinaryWrite: false,
|
|
381
|
+
transactionCommitReached: false,
|
|
304
382
|
invalidated: false,
|
|
305
383
|
};
|
|
306
384
|
bindings.set(session, binding);
|
|
@@ -559,8 +637,13 @@ export const runWithOrdinarySmartdataSession = async <TResult>(
|
|
|
559
637
|
}
|
|
560
638
|
};
|
|
561
639
|
|
|
640
|
+
/**
|
|
641
|
+
* Ends every session of the owner. `endOptionsArg.timeoutMS` is the driver's
|
|
642
|
+
* deadline for aborting a transaction a session still holds.
|
|
643
|
+
*/
|
|
562
644
|
export const invalidateSmartdataSessions = async (
|
|
563
645
|
ownerArg: SmartdataDb,
|
|
646
|
+
endOptionsArg?: Pick<plugins.mongodb.EndSessionOptions, 'timeoutMS'>,
|
|
564
647
|
): Promise<void> => {
|
|
565
648
|
const ownerBindings = bindingsByOwner.get(ownerArg);
|
|
566
649
|
if (!ownerBindings) {
|
|
@@ -568,11 +651,11 @@ export const invalidateSmartdataSessions = async (
|
|
|
568
651
|
}
|
|
569
652
|
const sessions = [...ownerBindings];
|
|
570
653
|
for (const bindingArg of sessions) {
|
|
571
|
-
bindingArg
|
|
654
|
+
invalidateSessionBinding(bindingArg);
|
|
572
655
|
}
|
|
573
656
|
const results = await Promise.allSettled(
|
|
574
657
|
sessions.map((bindingArg) =>
|
|
575
|
-
closeSessionBinding(bindingArg),
|
|
658
|
+
closeSessionBinding(bindingArg, endOptionsArg),
|
|
576
659
|
),
|
|
577
660
|
);
|
|
578
661
|
const errors = results.flatMap((resultArg) =>
|
|
@@ -589,12 +672,28 @@ export const invalidateSmartdataSessions = async (
|
|
|
589
672
|
}
|
|
590
673
|
};
|
|
591
674
|
|
|
675
|
+
/**
|
|
676
|
+
* Refuses every later operation on the session. A transaction still running on
|
|
677
|
+
* it records how far it had got, so it rejects instead of reporting a commit.
|
|
678
|
+
*/
|
|
679
|
+
const invalidateSessionBinding = (bindingArg: ISmartdataSessionBinding): void => {
|
|
680
|
+
bindingArg.invalidated = true;
|
|
681
|
+
if (bindingArg.transactionActive) {
|
|
682
|
+
bindingArg.transactionEnded ??= bindingArg.transactionCommitReached
|
|
683
|
+
? 'during_commit'
|
|
684
|
+
: 'before_commit';
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
592
688
|
/** Failed cleanup remains owned and invalidated until an explicit retry succeeds. */
|
|
593
|
-
const closeSessionBinding = async (
|
|
689
|
+
const closeSessionBinding = async (
|
|
690
|
+
bindingArg: ISmartdataSessionBinding,
|
|
691
|
+
endOptionsArg?: Pick<plugins.mongodb.EndSessionOptions, 'timeoutMS'>,
|
|
692
|
+
): Promise<void> => {
|
|
594
693
|
if (bindingArg.closing) return bindingArg.closing;
|
|
595
|
-
bindingArg
|
|
694
|
+
invalidateSessionBinding(bindingArg);
|
|
596
695
|
const closing = Promise.resolve().then(async () => {
|
|
597
|
-
await bindingArg.rawSession.endSession();
|
|
696
|
+
await bindingArg.rawSession.endSession(endOptionsArg);
|
|
598
697
|
bindingsByOwner.get(bindingArg.owner)?.delete(bindingArg);
|
|
599
698
|
bindings.delete(bindingArg.session);
|
|
600
699
|
});
|