@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.
Files changed (51) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/nosqldb/classes.atomicdelete.d.ts +1 -0
  3. package/dist_ts/nosqldb/classes.atomicdelete.js +10 -7
  4. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +2 -0
  5. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +9 -5
  6. package/dist_ts/nosqldb/classes.atomicupdate.d.ts +2 -0
  7. package/dist_ts/nosqldb/classes.atomicupdate.js +13 -8
  8. package/dist_ts/nosqldb/classes.collection.d.ts +13 -4
  9. package/dist_ts/nosqldb/classes.collection.js +57 -64
  10. package/dist_ts/nosqldb/classes.db.d.ts +1 -1
  11. package/dist_ts/nosqldb/classes.doc.d.ts +60 -3
  12. package/dist_ts/nosqldb/classes.doc.js +68 -26
  13. package/dist_ts/nosqldb/classes.exactpersistence.d.ts +36 -7
  14. package/dist_ts/nosqldb/classes.exactpersistence.js +92 -81
  15. package/dist_ts/nosqldb/classes.operationdeadline.d.ts +61 -0
  16. package/dist_ts/nosqldb/classes.operationdeadline.js +149 -0
  17. package/dist_ts/nosqldb/classes.persistence.d.ts +16 -1
  18. package/dist_ts/nosqldb/classes.persistence.js +20 -1
  19. package/dist_ts/nosqldb/classes.session.d.ts +73 -2
  20. package/dist_ts/nosqldb/classes.session.js +164 -38
  21. package/dist_ts/objectstorage/classes.bucket.d.ts +61 -3
  22. package/dist_ts/objectstorage/classes.bucket.js +243 -184
  23. package/dist_ts/objectstorage/classes.directory.js +17 -28
  24. package/dist_ts/objectstorage/classes.smartbucket.d.ts +13 -6
  25. package/dist_ts/objectstorage/classes.smartbucket.js +18 -12
  26. package/dist_ts/objectstorage/classes.watcher.js +1 -2
  27. package/dist_ts/objectstorage/interfaces.d.ts +8 -0
  28. package/dist_ts/objectstorage/internal.bucketrequests.d.ts +14 -0
  29. package/dist_ts/objectstorage/internal.bucketrequests.js +53 -0
  30. package/dist_ts/objectstorage/internal.exactupload.capability.d.ts +12 -0
  31. package/dist_ts/objectstorage/internal.exactupload.capability.js +46 -17
  32. package/package.json +3 -3
  33. package/readme.md +13 -2
  34. package/ts/00_commitinfo_data.ts +1 -1
  35. package/ts/nosqldb/classes.atomicdelete.ts +10 -6
  36. package/ts/nosqldb/classes.atomicfindoneandupdate.ts +10 -4
  37. package/ts/nosqldb/classes.atomicupdate.ts +14 -7
  38. package/ts/nosqldb/classes.collection.ts +82 -73
  39. package/ts/nosqldb/classes.db.ts +1 -1
  40. package/ts/nosqldb/classes.doc.ts +162 -33
  41. package/ts/nosqldb/classes.exactpersistence.ts +131 -93
  42. package/ts/nosqldb/classes.operationdeadline.ts +179 -0
  43. package/ts/nosqldb/classes.persistence.ts +36 -1
  44. package/ts/nosqldb/classes.session.ts +221 -39
  45. package/ts/objectstorage/classes.bucket.ts +350 -218
  46. package/ts/objectstorage/classes.directory.ts +18 -27
  47. package/ts/objectstorage/classes.smartbucket.ts +21 -11
  48. package/ts/objectstorage/classes.watcher.ts +0 -1
  49. package/ts/objectstorage/interfaces.ts +9 -0
  50. package/ts/objectstorage/internal.bucketrequests.ts +70 -0
  51. 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
- try {
111
- return await binding.rawSession.withTransaction(async () => {
112
- const attempt = Symbol('smartdataTransactionAttempt');
113
- binding.transactionAttempt = attempt;
114
- binding.transactionCallbackOpen = true;
115
- binding.transactionUsesOrdinaryWrite = false;
116
- binding.transactionFailure = undefined;
117
- callbackError = undefined;
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
- let callbackReturned = false;
120
- try {
121
- const result = await transactionContext.run(
122
- { contextToken: binding.contextToken, attempt },
123
- async () => callbackArg(this),
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
- return await operationArg(lease.rawSession);
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
  }