@lossless.org/client 1.23.3 → 2.1.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 (41) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/nosqldb/classes.persistence.d.ts +21 -1
  3. package/dist_ts/nosqldb/classes.persistence.js +23 -1
  4. package/dist_ts/nosqldb/classes.session.d.ts +8 -0
  5. package/dist_ts/nosqldb/classes.session.js +27 -2
  6. package/dist_ts/nosqldb/index.d.ts +1 -1
  7. package/dist_ts/nosqldb/index.js +2 -2
  8. package/dist_ts/objectstorage/classes.bucket.d.ts +11 -21
  9. package/dist_ts/objectstorage/classes.bucket.js +58 -29
  10. package/dist_ts/objectstorage/classes.connection.d.ts +2 -1
  11. package/dist_ts/objectstorage/classes.connection.js +3 -2
  12. package/dist_ts/objectstorage/classes.requestdeadlineerror.d.ts +11 -0
  13. package/dist_ts/objectstorage/classes.requestdeadlineerror.js +16 -0
  14. package/dist_ts/objectstorage/classes.smartbucket.d.ts +11 -2
  15. package/dist_ts/objectstorage/classes.smartbucket.js +20 -1
  16. package/dist_ts/objectstorage/helpers.d.ts +4 -0
  17. package/dist_ts/objectstorage/helpers.js +13 -1
  18. package/dist_ts/objectstorage/index.d.ts +1 -0
  19. package/dist_ts/objectstorage/index.js +2 -1
  20. package/dist_ts/objectstorage/interfaces.d.ts +49 -0
  21. package/dist_ts/objectstorage/internal.exactupload.capability.js +4 -2
  22. package/dist_ts/objectstorage/internal.requestbounds.d.ts +37 -0
  23. package/dist_ts/objectstorage/internal.requestbounds.js +154 -0
  24. package/dist_ts/objectstorage/plugins.d.ts +2 -1
  25. package/dist_ts/objectstorage/plugins.js +3 -2
  26. package/package.json +2 -2
  27. package/readme.md +7 -0
  28. package/ts/00_commitinfo_data.ts +1 -1
  29. package/ts/nosqldb/classes.persistence.ts +30 -1
  30. package/ts/nosqldb/classes.session.ts +26 -0
  31. package/ts/nosqldb/index.ts +1 -1
  32. package/ts/objectstorage/classes.bucket.ts +80 -56
  33. package/ts/objectstorage/classes.connection.ts +4 -2
  34. package/ts/objectstorage/classes.requestdeadlineerror.ts +16 -0
  35. package/ts/objectstorage/classes.smartbucket.ts +30 -1
  36. package/ts/objectstorage/helpers.ts +16 -0
  37. package/ts/objectstorage/index.ts +1 -0
  38. package/ts/objectstorage/interfaces.ts +58 -0
  39. package/ts/objectstorage/internal.exactupload.capability.ts +3 -1
  40. package/ts/objectstorage/internal.requestbounds.ts +192 -0
  41. package/ts/objectstorage/plugins.ts +2 -1
@@ -11,6 +11,7 @@ import {
11
11
  import { assertDataOptions } from './classes.operationbudget.js';
12
12
  import {
13
13
  SmartdataPersistenceError,
14
+ SmartdataTransactionConflictError,
14
15
  createSmartdataTimeoutError,
15
16
  isMongoDeadlineError,
16
17
  isMongoTransientTransactionError,
@@ -355,6 +356,14 @@ export class SmartdataSession {
355
356
  * committed, afterwards the commit outcome is unknown. The callback's
356
357
  * error, if it threw, is the `cause`.
357
358
  *
359
+ * The driver re-runs an attempt the database aborted as transient — a
360
+ * write conflict or another error labelled `TransientTransactionError` —
361
+ * until its retry window of 120 seconds closes. When every attempt was
362
+ * aborted that way, the call rejects with `SmartdataTransactionConflictError`
363
+ * (code `transaction_conflict`): nothing was committed and the transaction
364
+ * is safe to re-run. With `timeoutMS` the deadline is the window, and the
365
+ * call rejects with `timeout` instead.
366
+ *
358
367
  * Every rejection states whether the transaction could have committed:
359
368
  * `SmartdataSession.getCommitOutcome(error)` returns `not_committed` or
360
369
  * `unknown` for the value the call rejected with.
@@ -370,6 +379,9 @@ export class SmartdataSession {
370
379
  throw rejectTransaction(errorArg, 'not_committed');
371
380
  }
372
381
  const { binding, timeoutMS, signal } = admitted;
382
+ const startedAt = performance.now();
383
+ /** How many times the driver started the callback. */
384
+ let attempts = 0;
373
385
  binding.transactionActive = true;
374
386
  binding.transactionTimeoutMS = timeoutMS;
375
387
  binding.transactionSignal = signal;
@@ -385,6 +397,7 @@ export class SmartdataSession {
385
397
  const runAttempt = async () => {
386
398
  // The last attempt's slots end after this one starts, so it bumps others.
387
399
  const attemptStartedAt = performance.now();
400
+ attempts++;
388
401
  revisionAttempt?.finish(false);
389
402
  revisionAttempt = beginSmartdataWriteRevisionAttempt(binding.owner, attemptStartedAt);
390
403
  // The driver starts another attempt only once the last one was aborted.
@@ -511,6 +524,19 @@ export class SmartdataSession {
511
524
  outcome: commitOutcomeOfTransactionEnd(binding.transactionEnded),
512
525
  };
513
526
  }
527
+ // The driver re-runs every attempt aborted as transient until its retry
528
+ // window closes, so such an error that still reaches here exhausted it.
529
+ // With `timeoutMS` the driver raises a deadline error in its place.
530
+ if (isMongoTransientTransactionError(errorArg) && !isMongoDeadlineError(errorArg)) {
531
+ return {
532
+ error: new SmartdataTransactionConflictError({
533
+ attempts,
534
+ elapsedMS: Math.round(performance.now() - startedAt),
535
+ cause: errorArg,
536
+ }),
537
+ outcome: 'not_committed',
538
+ };
539
+ }
514
540
  if (errorArg === callbackError) {
515
541
  return { error: normalizeDeadlineError(errorArg), outcome: 'not_committed' };
516
542
  }
@@ -48,7 +48,7 @@ export type {
48
48
  export type {
49
49
  ISmartdataQueryPlanOptions, ISmartdataQueryPlan, ISmartdataQueryPlanStage,
50
50
  } from './classes.queryplaninspection.js';
51
- export { SmartdataPersistenceError } from './classes.persistence.js';
51
+ export { SmartdataPersistenceError, SmartdataTransactionConflictError } from './classes.persistence.js';
52
52
  export type { TSmartdataPersistenceErrorCode } from './classes.persistence.js';
53
53
  export * from './classes.exactpersistence.js';
54
54
  export * from './classes.easystore.js';
@@ -30,6 +30,8 @@ export interface IListObjectKeysPageOptions {
30
30
  prefix?: string;
31
31
  limit: number;
32
32
  startAfter?: string;
33
+ /** Optional caller-owned cancellation signal */
34
+ signal?: AbortSignal;
33
35
  }
34
36
 
35
37
  export interface IListObjectKeysPageResult {
@@ -37,10 +39,7 @@ export interface IListObjectKeysPageResult {
37
39
  nextStartAfter?: string;
38
40
  }
39
41
 
40
- export interface IListObjectEntriesPageOptions extends IListObjectKeysPageOptions {
41
- /** Optional caller-owned cancellation signal */
42
- signal?: AbortSignal;
43
- }
42
+ export interface IListObjectEntriesPageOptions extends IListObjectKeysPageOptions {}
44
43
 
45
44
  /**
46
45
  * One object as ListObjectsV2 reported it when the page was listed.
@@ -429,17 +428,15 @@ export class Bucket {
429
428
  // ===============
430
429
 
431
430
  /**
432
- * store file
431
+ * Stores an object with one PutObject request; without `overwrite: true`
432
+ * a HeadObject request first refuses an existing object. `signal` aborts
433
+ * the request in flight and rejects with its reason.
433
434
  */
434
- public async fastPut(
435
- optionsArg: interfaces.IPathDecriptor & {
436
- contents: string | Buffer;
437
- overwrite?: boolean;
438
- }
439
- ): Promise<File> {
435
+ public async fastPut(optionsArg: interfaces.IFastPutOptions): Promise<File> {
436
+ const signal = helpers.requireOptionalAbortSignal(optionsArg.signal, 'fastPut signal');
440
437
  const reducedPath = await helpers.reducePathDescriptorToPath(optionsArg);
441
438
  if (optionsArg.overwrite !== true) {
442
- const exists = await this.fastExists({ path: reducedPath });
439
+ const exists = await this.fastExists({ path: reducedPath, signal });
443
440
 
444
441
  if (exists) {
445
442
  throw new Error(
@@ -449,12 +446,19 @@ export class Bucket {
449
446
  }
450
447
  }
451
448
 
452
- const command = new plugins.s3.PutObjectCommand({
453
- Bucket: this.name,
454
- Key: reducedPath,
455
- Body: optionsArg.contents,
456
- });
457
- await this.smartbucketRef.storageClient.send(command);
449
+ const operation = beginBasicBucketOperation(this, signal);
450
+ try {
451
+ const command = new plugins.s3.PutObjectCommand({
452
+ Bucket: operation.bucketName,
453
+ Key: reducedPath,
454
+ Body: optionsArg.contents,
455
+ });
456
+ await operation.runProvider(
457
+ () => operation.client.send(command, { abortSignal: operation.signal }),
458
+ );
459
+ } finally {
460
+ operation.finish();
461
+ }
458
462
 
459
463
  const parsedPath = plugins.path.parse(reducedPath);
460
464
  return new File({
@@ -469,7 +473,7 @@ export class Bucket {
469
473
  /**
470
474
  * get file
471
475
  */
472
- public async fastGet(optionsArg: { path: string }): Promise<Buffer> {
476
+ public async fastGet(optionsArg: interfaces.IFastGetStreamOptions): Promise<Buffer> {
473
477
  return await readObjectBody(await this.fastGetStream(optionsArg));
474
478
  }
475
479
 
@@ -502,9 +506,9 @@ export class Bucket {
502
506
  * @param optionsArg
503
507
  * @returns
504
508
  */
505
- public async fastGetReplaySubject(optionsArg: {
506
- path: string;
507
- }): Promise<plugins.smartrx.rxjs.ReplaySubject<Buffer>> {
509
+ public async fastGetReplaySubject(
510
+ optionsArg: interfaces.IFastGetStreamOptions,
511
+ ): Promise<plugins.smartrx.rxjs.ReplaySubject<Buffer>> {
508
512
  const source = await this.fastGetStream(optionsArg);
509
513
  const replaySubject = new plugins.smartrx.rxjs.ReplaySubject<Buffer>();
510
514
  void (async () => {
@@ -965,15 +969,12 @@ export class Bucket {
965
969
  /**
966
970
  * Move object from one path to another within the same bucket or to another bucket
967
971
  */
968
- public async fastMove(optionsArg: {
969
- sourcePath: string;
970
- destinationPath: string;
971
- targetBucket?: Bucket;
972
- overwrite?: boolean;
973
- }): Promise<void> {
972
+ public async fastMove(optionsArg: interfaces.IFastMoveOptions): Promise<void> {
973
+ const signal = helpers.requireOptionalAbortSignal(optionsArg.signal, 'fastMove signal');
974
974
  const destinationBucket = optionsArg.targetBucket || this;
975
975
  const exists = await destinationBucket.fastExists({
976
976
  path: optionsArg.destinationPath,
977
+ signal,
977
978
  });
978
979
 
979
980
  if (exists && !optionsArg.overwrite) {
@@ -984,7 +985,7 @@ export class Bucket {
984
985
  }
985
986
 
986
987
  await this.fastCopy(optionsArg);
987
- await this.fastRemove({ path: optionsArg.sourcePath });
988
+ await this.fastRemove({ path: optionsArg.sourcePath, signal });
988
989
  }
989
990
 
990
991
  /**
@@ -1120,17 +1121,30 @@ export class Bucket {
1120
1121
  * }
1121
1122
  * ```
1122
1123
  */
1123
- public async *listAllObjects(prefix: string = ''): AsyncIterableIterator<string> {
1124
+ public async *listAllObjects(
1125
+ prefix: string = '',
1126
+ optionsArg: interfaces.IListAllObjectsOptions = {},
1127
+ ): AsyncIterableIterator<string> {
1128
+ const signal = helpers.requireOptionalAbortSignal(optionsArg.signal, 'listAllObjects signal');
1124
1129
  let continuationToken: string | undefined;
1125
1130
 
1126
1131
  do {
1127
- const command = new plugins.s3.ListObjectsV2Command({
1128
- Bucket: this.name,
1129
- Prefix: prefix,
1130
- ContinuationToken: continuationToken,
1131
- });
1132
-
1133
- const response = await this.smartbucketRef.storageClient.send(command);
1132
+ const operation = beginBasicBucketOperation(this, signal);
1133
+ let response: plugins.s3.ListObjectsV2CommandOutput;
1134
+ try {
1135
+ operation.assertCurrent();
1136
+ const command = new plugins.s3.ListObjectsV2Command({
1137
+ Bucket: operation.bucketName,
1138
+ Prefix: prefix,
1139
+ ContinuationToken: continuationToken,
1140
+ });
1141
+ response = await operation.runProvider(
1142
+ () => operation.client.send(command, { abortSignal: operation.signal }),
1143
+ );
1144
+ operation.assertCurrent();
1145
+ } finally {
1146
+ operation.finish();
1147
+ }
1134
1148
 
1135
1149
  for (const obj of response.Contents || []) {
1136
1150
  if (obj.Key) yield obj.Key;
@@ -1210,19 +1224,28 @@ export class Bucket {
1210
1224
  optionsArg: IListObjectKeysPageOptions,
1211
1225
  ): Promise<IListObjectKeysPageResult> {
1212
1226
  const prefix = assertListingPageOptions(optionsArg, 'Object key page');
1213
- const response = await this.smartbucketRef.storageClient.send(
1214
- createListingPageCommand(this.name, prefix, optionsArg),
1215
- );
1216
- const { keys, nextStartAfter } = readListingPageContents(
1217
- response,
1218
- prefix,
1219
- optionsArg,
1220
- 'key page',
1221
- );
1222
- return {
1223
- keys,
1224
- ...(nextStartAfter !== undefined ? { nextStartAfter } : {}),
1225
- };
1227
+ const signal = helpers.requireOptionalAbortSignal(optionsArg.signal, 'Object key page signal');
1228
+ const operation = beginBasicBucketOperation(this, signal);
1229
+ try {
1230
+ operation.assertCurrent();
1231
+ const command = createListingPageCommand(operation.bucketName, prefix, optionsArg);
1232
+ const response = await operation.runProvider(
1233
+ () => operation.client.send(command, { abortSignal: operation.signal }),
1234
+ );
1235
+ operation.assertCurrent();
1236
+ const { keys, nextStartAfter } = readListingPageContents(
1237
+ response,
1238
+ prefix,
1239
+ optionsArg,
1240
+ 'key page',
1241
+ );
1242
+ return {
1243
+ keys,
1244
+ ...(nextStartAfter !== undefined ? { nextStartAfter } : {}),
1245
+ };
1246
+ } finally {
1247
+ operation.finish();
1248
+ }
1226
1249
  }
1227
1250
 
1228
1251
  /**
@@ -1238,10 +1261,8 @@ export class Bucket {
1238
1261
  optionsArg: IListObjectEntriesPageOptions,
1239
1262
  ): Promise<IListObjectEntriesPageResult> {
1240
1263
  const prefix = assertListingPageOptions(optionsArg, 'Object entry page');
1241
- if (optionsArg.signal !== undefined && !(optionsArg.signal instanceof AbortSignal)) {
1242
- throw new TypeError('Object entry page signal must be an AbortSignal');
1243
- }
1244
- const operation = beginBasicBucketOperation(this, optionsArg.signal);
1264
+ const signal = helpers.requireOptionalAbortSignal(optionsArg.signal, 'Object entry page signal');
1265
+ const operation = beginBasicBucketOperation(this, signal);
1245
1266
  try {
1246
1267
  operation.assertCurrent();
1247
1268
  const command = createListingPageCommand(operation.bucketName, prefix, optionsArg);
@@ -1345,9 +1366,12 @@ export class Bucket {
1345
1366
  * console.log(`Found ${allKeys.length} objects`);
1346
1367
  * ```
1347
1368
  */
1348
- public async listAllObjectsArray(prefix: string = ''): Promise<string[]> {
1369
+ public async listAllObjectsArray(
1370
+ prefix: string = '',
1371
+ optionsArg: interfaces.IListAllObjectsOptions = {},
1372
+ ): Promise<string[]> {
1349
1373
  const keys: string[] = [];
1350
- for await (const key of this.listAllObjects(prefix)) {
1374
+ for await (const key of this.listAllObjects(prefix, optionsArg)) {
1351
1375
  keys.push(key);
1352
1376
  }
1353
1377
  return keys;
@@ -1,7 +1,8 @@
1
1
  import { SmartBucket } from './classes.smartbucket.js';
2
2
  import type { ICapabilities, IOperationOptions, IReadiness } from '../core/interfaces.js';
3
+ import type { IObjectStorageRequestOptions } from './interfaces.js';
3
4
 
4
- export interface IObjectStorageConnectionOptions {
5
+ export interface IObjectStorageConnectionOptions extends IObjectStorageRequestOptions {
5
6
  backend: 'objectstorage' | 's3';
6
7
  endpoint: string;
7
8
  region?: string;
@@ -23,7 +24,8 @@ export class ObjectStorageConnection extends SmartBucket {
23
24
  super({ endpoint: endpoint.hostname, port: Number(endpoint.port || (endpoint.protocol === 'https:' ? 443 : 80)),
24
25
  useSsl: endpoint.protocol === 'https:', accessKey: options.credentials.accessKeyId,
25
26
  accessSecret: options.credentials.secretAccessKey, region: options.region ?? 'us-east-1' },
26
- { forcePathStyle: options.forcePathStyle });
27
+ { forcePathStyle: options.forcePathStyle, connectionTimeoutMs: options.connectionTimeoutMs,
28
+ requestTimeoutMs: options.requestTimeoutMs, maxAttempts: options.maxAttempts });
27
29
  this.backend = options.backend;
28
30
  this.capabilities = Object.freeze({ backend: this.backend, transactions: 'unsupported', changeStreams: 'unsupported',
29
31
  compositeGrouping: 'unsupported', objectMetrics: 'unsupported', diskResidentScans: 'unknown' });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The rejection of a request that reached the request deadline of the
3
+ * storage client a SmartBucket built (`SmartBucket.requestBounds`). The
4
+ * attempt in flight was destroyed and no further attempt starts. A request
5
+ * the provider had received completely before the deadline may still take
6
+ * effect.
7
+ */
8
+ export class ObjectStorageRequestDeadlineError extends Error {
9
+ public readonly deadlineMs: number;
10
+
11
+ constructor(deadlineMsArg: number) {
12
+ super(`The object storage request exceeded its deadline of ${deadlineMsArg} ms.`);
13
+ this.name = 'ObjectStorageRequestDeadlineError';
14
+ this.deadlineMs = deadlineMsArg;
15
+ }
16
+ }
@@ -4,6 +4,11 @@ import * as plugins from './plugins.js';
4
4
  import { Bucket } from './classes.bucket.js';
5
5
  import { normalizeStorageDescriptor } from './helpers.js';
6
6
  import { headBucketExists } from './internal.bucketrequests.js';
7
+ import {
8
+ bindRequestDeadline,
9
+ createBoundedClientConfig,
10
+ resolveRequestBounds,
11
+ } from './internal.requestbounds.js';
7
12
  import {
8
13
  beginOperationLifecycleClose,
9
14
  commitOperationLifecycleClient,
@@ -14,6 +19,8 @@ import {
14
19
  } from './internal.exactupload.capability.js';
15
20
  import type {
16
21
  IBucketRequestOptions,
22
+ IObjectStorageRequestBounds,
23
+ IObjectStorageRequestOptions,
17
24
  ISmartBucketReadinessOptions,
18
25
  TSmartBucketReadinessFailureReason,
19
26
  TSmartBucketReadinessResult,
@@ -31,6 +38,8 @@ export class SmartBucket {
31
38
 
32
39
  private currentStorageClient: plugins.s3.S3Client;
33
40
  private ownedStorageClient: plugins.s3.S3Client;
41
+ private readonly boundedStorageClient: plugins.s3.S3Client;
42
+ private readonly boundedStorageClientBounds: IObjectStorageRequestBounds;
34
43
  private lifecycleState: TSmartBucketLifecycleState = 'open';
35
44
  private lifecycleEpoch = 0;
36
45
  private activeReadinessControllers = new Set<AbortController>();
@@ -85,6 +94,18 @@ export class SmartBucket {
85
94
  }
86
95
  }
87
96
 
97
+ /**
98
+ * The bounds every request of the storage client this SmartBucket built
99
+ * runs within, or `undefined` while a client assigned through
100
+ * `storageClient` or `replaceStorageClient()` is in use: its requests are
101
+ * bounded only by its own configuration.
102
+ */
103
+ public get requestBounds(): IObjectStorageRequestBounds | undefined {
104
+ return this.currentStorageClient === this.boundedStorageClient
105
+ ? this.boundedStorageClientBounds
106
+ : undefined;
107
+ }
108
+
88
109
  /** @deprecated Use storageClient instead */
89
110
  public get s3Client(): plugins.s3.S3Client {
90
111
  return this.currentStorageClient;
@@ -93,18 +114,26 @@ export class SmartBucket {
93
114
  /**
94
115
  * the constructor of SmartBucket
95
116
  */
96
- constructor(configArg: plugins.tsclass.storage.IStorageDescriptor, clientOptions: { forcePathStyle?: boolean } = {}) {
117
+ constructor(
118
+ configArg: plugins.tsclass.storage.IStorageDescriptor,
119
+ clientOptions: { forcePathStyle?: boolean } & IObjectStorageRequestOptions = {},
120
+ ) {
97
121
  this.config = configArg;
98
122
 
99
123
  // Use the normalizer to handle various endpoint formats
100
124
  const { normalized } = normalizeStorageDescriptor(configArg);
125
+ const requestBounds = resolveRequestBounds(clientOptions);
101
126
 
102
127
  this.currentStorageClient = new plugins.s3.S3Client({
103
128
  endpoint: normalized.endpointUrl,
104
129
  region: normalized.region,
105
130
  credentials: normalized.credentials,
106
131
  forcePathStyle: clientOptions.forcePathStyle ?? normalized.forcePathStyle,
132
+ ...createBoundedClientConfig(requestBounds),
107
133
  });
134
+ bindRequestDeadline(this.currentStorageClient, requestBounds);
135
+ this.boundedStorageClient = this.currentStorageClient;
136
+ this.boundedStorageClientBounds = requestBounds;
108
137
  this.ownedStorageClient = this.currentStorageClient;
109
138
  registerOperationLifecycle(this, this.currentStorageClient);
110
139
  }
@@ -21,6 +21,22 @@ export const reducePathDescriptorToPath = async (pathDescriptorArg: interfaces.I
21
21
  return returnPath;
22
22
  }
23
23
 
24
+ /**
25
+ * Validates a caller's cancellation signal: a genuine `AbortSignal`.
26
+ */
27
+ export const requireOptionalAbortSignal = (
28
+ valueArg: unknown,
29
+ labelArg: string,
30
+ ): AbortSignal | undefined => {
31
+ if (valueArg === undefined) {
32
+ return undefined;
33
+ }
34
+ if (plugins.nodeUtil.types.isProxy(valueArg) || !(valueArg instanceof AbortSignal)) {
35
+ throw new TypeError(`${labelArg} must be an AbortSignal.`);
36
+ }
37
+ return valueArg;
38
+ };
39
+
24
40
  // Storage Descriptor Normalization
25
41
  export interface IStorageWarning {
26
42
  code: string;
@@ -3,6 +3,7 @@ export * from './classes.bucket.js';
3
3
  export * from './classes.exactuploaderror.js';
4
4
  export * from './classes.exactpathpurgeerror.js';
5
5
  export * from './classes.objectrangeerror.js';
6
+ export * from './classes.requestdeadlineerror.js';
6
7
  export * from './classes.directory.js';
7
8
  export * from './classes.file.js';
8
9
  export * from './classes.listcursor.js';
@@ -87,6 +87,25 @@ export interface IFastStatOptions extends IPathDecriptor {
87
87
  signal?: AbortSignal;
88
88
  }
89
89
 
90
+ export interface IFastPutOptions extends IPathDecriptor {
91
+ contents: string | Buffer;
92
+ overwrite?: boolean;
93
+ signal?: AbortSignal;
94
+ }
95
+
96
+ export interface IFastMoveOptions {
97
+ sourcePath: string;
98
+ destinationPath: string;
99
+ targetBucket?: Bucket;
100
+ overwrite?: boolean;
101
+ signal?: AbortSignal;
102
+ }
103
+
104
+ export interface IListAllObjectsOptions {
105
+ /** Optional caller-owned cancellation signal */
106
+ signal?: AbortSignal;
107
+ }
108
+
90
109
  export interface IDirectoryFastPutStreamOptions {
91
110
  path: string;
92
111
  stream: Readable;
@@ -103,6 +122,45 @@ export interface IBucketRequestOptions {
103
122
  signal?: AbortSignal;
104
123
  }
105
124
 
125
+ // ================================
126
+ // Request Bound Interfaces
127
+ // ================================
128
+
129
+ /**
130
+ * Bounds of every request sent by the storage client a SmartBucket builds.
131
+ * Each is a positive safe integer; an omitted one takes its default.
132
+ */
133
+ export interface IObjectStorageRequestOptions {
134
+ /** Time one attempt may take to connect its socket in milliseconds (default: 10000, maximum: 60000) */
135
+ connectionTimeoutMs?: number;
136
+ /**
137
+ * Time from the start of one attempt to its response headers in
138
+ * milliseconds, connecting and uploading the request body included
139
+ * (default: 60000, maximum: 3600000)
140
+ */
141
+ requestTimeoutMs?: number;
142
+ /** Attempts of one request, the first one included (default: 3, maximum: 10) */
143
+ maxAttempts?: number;
144
+ }
145
+
146
+ /**
147
+ * The effective bounds of the storage client a SmartBucket built.
148
+ */
149
+ export interface IObjectStorageRequestBounds {
150
+ readonly connectionTimeoutMs: number;
151
+ readonly requestTimeoutMs: number;
152
+ readonly maxAttempts: number;
153
+ /** The longest wait of the SDK before a retry */
154
+ readonly maxRetryDelayMs: number;
155
+ /**
156
+ * `maxAttempts * requestTimeoutMs + (maxAttempts - 1) * maxRetryDelayMs`
157
+ * plus 1000 ms for the client's own work around the attempts: the client
158
+ * destroys a request still unsettled after this time, retries included,
159
+ * and starts no further attempt of it
160
+ */
161
+ readonly requestDeadlineMs: number;
162
+ }
163
+
106
164
  // ================================
107
165
  // SmartBucket Readiness Interfaces
108
166
  // ================================
@@ -1,4 +1,5 @@
1
1
  import * as plugins from './plugins.js';
2
+ import { requireOptionalAbortSignal } from './helpers.js';
2
3
  import type {
3
4
  IExactPathPurgeCapability,
4
5
  IExactUploadCapability,
@@ -541,6 +542,7 @@ export const beginBasicBucketOperation = (
541
542
  bucketRefArg: IBucketCapabilityRef,
542
543
  callerSignalArg?: AbortSignal,
543
544
  ): IBasicBucketOperation => {
545
+ const callerSignal = requireOptionalAbortSignal(callerSignalArg, 'Object storage request signal');
544
546
  const smartbucketRef = bucketRefArg.smartbucketRef;
545
547
  const lifecycleRecord = getLifecycleRecord(smartbucketRef);
546
548
  const binding: IExactCapabilityBinding = {
@@ -550,7 +552,7 @@ export const beginBasicBucketOperation = (
550
552
  storageClient: lifecycleRecord.storageClient,
551
553
  lifecycleGeneration: lifecycleRecord.generation,
552
554
  };
553
- return beginOperation(binding, callerSignalArg);
555
+ return beginOperation(binding, callerSignal);
554
556
  };
555
557
 
556
558
  export const beginExactUploadProbeOperation = (