@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
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
|
|
3
3
|
import type { SmartdataDb } from './classes.db.js';
|
|
4
|
+
import {
|
|
5
|
+
SmartdataOperationDeadline,
|
|
6
|
+
requireOperationSignal,
|
|
7
|
+
requireOperationTimeoutMS,
|
|
8
|
+
} from './classes.operationdeadline.js';
|
|
9
|
+
import { assertDataOptions } from './classes.operationbudget.js';
|
|
4
10
|
import {
|
|
5
11
|
SmartdataPersistenceError,
|
|
12
|
+
createSmartdataTimeoutError,
|
|
13
|
+
isMongoDeadlineError,
|
|
14
|
+
normalizeDeadlineError,
|
|
6
15
|
normalizeOrdinaryPersistenceError,
|
|
7
16
|
} from './classes.persistence.js';
|
|
8
17
|
|
|
@@ -16,6 +25,10 @@ interface ISmartdataSessionBinding {
|
|
|
16
25
|
transactionActive: boolean;
|
|
17
26
|
transactionCallbackOpen: boolean;
|
|
18
27
|
transactionUsesOrdinaryWrite: boolean;
|
|
28
|
+
/** Set while a transaction with a client deadline runs; it bounds every operation inside. */
|
|
29
|
+
transactionTimeoutMS?: number;
|
|
30
|
+
/** Set while a transaction with a signal runs; every operation inside observes it. */
|
|
31
|
+
transactionSignal?: AbortSignal;
|
|
19
32
|
transactionAttempt?: symbol;
|
|
20
33
|
transactionFailure?: { error: unknown };
|
|
21
34
|
operationSettled?: Promise<void>;
|
|
@@ -47,6 +60,28 @@ interface IOrdinarySessionOperationOptions {
|
|
|
47
60
|
prepared?: boolean;
|
|
48
61
|
/** Names the collection whose preparation a refusal must ask for. */
|
|
49
62
|
collectionName?: string;
|
|
63
|
+
/** The caller's validated client deadline for the call's database work. */
|
|
64
|
+
timeoutMS?: number;
|
|
65
|
+
/** The API's own default deadline; a transaction deadline replaces it. */
|
|
66
|
+
defaultTimeoutMS?: number;
|
|
67
|
+
/** The caller's validated cancellation signal for the call's database work. */
|
|
68
|
+
signal?: AbortSignal;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ISmartdataTransactionOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Client deadline for the whole transaction (1..120000 ms): every attempt,
|
|
74
|
+
* the operations the callback runs on the session, the driver's retries and
|
|
75
|
+
* the commit. Operations inside take no `timeoutMS` of their own.
|
|
76
|
+
*/
|
|
77
|
+
timeoutMS?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Cancels the transaction. Every operation the callback runs on the session
|
|
80
|
+
* observes it: the one in flight is interrupted and every later one is
|
|
81
|
+
* refused with the abort reason. An aborted transaction is rolled back and
|
|
82
|
+
* never committed; a commit already sent runs to its own outcome.
|
|
83
|
+
*/
|
|
84
|
+
signal?: AbortSignal;
|
|
50
85
|
}
|
|
51
86
|
|
|
52
87
|
const transactionContext = new plugins.nodeAsyncHooks.AsyncLocalStorage<
|
|
@@ -82,6 +117,19 @@ const resolveBinding = (
|
|
|
82
117
|
return binding;
|
|
83
118
|
};
|
|
84
119
|
|
|
120
|
+
const normalizeTransactionOptions = (
|
|
121
|
+
optionsArg: ISmartdataTransactionOptions | undefined,
|
|
122
|
+
): { timeoutMS?: number; signal?: AbortSignal } => {
|
|
123
|
+
if (optionsArg === undefined) {
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
const { timeoutMS, signal } = assertDataOptions(optionsArg, ['timeoutMS', 'signal']);
|
|
127
|
+
return {
|
|
128
|
+
timeoutMS: requireOperationTimeoutMS(timeoutMS, 'SmartData transaction timeoutMS'),
|
|
129
|
+
signal: requireOperationSignal(signal, 'SmartData transaction signal'),
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
|
|
85
133
|
const assertTransactionSucceeded = (bindingArg: ISmartdataSessionBinding): void => {
|
|
86
134
|
if (bindingArg.transactionFailure) {
|
|
87
135
|
throw bindingArg.transactionFailure.error;
|
|
@@ -89,8 +137,35 @@ const assertTransactionSucceeded = (bindingArg: ISmartdataSessionBinding): void
|
|
|
89
137
|
};
|
|
90
138
|
|
|
91
139
|
export class SmartdataSession {
|
|
140
|
+
/**
|
|
141
|
+
* Runs `callbackArg` in a transaction, retrying it as the driver decides.
|
|
142
|
+
*
|
|
143
|
+
* `timeoutMS` bounds the whole transaction through the driver's client-side
|
|
144
|
+
* operation timeout. When it expires the call rejects with a
|
|
145
|
+
* `SmartdataPersistenceError` of code `timeout`: before the commit was sent
|
|
146
|
+
* nothing was committed, during the commit the outcome is unknown. When the
|
|
147
|
+
* deadline expires inside the callback, the driver's closing
|
|
148
|
+
* `abortTransaction` runs on a fresh deadline of the same length, so a
|
|
149
|
+
* stalled database settles the call within about twice `timeoutMS`.
|
|
150
|
+
*
|
|
151
|
+
* `signal` cancels the transaction. An abort before the call rejects with
|
|
152
|
+
* the abort reason at once. An abort while the callback runs interrupts the
|
|
153
|
+
* operation in flight on the session and refuses every later one with the
|
|
154
|
+
* reason; once the callback has settled, the driver's `abortTransaction`
|
|
155
|
+
* rolls the attempt back and the call rejects with the reason, even when
|
|
156
|
+
* the callback caught it, and no further attempt starts. The driver cannot
|
|
157
|
+
* cancel a commit in flight, so an abort that arrives after the commit was
|
|
158
|
+
* sent leaves the commit to its own outcome: the call returns the result
|
|
159
|
+
* when it committed, and rejects with `timeout` ("the commit outcome is
|
|
160
|
+
* unknown") when the deadline expired during it.
|
|
161
|
+
*
|
|
162
|
+
* Any operation on the session that its deadline or a signal interrupts
|
|
163
|
+
* fails the attempt: the call rolls it back and rejects with that
|
|
164
|
+
* operation's error even when the callback caught it.
|
|
165
|
+
*/
|
|
92
166
|
public async withTransaction<TResult>(
|
|
93
167
|
callbackArg: (sessionArg: SmartdataSession) => Promise<TResult>,
|
|
168
|
+
optionsArg?: ISmartdataTransactionOptions,
|
|
94
169
|
): Promise<TResult | undefined> {
|
|
95
170
|
if (typeof callbackArg !== 'function') {
|
|
96
171
|
throw new SmartdataPersistenceError(
|
|
@@ -98,6 +173,7 @@ export class SmartdataSession {
|
|
|
98
173
|
'SmartData transaction callback must be a function.',
|
|
99
174
|
);
|
|
100
175
|
}
|
|
176
|
+
const { timeoutMS, signal } = normalizeTransactionOptions(optionsArg);
|
|
101
177
|
const binding = resolveBinding(this);
|
|
102
178
|
if (binding.operationActive || binding.transactionActive) {
|
|
103
179
|
throw new SmartdataPersistenceError(
|
|
@@ -105,52 +181,76 @@ export class SmartdataSession {
|
|
|
105
181
|
'Parallel or nested SmartData session operations are not supported.',
|
|
106
182
|
);
|
|
107
183
|
}
|
|
184
|
+
signal?.throwIfAborted();
|
|
108
185
|
binding.transactionActive = true;
|
|
186
|
+
binding.transactionTimeoutMS = timeoutMS;
|
|
187
|
+
binding.transactionSignal = signal;
|
|
109
188
|
let callbackError: unknown;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
189
|
+
let commitReached = false;
|
|
190
|
+
const runAttempt = async () => {
|
|
191
|
+
const attempt = Symbol('smartdataTransactionAttempt');
|
|
192
|
+
binding.transactionAttempt = attempt;
|
|
193
|
+
binding.transactionCallbackOpen = true;
|
|
194
|
+
binding.transactionUsesOrdinaryWrite = false;
|
|
195
|
+
binding.transactionFailure = undefined;
|
|
196
|
+
callbackError = undefined;
|
|
197
|
+
commitReached = false;
|
|
198
|
+
try {
|
|
199
|
+
let result: TResult;
|
|
200
|
+
let callbackReturned = false;
|
|
118
201
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
202
|
+
// A retry the driver starts after an abort ends before the callback.
|
|
203
|
+
signal?.throwIfAborted();
|
|
204
|
+
result = await transactionContext.run(
|
|
205
|
+
{ contextToken: binding.contextToken, attempt },
|
|
206
|
+
async () => callbackArg(this),
|
|
207
|
+
);
|
|
208
|
+
callbackReturned = true;
|
|
209
|
+
} finally {
|
|
210
|
+
const activeOperation = binding.operationActive
|
|
211
|
+
? binding.operationSettled
|
|
212
|
+
: undefined;
|
|
213
|
+
binding.transactionCallbackOpen = false;
|
|
214
|
+
if (binding.transactionAttempt === attempt) {
|
|
215
|
+
binding.transactionAttempt = undefined;
|
|
216
|
+
}
|
|
217
|
+
if (activeOperation) {
|
|
218
|
+
await activeOperation;
|
|
219
|
+
}
|
|
220
|
+
assertTransactionSucceeded(binding);
|
|
221
|
+
if (callbackReturned && activeOperation) {
|
|
222
|
+
throw new SmartdataPersistenceError(
|
|
223
|
+
'unsupported_operation',
|
|
224
|
+
'A SmartData transaction callback returned with an operation still active.',
|
|
124
225
|
);
|
|
125
|
-
callbackReturned = true;
|
|
126
|
-
return result;
|
|
127
|
-
} finally {
|
|
128
|
-
const activeOperation = binding.operationActive
|
|
129
|
-
? binding.operationSettled
|
|
130
|
-
: undefined;
|
|
131
|
-
binding.transactionCallbackOpen = false;
|
|
132
|
-
if (binding.transactionAttempt === attempt) {
|
|
133
|
-
binding.transactionAttempt = undefined;
|
|
134
|
-
}
|
|
135
|
-
if (activeOperation) {
|
|
136
|
-
await activeOperation;
|
|
137
|
-
}
|
|
138
|
-
assertTransactionSucceeded(binding);
|
|
139
|
-
if (callbackReturned && activeOperation) {
|
|
140
|
-
throw new SmartdataPersistenceError(
|
|
141
|
-
'unsupported_operation',
|
|
142
|
-
'A SmartData transaction callback returned with an operation still active.',
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
226
|
}
|
|
146
|
-
} catch (errorArg) {
|
|
147
|
-
callbackError = errorArg;
|
|
148
|
-
throw errorArg;
|
|
149
227
|
}
|
|
150
|
-
|
|
228
|
+
// An abort that arrived while the callback ran rolls the attempt back.
|
|
229
|
+
signal?.throwIfAborted();
|
|
230
|
+
// The driver commits next; a deadline that expires from here on
|
|
231
|
+
// leaves the commit outcome unknown.
|
|
232
|
+
commitReached = true;
|
|
233
|
+
return result;
|
|
234
|
+
} catch (errorArg) {
|
|
235
|
+
callbackError = errorArg;
|
|
236
|
+
throw errorArg;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
try {
|
|
240
|
+
return timeoutMS === undefined
|
|
241
|
+
? await binding.rawSession.withTransaction(runAttempt)
|
|
242
|
+
: await binding.rawSession.withTransaction(runAttempt, { timeoutMS });
|
|
151
243
|
} catch (errorArg) {
|
|
152
244
|
if (errorArg === callbackError) {
|
|
153
|
-
throw errorArg;
|
|
245
|
+
throw normalizeDeadlineError(errorArg);
|
|
246
|
+
}
|
|
247
|
+
if (isMongoDeadlineError(errorArg)) {
|
|
248
|
+
throw createSmartdataTimeoutError(
|
|
249
|
+
commitReached
|
|
250
|
+
? 'SmartData transaction exceeded its client deadline during commit; the commit outcome is unknown.'
|
|
251
|
+
: 'SmartData transaction exceeded its client deadline before commit; nothing was committed.',
|
|
252
|
+
errorArg,
|
|
253
|
+
);
|
|
154
254
|
}
|
|
155
255
|
if (binding.transactionUsesOrdinaryWrite) {
|
|
156
256
|
return normalizeOrdinaryPersistenceError(
|
|
@@ -161,6 +261,8 @@ export class SmartdataSession {
|
|
|
161
261
|
throw errorArg;
|
|
162
262
|
} finally {
|
|
163
263
|
binding.transactionActive = false;
|
|
264
|
+
binding.transactionTimeoutMS = undefined;
|
|
265
|
+
binding.transactionSignal = undefined;
|
|
164
266
|
binding.transactionUsesOrdinaryWrite = false;
|
|
165
267
|
binding.transactionFailure = undefined;
|
|
166
268
|
}
|
|
@@ -220,6 +322,15 @@ export const acquireSmartdataSession = (
|
|
|
220
322
|
release: () => void;
|
|
221
323
|
} => {
|
|
222
324
|
const binding = resolveBinding(sessionArg, ownerArg);
|
|
325
|
+
if (
|
|
326
|
+
binding.transactionActive
|
|
327
|
+
&& binding.transactionSignal?.aborted
|
|
328
|
+
&& transactionContext.getStore()?.contextToken === binding.contextToken
|
|
329
|
+
) {
|
|
330
|
+
// The transaction was cancelled: the attempt fails with the abort reason.
|
|
331
|
+
binding.transactionFailure ??= { error: binding.transactionSignal.reason };
|
|
332
|
+
throw binding.transactionSignal.reason;
|
|
333
|
+
}
|
|
223
334
|
if (binding.transactionFailure) {
|
|
224
335
|
throw new SmartdataPersistenceError(
|
|
225
336
|
'unsupported_operation',
|
|
@@ -361,17 +472,88 @@ export const leaseOrdinarySmartdataSession = (
|
|
|
361
472
|
});
|
|
362
473
|
};
|
|
363
474
|
|
|
475
|
+
/**
|
|
476
|
+
* The deadline and signal one operation runs under. Inside an owned
|
|
477
|
+
* transaction with a deadline, the transaction's deadline bounds the
|
|
478
|
+
* operation through the session, and the driver refuses a second one on the
|
|
479
|
+
* same session, so an explicit operation deadline is refused and a default
|
|
480
|
+
* one is dropped. Inside an owned transaction with a signal, the operation
|
|
481
|
+
* observes that signal beside its own.
|
|
482
|
+
*/
|
|
483
|
+
export const resolveSmartdataOperationDeadline = (
|
|
484
|
+
sessionArg: TSmartdataOrdinarySession | undefined,
|
|
485
|
+
optionsArg: Pick<IOrdinarySessionOperationOptions, 'timeoutMS' | 'defaultTimeoutMS' | 'signal'>,
|
|
486
|
+
): SmartdataOperationDeadline => {
|
|
487
|
+
const binding = isSmartdataSession(sessionArg) ? bindings.get(sessionArg) : undefined;
|
|
488
|
+
const transactionSignal = binding?.transactionActive ? binding.transactionSignal : undefined;
|
|
489
|
+
const signals = [optionsArg.signal, transactionSignal].filter(
|
|
490
|
+
(signalArg): signalArg is AbortSignal => signalArg !== undefined,
|
|
491
|
+
);
|
|
492
|
+
const signal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
|
|
493
|
+
if (binding?.transactionTimeoutMS !== undefined) {
|
|
494
|
+
if (optionsArg.timeoutMS !== undefined) {
|
|
495
|
+
throw new SmartdataPersistenceError(
|
|
496
|
+
'invalid_argument',
|
|
497
|
+
'The SmartData transaction deadline bounds every operation inside it; omit the operation timeoutMS.',
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
return signal === undefined
|
|
501
|
+
? SmartdataOperationDeadline.unbounded
|
|
502
|
+
: new SmartdataOperationDeadline(undefined, signal);
|
|
503
|
+
}
|
|
504
|
+
const timeoutMS = optionsArg.timeoutMS ?? optionsArg.defaultTimeoutMS;
|
|
505
|
+
return timeoutMS === undefined && signal === undefined
|
|
506
|
+
? SmartdataOperationDeadline.unbounded
|
|
507
|
+
: new SmartdataOperationDeadline(timeoutMS, signal);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* The error one operation on the session boundary rejects with: an expired
|
|
512
|
+
* driver deadline becomes the `timeout` persistence error, everything else
|
|
513
|
+
* passes through. An operation its deadline or signal interrupted inside an
|
|
514
|
+
* owned transaction also fails the transaction attempt, so `withTransaction`
|
|
515
|
+
* rolls it back and rejects with that error even when the callback caught it:
|
|
516
|
+
* whatever the interrupted command did inside the transaction is unknown.
|
|
517
|
+
*/
|
|
518
|
+
export const settleSmartdataOperationError = (
|
|
519
|
+
sessionArg: TSmartdataOrdinarySession | undefined,
|
|
520
|
+
deadlineArg: SmartdataOperationDeadline,
|
|
521
|
+
errorArg: unknown,
|
|
522
|
+
): unknown => {
|
|
523
|
+
const error = normalizeDeadlineError(errorArg);
|
|
524
|
+
const binding = isSmartdataSession(sessionArg) ? bindings.get(sessionArg) : undefined;
|
|
525
|
+
const aborted = deadlineArg.signal?.aborted === true && errorArg === deadlineArg.signal.reason;
|
|
526
|
+
const expired = error instanceof SmartdataPersistenceError && error.code === 'timeout';
|
|
527
|
+
if (binding?.transactionActive && (aborted || expired)) {
|
|
528
|
+
binding.transactionFailure ??= { error };
|
|
529
|
+
}
|
|
530
|
+
return error;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Runs one model operation on the ordinary session boundary: the owned
|
|
535
|
+
* session lease, the operation's client deadline and signal, and the mapping
|
|
536
|
+
* of an expired driver deadline to the `timeout` persistence error.
|
|
537
|
+
*/
|
|
364
538
|
export const runWithOrdinarySmartdataSession = async <TResult>(
|
|
365
539
|
sessionArg: TSmartdataOrdinarySession | undefined,
|
|
366
540
|
ownerArg: SmartdataDb,
|
|
367
541
|
optionsArg: IOrdinarySessionOperationOptions,
|
|
368
542
|
operationArg: (
|
|
369
543
|
rawSessionArg: plugins.mongodb.ClientSession | undefined,
|
|
544
|
+
deadlineArg: SmartdataOperationDeadline,
|
|
370
545
|
) => Promise<TResult>,
|
|
371
546
|
): Promise<TResult> => {
|
|
547
|
+
optionsArg.signal?.throwIfAborted();
|
|
372
548
|
const lease = leaseOrdinarySmartdataSession(sessionArg, ownerArg, optionsArg);
|
|
549
|
+
let deadline: SmartdataOperationDeadline | undefined;
|
|
373
550
|
try {
|
|
374
|
-
|
|
551
|
+
deadline = resolveSmartdataOperationDeadline(sessionArg, optionsArg);
|
|
552
|
+
return await operationArg(lease.rawSession, deadline);
|
|
553
|
+
} catch (errorArg) {
|
|
554
|
+
throw deadline
|
|
555
|
+
? settleSmartdataOperationError(sessionArg, deadline, errorArg)
|
|
556
|
+
: normalizeDeadlineError(errorArg);
|
|
375
557
|
} finally {
|
|
376
558
|
lease.release();
|
|
377
559
|
}
|