@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.
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/nosqldb/classes.persistence.d.ts +21 -1
- package/dist_ts/nosqldb/classes.persistence.js +23 -1
- package/dist_ts/nosqldb/classes.session.d.ts +8 -0
- package/dist_ts/nosqldb/classes.session.js +27 -2
- package/dist_ts/nosqldb/index.d.ts +1 -1
- package/dist_ts/nosqldb/index.js +2 -2
- package/dist_ts/objectstorage/classes.bucket.d.ts +11 -21
- package/dist_ts/objectstorage/classes.bucket.js +58 -29
- package/dist_ts/objectstorage/classes.connection.d.ts +2 -1
- package/dist_ts/objectstorage/classes.connection.js +3 -2
- package/dist_ts/objectstorage/classes.requestdeadlineerror.d.ts +11 -0
- package/dist_ts/objectstorage/classes.requestdeadlineerror.js +16 -0
- package/dist_ts/objectstorage/classes.smartbucket.d.ts +11 -2
- package/dist_ts/objectstorage/classes.smartbucket.js +20 -1
- package/dist_ts/objectstorage/helpers.d.ts +4 -0
- package/dist_ts/objectstorage/helpers.js +13 -1
- package/dist_ts/objectstorage/index.d.ts +1 -0
- package/dist_ts/objectstorage/index.js +2 -1
- package/dist_ts/objectstorage/interfaces.d.ts +49 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.js +4 -2
- package/dist_ts/objectstorage/internal.requestbounds.d.ts +37 -0
- package/dist_ts/objectstorage/internal.requestbounds.js +154 -0
- package/dist_ts/objectstorage/plugins.d.ts +2 -1
- package/dist_ts/objectstorage/plugins.js +3 -2
- package/package.json +2 -2
- package/readme.md +7 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.persistence.ts +30 -1
- package/ts/nosqldb/classes.session.ts +26 -0
- package/ts/nosqldb/index.ts +1 -1
- package/ts/objectstorage/classes.bucket.ts +80 -56
- package/ts/objectstorage/classes.connection.ts +4 -2
- package/ts/objectstorage/classes.requestdeadlineerror.ts +16 -0
- package/ts/objectstorage/classes.smartbucket.ts +30 -1
- package/ts/objectstorage/helpers.ts +16 -0
- package/ts/objectstorage/index.ts +1 -0
- package/ts/objectstorage/interfaces.ts +58 -0
- package/ts/objectstorage/internal.exactupload.capability.ts +3 -1
- package/ts/objectstorage/internal.requestbounds.ts +192 -0
- 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
|
}
|
package/ts/nosqldb/index.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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:
|
|
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(
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
1242
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
555
|
+
return beginOperation(binding, callerSignal);
|
|
554
556
|
};
|
|
555
557
|
|
|
556
558
|
export const beginExactUploadProbeOperation = (
|