@lossless.org/client 1.23.2 → 2.0.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/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.directory.js +9 -1
- 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 +6 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/objectstorage/classes.bucket.ts +80 -56
- package/ts/objectstorage/classes.connection.ts +4 -2
- package/ts/objectstorage/classes.directory.ts +8 -0
- 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
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { ObjectStorageRequestDeadlineError } from './classes.requestdeadlineerror.js';
|
|
3
|
+
import type {
|
|
4
|
+
IObjectStorageRequestBounds,
|
|
5
|
+
IObjectStorageRequestOptions,
|
|
6
|
+
} from './interfaces.js';
|
|
7
|
+
|
|
8
|
+
const requestOptionLimits = {
|
|
9
|
+
connectionTimeoutMs: { defaultValue: 10_000, maximum: 60_000 },
|
|
10
|
+
requestTimeoutMs: { defaultValue: 60_000, maximum: 3_600_000 },
|
|
11
|
+
maxAttempts: { defaultValue: 3, maximum: 10 },
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The longest wait of the SDK's standard retry strategy before a retry
|
|
16
|
+
* (`MAXIMUM_RETRY_DELAY` of `@smithy/core`, which caps a retry-after hint
|
|
17
|
+
* as well).
|
|
18
|
+
*/
|
|
19
|
+
const maxRetryDelayMs = 20_000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Time the deadline grants beyond the attempts and retry waits for the
|
|
23
|
+
* client's own work around them (serializing, signing, checksums), so an
|
|
24
|
+
* attempt's own timeout decides its failure and the deadline only ends a
|
|
25
|
+
* request the attempt bounds do not.
|
|
26
|
+
*/
|
|
27
|
+
const requestDeadlineAllowanceMs = 1_000;
|
|
28
|
+
|
|
29
|
+
type TRequestOptionName = keyof typeof requestOptionLimits;
|
|
30
|
+
|
|
31
|
+
const requireRequestOption = (
|
|
32
|
+
optionsArg: IObjectStorageRequestOptions,
|
|
33
|
+
nameArg: TRequestOptionName,
|
|
34
|
+
): number => {
|
|
35
|
+
const value: unknown = optionsArg[nameArg];
|
|
36
|
+
const { defaultValue, maximum } = requestOptionLimits[nameArg];
|
|
37
|
+
if (value === undefined) {
|
|
38
|
+
return defaultValue;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
41
|
+
throw new TypeError(
|
|
42
|
+
`Object storage ${nameArg} must be a positive safe integer no greater than ${maximum}.`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validates the request options of a storage client and derives its bounds.
|
|
50
|
+
*/
|
|
51
|
+
export const resolveRequestBounds = (
|
|
52
|
+
optionsArg: IObjectStorageRequestOptions,
|
|
53
|
+
): IObjectStorageRequestBounds => {
|
|
54
|
+
const connectionTimeoutMs = requireRequestOption(optionsArg, 'connectionTimeoutMs');
|
|
55
|
+
const requestTimeoutMs = requireRequestOption(optionsArg, 'requestTimeoutMs');
|
|
56
|
+
const maxAttempts = requireRequestOption(optionsArg, 'maxAttempts');
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
connectionTimeoutMs,
|
|
59
|
+
requestTimeoutMs,
|
|
60
|
+
maxAttempts,
|
|
61
|
+
maxRetryDelayMs,
|
|
62
|
+
requestDeadlineMs: maxAttempts * requestTimeoutMs
|
|
63
|
+
+ (maxAttempts - 1) * maxRetryDelayMs
|
|
64
|
+
+ requestDeadlineAllowanceMs,
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The S3 client configuration that applies the bounds to every attempt:
|
|
70
|
+
* `@smithy/node-http-handler` destroys an attempt whose socket does not
|
|
71
|
+
* connect within `connectionTimeout` and, with `throwOnRequestTimeout`, one
|
|
72
|
+
* whose response headers did not arrive within `requestTimeout` of its start.
|
|
73
|
+
* `maxAttempts` and the standard retry mode are set explicitly, so
|
|
74
|
+
* `AWS_MAX_ATTEMPTS`, `AWS_RETRY_MODE` and shared config files cannot change
|
|
75
|
+
* them.
|
|
76
|
+
*/
|
|
77
|
+
export const createBoundedClientConfig = (boundsArg: IObjectStorageRequestBounds) => ({
|
|
78
|
+
maxAttempts: boundsArg.maxAttempts,
|
|
79
|
+
retryMode: 'standard',
|
|
80
|
+
requestHandler: {
|
|
81
|
+
connectionTimeout: boundsArg.connectionTimeoutMs,
|
|
82
|
+
requestTimeout: boundsArg.requestTimeoutMs,
|
|
83
|
+
throwOnRequestTimeout: true,
|
|
84
|
+
},
|
|
85
|
+
}) satisfies Partial<plugins.s3.S3ClientConfig>;
|
|
86
|
+
|
|
87
|
+
type TSendOptions = { abortSignal?: AbortSignal } & Record<string, unknown>;
|
|
88
|
+
type TSendCallback = (errorArg: unknown, outputArg?: unknown) => void;
|
|
89
|
+
type TUnboundedSend = (commandArg: unknown, optionsArg?: TSendOptions) => Promise<unknown>;
|
|
90
|
+
|
|
91
|
+
const readStreamingBody = (outputArg: unknown): plugins.stream.Readable | undefined => {
|
|
92
|
+
if (typeof outputArg !== 'object' || outputArg === null) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const body = (outputArg as { Body?: unknown }).Body;
|
|
96
|
+
return body instanceof plugins.stream.Readable ? body : undefined;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Bounds every `send()` of a storage client by `requestDeadlineMs`.
|
|
101
|
+
*
|
|
102
|
+
* Each request gets its own abort signal, which the caller's `abortSignal`
|
|
103
|
+
* and the deadline abort. The HTTP handler destroys the attempt in flight
|
|
104
|
+
* when it aborts, and refuses every later attempt of the request, so no
|
|
105
|
+
* attempt starts or sends after the deadline. The request rejects at the
|
|
106
|
+
* deadline with `ObjectStorageRequestDeadlineError`; the handler fails the
|
|
107
|
+
* aborted attempt with an `AbortError`, which the retry strategy never
|
|
108
|
+
* retries. A caller's abort still reaches a response body the request
|
|
109
|
+
* resolved with until that body closes.
|
|
110
|
+
*/
|
|
111
|
+
export const bindRequestDeadline = (
|
|
112
|
+
clientArg: plugins.s3.S3Client,
|
|
113
|
+
boundsArg: IObjectStorageRequestBounds,
|
|
114
|
+
): void => {
|
|
115
|
+
const sendWithoutDeadline = clientArg.send.bind(clientArg) as unknown as TUnboundedSend;
|
|
116
|
+
const sendWithinDeadline = (
|
|
117
|
+
commandArg: unknown,
|
|
118
|
+
optionsArg: TSendOptions | undefined,
|
|
119
|
+
): Promise<unknown> => new Promise((resolve, reject) => {
|
|
120
|
+
const callerSignal = optionsArg?.abortSignal;
|
|
121
|
+
const requestAbortController = new AbortController();
|
|
122
|
+
const forwardCallerAbort = (): void => {
|
|
123
|
+
requestAbortController.abort(callerSignal?.reason);
|
|
124
|
+
};
|
|
125
|
+
const releaseCallerSignal = (): void => {
|
|
126
|
+
callerSignal?.removeEventListener('abort', forwardCallerAbort);
|
|
127
|
+
};
|
|
128
|
+
if (callerSignal?.aborted) {
|
|
129
|
+
forwardCallerAbort();
|
|
130
|
+
} else {
|
|
131
|
+
callerSignal?.addEventListener('abort', forwardCallerAbort, { once: true });
|
|
132
|
+
}
|
|
133
|
+
let settled = false;
|
|
134
|
+
const deadlineTimer = setTimeout(() => {
|
|
135
|
+
settled = true;
|
|
136
|
+
const deadlineError = new ObjectStorageRequestDeadlineError(boundsArg.requestDeadlineMs);
|
|
137
|
+
requestAbortController.abort(deadlineError);
|
|
138
|
+
reject(deadlineError);
|
|
139
|
+
}, boundsArg.requestDeadlineMs);
|
|
140
|
+
sendWithoutDeadline(commandArg, {
|
|
141
|
+
...optionsArg,
|
|
142
|
+
abortSignal: requestAbortController.signal,
|
|
143
|
+
}).then(
|
|
144
|
+
(outputArg) => {
|
|
145
|
+
clearTimeout(deadlineTimer);
|
|
146
|
+
const body = readStreamingBody(outputArg);
|
|
147
|
+
if (settled) {
|
|
148
|
+
releaseCallerSignal();
|
|
149
|
+
body?.destroy();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
settled = true;
|
|
153
|
+
if (body && !body.destroyed) {
|
|
154
|
+
body.once('close', releaseCallerSignal);
|
|
155
|
+
} else {
|
|
156
|
+
releaseCallerSignal();
|
|
157
|
+
}
|
|
158
|
+
resolve(outputArg);
|
|
159
|
+
},
|
|
160
|
+
(errorArg: unknown) => {
|
|
161
|
+
clearTimeout(deadlineTimer);
|
|
162
|
+
releaseCallerSignal();
|
|
163
|
+
if (settled) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
settled = true;
|
|
167
|
+
reject(errorArg);
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
const boundedSend = (
|
|
172
|
+
commandArg: unknown,
|
|
173
|
+
optionsOrCallbackArg?: TSendOptions | TSendCallback,
|
|
174
|
+
callbackArg?: TSendCallback,
|
|
175
|
+
): Promise<unknown> | void => {
|
|
176
|
+
const callback = typeof optionsOrCallbackArg === 'function' ? optionsOrCallbackArg : callbackArg;
|
|
177
|
+
const options = typeof optionsOrCallbackArg === 'function' ? undefined : optionsOrCallbackArg;
|
|
178
|
+
const result = sendWithinDeadline(commandArg, options);
|
|
179
|
+
if (typeof callback !== 'function') {
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
// As the SDK's own callback form: a callback that throws must not become
|
|
183
|
+
// an unhandled rejection.
|
|
184
|
+
result.then(
|
|
185
|
+
(outputArg) => callback(null, outputArg),
|
|
186
|
+
(errorArg: unknown) => callback(errorArg),
|
|
187
|
+
).catch(() => {});
|
|
188
|
+
};
|
|
189
|
+
// S3Client.send() is generic over the SDK's Command type, which the
|
|
190
|
+
// optional peer does not export; the bounded send keeps its signature.
|
|
191
|
+
clientArg.send = boundedSend as typeof clientArg.send;
|
|
192
|
+
};
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
import * as crypto from 'node:crypto';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import * as stream from 'node:stream';
|
|
7
|
+
import * as nodeUtil from 'node:util';
|
|
7
8
|
|
|
8
|
-
export { crypto, path, stream };
|
|
9
|
+
export { crypto, nodeUtil, path, stream };
|
|
9
10
|
|
|
10
11
|
// @push.rocks scope
|
|
11
12
|
import * as smartmime from '@push.rocks/smartmime';
|