@aws-sdk/lib-transfer-manager 0.0.0-stage → 0.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 (60) hide show
  1. package/README.md +303 -2
  2. package/dist-cjs/index.js +2 -0
  3. package/dist-cjs/submodules/transfer-manager/index.js +3166 -0
  4. package/dist-cjs/submodules/worker/index.js +579 -0
  5. package/dist-es/index.js +1 -0
  6. package/dist-es/submodules/transfer-manager/S3TransferManager.js +2229 -0
  7. package/dist-es/submodules/transfer-manager/chunker.js +115 -0
  8. package/dist-es/submodules/transfer-manager/directory-transfer-utils.js +102 -0
  9. package/dist-es/submodules/transfer-manager/event-listener-types.js +1 -0
  10. package/dist-es/submodules/transfer-manager/file-manager.js +70 -0
  11. package/dist-es/submodules/transfer-manager/index.browser.js +11 -0
  12. package/dist-es/submodules/transfer-manager/index.js +11 -0
  13. package/dist-es/submodules/transfer-manager/join-streams.browser.js +101 -0
  14. package/dist-es/submodules/transfer-manager/join-streams.js +105 -0
  15. package/dist-es/submodules/transfer-manager/log-level.js +30 -0
  16. package/dist-es/submodules/transfer-manager/ordered-part-queue.js +57 -0
  17. package/dist-es/submodules/transfer-manager/types.js +1 -0
  18. package/dist-es/submodules/transfer-manager/worker-http-handler.browser.js +25 -0
  19. package/dist-es/submodules/transfer-manager/worker-http-handler.js +453 -0
  20. package/dist-es/submodules/worker/http-request-worker.js +578 -0
  21. package/dist-es/submodules/worker/index.js +1 -0
  22. package/dist-types/index.d.ts +2 -0
  23. package/dist-types/submodules/transfer-manager/S3TransferManager.d.ts +60 -0
  24. package/dist-types/submodules/transfer-manager/chunker.d.ts +7 -0
  25. package/dist-types/submodules/transfer-manager/directory-transfer-utils.d.ts +90 -0
  26. package/dist-types/submodules/transfer-manager/event-listener-types.d.ts +54 -0
  27. package/dist-types/submodules/transfer-manager/file-manager.d.ts +51 -0
  28. package/dist-types/submodules/transfer-manager/index.browser.d.ts +10 -0
  29. package/dist-types/submodules/transfer-manager/index.d.ts +10 -0
  30. package/dist-types/submodules/transfer-manager/join-streams.browser.d.ts +21 -0
  31. package/dist-types/submodules/transfer-manager/join-streams.d.ts +26 -0
  32. package/dist-types/submodules/transfer-manager/log-level.d.ts +13 -0
  33. package/dist-types/submodules/transfer-manager/ordered-part-queue.d.ts +73 -0
  34. package/dist-types/submodules/transfer-manager/types.d.ts +473 -0
  35. package/dist-types/submodules/transfer-manager/worker-http-handler.browser.d.ts +20 -0
  36. package/dist-types/submodules/transfer-manager/worker-http-handler.d.ts +410 -0
  37. package/dist-types/submodules/worker/http-request-worker.d.ts +1 -0
  38. package/dist-types/submodules/worker/index.d.ts +1 -0
  39. package/dist-types/ts3.4/index.d.ts +2 -0
  40. package/dist-types/ts3.4/submodules/transfer-manager/S3TransferManager.d.ts +38 -0
  41. package/dist-types/ts3.4/submodules/transfer-manager/chunker.d.ts +10 -0
  42. package/dist-types/ts3.4/submodules/transfer-manager/directory-transfer-utils.d.ts +23 -0
  43. package/dist-types/ts3.4/submodules/transfer-manager/event-listener-types.d.ts +12 -0
  44. package/dist-types/ts3.4/submodules/transfer-manager/file-manager.d.ts +9 -0
  45. package/dist-types/ts3.4/submodules/transfer-manager/index.browser.d.ts +3 -0
  46. package/dist-types/ts3.4/submodules/transfer-manager/index.d.ts +3 -0
  47. package/dist-types/ts3.4/submodules/transfer-manager/join-streams.browser.d.ts +13 -0
  48. package/dist-types/ts3.4/submodules/transfer-manager/join-streams.d.ts +13 -0
  49. package/dist-types/ts3.4/submodules/transfer-manager/log-level.d.ts +13 -0
  50. package/dist-types/ts3.4/submodules/transfer-manager/ordered-part-queue.d.ts +18 -0
  51. package/dist-types/ts3.4/submodules/transfer-manager/types.d.ts +184 -0
  52. package/dist-types/ts3.4/submodules/transfer-manager/worker-http-handler.browser.d.ts +17 -0
  53. package/dist-types/ts3.4/submodules/transfer-manager/worker-http-handler.d.ts +241 -0
  54. package/dist-types/ts3.4/submodules/worker/http-request-worker.d.ts +1 -0
  55. package/dist-types/ts3.4/submodules/worker/index.d.ts +1 -0
  56. package/package.json +108 -4
  57. package/transfer-manager.d.ts +5 -0
  58. package/transfer-manager.js +5 -0
  59. package/worker.d.ts +5 -0
  60. package/worker.js +5 -0
@@ -0,0 +1,453 @@
1
+ import { HttpResponse } from "@smithy/core/protocols";
2
+ import { NodeHttpHandler } from "@smithy/node-http-handler";
3
+ import { existsSync } from "node:fs";
4
+ import { cpus } from "node:os";
5
+ import * as path from "node:path";
6
+ import { Readable } from "node:stream";
7
+ import { Worker } from "node:worker_threads";
8
+ function buildDownloadError(msg) {
9
+ const error = new Error(msg.error);
10
+ if (msg.name)
11
+ error.name = msg.name;
12
+ if (msg.code)
13
+ error.code = msg.code;
14
+ return error;
15
+ }
16
+ export function createEmptyReadable() {
17
+ return new Readable({
18
+ read() {
19
+ this.push(null);
20
+ },
21
+ });
22
+ }
23
+ export function defaultWorkerCount() {
24
+ return cpus().length;
25
+ }
26
+ export class WorkerHttpHandler {
27
+ workers = [];
28
+ inflightRequests = new Map();
29
+ inflightDownloads = new Map();
30
+ inflightStreamDownloads = new Map();
31
+ nextRequestId = 0;
32
+ workerInflightCounts = [];
33
+ workerThreadCount;
34
+ maxConcurrentUploads;
35
+ maxConcurrentDownloads;
36
+ useODirect;
37
+ logger;
38
+ initialized = false;
39
+ initPromise;
40
+ fallbackHandler;
41
+ completedDownloads = new Map();
42
+ completedTransfers = new Map();
43
+ metadata = { handlerProtocol: "http/1.1" };
44
+ constructor(options) {
45
+ this.workerThreadCount = options?.workerThreadCount ?? defaultWorkerCount();
46
+ this.maxConcurrentUploads = options?.maxConcurrentUploads ?? 32;
47
+ this.maxConcurrentDownloads = options?.maxConcurrentDownloads ?? 32;
48
+ this.useODirect = options?.useODirect ?? false;
49
+ this.logger = options?.logger;
50
+ this.fallbackHandler = new NodeHttpHandler();
51
+ }
52
+ getDownloadResult(token) {
53
+ const result = this.completedDownloads.get(token);
54
+ if (result) {
55
+ this.completedDownloads.delete(token);
56
+ }
57
+ return result;
58
+ }
59
+ getStreamDownloadResult(token) {
60
+ const result = this.completedTransfers.get(token);
61
+ if (result) {
62
+ this.completedTransfers.delete(token);
63
+ }
64
+ return result;
65
+ }
66
+ resolveWorkerConfig() {
67
+ const submodulePath = path.join(__dirname, "..", "worker", "index.js");
68
+ if (existsSync(submodulePath)) {
69
+ return { workerPath: submodulePath };
70
+ }
71
+ const tsPath = path.join(__dirname, "..", "worker", "index.ts");
72
+ if (existsSync(tsPath)) {
73
+ return { workerPath: tsPath, workerOptions: { execArgv: ["--require", "tsx/cjs"] } };
74
+ }
75
+ return { workerPath: submodulePath };
76
+ }
77
+ ensureInitialized() {
78
+ if (this.initialized)
79
+ return Promise.resolve();
80
+ if (this.initPromise)
81
+ return this.initPromise;
82
+ this.initPromise = new Promise((resolve, reject) => {
83
+ const { workerPath, workerOptions } = this.resolveWorkerConfig();
84
+ let readyCount = 0;
85
+ let errorCount = 0;
86
+ let settled = false;
87
+ this.workerInflightCounts = new Array(this.workerThreadCount).fill(0);
88
+ for (let i = 0; i < this.workerThreadCount; i++) {
89
+ const worker = new Worker(workerPath, workerOptions);
90
+ this.workers.push(worker);
91
+ const workerIndex = i;
92
+ worker.on("message", (msg) => {
93
+ if (msg.type === "ready") {
94
+ worker.postMessage({
95
+ type: "config",
96
+ maxSockets: Math.max(50, Math.ceil(Math.max(this.maxConcurrentUploads, this.maxConcurrentDownloads) / this.workerThreadCount)),
97
+ useODirect: this.useODirect,
98
+ });
99
+ readyCount++;
100
+ if (!settled && readyCount === this.workerThreadCount) {
101
+ settled = true;
102
+ this.initialized = true;
103
+ resolve();
104
+ }
105
+ return;
106
+ }
107
+ if (msg.type === "workerLog") {
108
+ this.logger?.[msg.level]?.(`[S3TransferManager:worker] ${msg.message}`);
109
+ return;
110
+ }
111
+ if (msg.type === "httpResponse") {
112
+ const pending = this.inflightRequests.get(msg.id);
113
+ this.inflightRequests.delete(msg.id);
114
+ if (pending) {
115
+ this.workerInflightCounts[pending.workerIndex]--;
116
+ pending.resolve({
117
+ response: new HttpResponse({
118
+ statusCode: msg.response.statusCode,
119
+ headers: msg.response.headers,
120
+ body: msg.response.body,
121
+ }),
122
+ });
123
+ }
124
+ return;
125
+ }
126
+ if (msg.type === "httpError") {
127
+ const pending = this.inflightRequests.get(msg.id);
128
+ this.inflightRequests.delete(msg.id);
129
+ if (pending) {
130
+ this.workerInflightCounts[pending.workerIndex]--;
131
+ const error = Object.assign(new Error(msg.error), {
132
+ ...(msg.code && { code: msg.code }),
133
+ ...(msg.name && { name: msg.name }),
134
+ });
135
+ pending.reject(error);
136
+ }
137
+ return;
138
+ }
139
+ if (msg.type === "httpDownloadResult") {
140
+ const pending = this.inflightDownloads.get(msg.id);
141
+ this.inflightDownloads.delete(msg.id);
142
+ if (pending) {
143
+ this.workerInflightCounts[pending.workerIndex]--;
144
+ pending.resolve({
145
+ statusCode: msg.statusCode,
146
+ headers: msg.headers,
147
+ bytesWritten: msg.bytesWritten,
148
+ checksum: msg.checksum,
149
+ });
150
+ }
151
+ return;
152
+ }
153
+ if (msg.type === "httpDownloadError") {
154
+ const pending = this.inflightDownloads.get(msg.id);
155
+ this.inflightDownloads.delete(msg.id);
156
+ if (pending) {
157
+ this.workerInflightCounts[pending.workerIndex]--;
158
+ pending.reject(buildDownloadError(msg));
159
+ return;
160
+ }
161
+ const pendingTransfer = this.inflightStreamDownloads.get(msg.id);
162
+ this.inflightStreamDownloads.delete(msg.id);
163
+ if (pendingTransfer) {
164
+ this.workerInflightCounts[pendingTransfer.workerIndex]--;
165
+ pendingTransfer.reject(buildDownloadError(msg));
166
+ }
167
+ return;
168
+ }
169
+ if (msg.type === "httpDownloadHttpError") {
170
+ const pending = this.inflightDownloads.get(msg.id);
171
+ this.inflightDownloads.delete(msg.id);
172
+ if (pending) {
173
+ this.workerInflightCounts[pending.workerIndex]--;
174
+ pending.resolve({
175
+ statusCode: msg.statusCode,
176
+ headers: msg.headers,
177
+ bytesWritten: 0,
178
+ errorBody: msg.body,
179
+ });
180
+ return;
181
+ }
182
+ const pendingTransfer = this.inflightStreamDownloads.get(msg.id);
183
+ this.inflightStreamDownloads.delete(msg.id);
184
+ if (pendingTransfer) {
185
+ this.workerInflightCounts[pendingTransfer.workerIndex]--;
186
+ pendingTransfer.resolve({
187
+ rangeIndex: -1,
188
+ buffer: new ArrayBuffer(0),
189
+ byteLength: 0,
190
+ statusCode: msg.statusCode,
191
+ headers: msg.headers,
192
+ errorBody: msg.body,
193
+ });
194
+ }
195
+ return;
196
+ }
197
+ if (msg.type === "httpDownloadStreamResult") {
198
+ const pending = this.inflightStreamDownloads.get(msg.id);
199
+ this.inflightStreamDownloads.delete(msg.id);
200
+ if (pending) {
201
+ this.workerInflightCounts[pending.workerIndex]--;
202
+ pending.resolve({
203
+ rangeIndex: msg.rangeIndex,
204
+ buffer: msg.buffer,
205
+ byteLength: msg.byteLength,
206
+ statusCode: msg.statusCode,
207
+ headers: msg.headers,
208
+ checksum: msg.checksum,
209
+ });
210
+ }
211
+ return;
212
+ }
213
+ });
214
+ worker.on("error", (err) => {
215
+ if (!settled) {
216
+ errorCount++;
217
+ if (errorCount + readyCount === this.workerThreadCount) {
218
+ settled = true;
219
+ this.initPromise = undefined;
220
+ reject(new Error(`Failed to initialize worker threads: ${err.message}`));
221
+ }
222
+ }
223
+ for (const [id, pending] of this.inflightRequests) {
224
+ if (pending.workerIndex === workerIndex) {
225
+ pending.reject(err);
226
+ this.inflightRequests.delete(id);
227
+ }
228
+ }
229
+ for (const [id, pending] of this.inflightDownloads) {
230
+ if (pending.workerIndex === workerIndex) {
231
+ pending.reject(err);
232
+ this.inflightDownloads.delete(id);
233
+ }
234
+ }
235
+ for (const [id, pending] of this.inflightStreamDownloads) {
236
+ if (pending.workerIndex === workerIndex) {
237
+ pending.reject(err);
238
+ this.inflightStreamDownloads.delete(id);
239
+ }
240
+ }
241
+ this.workerInflightCounts[workerIndex] = Infinity;
242
+ });
243
+ }
244
+ });
245
+ return this.initPromise;
246
+ }
247
+ pickWorkerIndex() {
248
+ let minIndex = 0;
249
+ for (let i = 1; i < this.workerInflightCounts.length; i++) {
250
+ if (this.workerInflightCounts[i] < this.workerInflightCounts[minIndex]) {
251
+ minIndex = i;
252
+ }
253
+ }
254
+ return minIndex;
255
+ }
256
+ dispatchToWorker(id, message) {
257
+ const workerIndex = this.pickWorkerIndex();
258
+ this.workerInflightCounts[workerIndex]++;
259
+ return new Promise((resolve, reject) => {
260
+ this.inflightRequests.set(id, { resolve, reject, workerIndex });
261
+ this.workers[workerIndex].postMessage(message);
262
+ });
263
+ }
264
+ dispatchDownloadToWorker(id, message) {
265
+ const workerIndex = this.pickWorkerIndex();
266
+ this.workerInflightCounts[workerIndex]++;
267
+ return new Promise((resolve, reject) => {
268
+ this.inflightDownloads.set(id, { resolve, reject, workerIndex });
269
+ this.workers[workerIndex].postMessage(message);
270
+ });
271
+ }
272
+ extractPartNumber(request) {
273
+ const partNumber = request.query?.partNumber;
274
+ if (partNumber) {
275
+ return Number(Array.isArray(partNumber) ? partNumber[0] : partNumber);
276
+ }
277
+ return undefined;
278
+ }
279
+ returnBuffer(buffer, workerIndex) {
280
+ const targetIndex = workerIndex ?? this.nextRequestId % this.workers.length;
281
+ const message = { type: "returnBuffer", buffer };
282
+ this.workers[targetIndex]?.postMessage(message, [buffer]);
283
+ }
284
+ async handle(request, options) {
285
+ const downloadDataToFile = options?.downloadDataToFile;
286
+ if (downloadDataToFile) {
287
+ await this.ensureInitialized();
288
+ const id = this.nextRequestId++;
289
+ const serializedRequest = {
290
+ method: request.method,
291
+ protocol: request.protocol,
292
+ hostname: request.hostname,
293
+ port: request.port,
294
+ path: request.path,
295
+ query: request.query,
296
+ headers: request.headers,
297
+ };
298
+ const message = {
299
+ type: "httpDownloadToFile",
300
+ id,
301
+ request: serializedRequest,
302
+ filePath: downloadDataToFile.filePath,
303
+ offset: downloadDataToFile.offset,
304
+ expectedLength: downloadDataToFile.expectedLength,
305
+ checksumAlgorithm: downloadDataToFile.checksumAlgorithm,
306
+ };
307
+ const result = await this.dispatchDownloadToWorker(id, message);
308
+ if (result.errorBody !== undefined) {
309
+ return {
310
+ response: new HttpResponse({
311
+ statusCode: result.statusCode,
312
+ headers: result.headers,
313
+ body: Readable.from([Buffer.from(result.errorBody)]),
314
+ }),
315
+ };
316
+ }
317
+ if (downloadDataToFile.resultToken) {
318
+ this.completedDownloads.set(downloadDataToFile.resultToken, {
319
+ bytesWritten: result.bytesWritten,
320
+ checksum: result.checksum,
321
+ });
322
+ }
323
+ const responseHeaders = {};
324
+ for (const [key, value] of Object.entries(result.headers)) {
325
+ if (!key.startsWith("x-amz-checksum-")) {
326
+ responseHeaders[key] = value;
327
+ }
328
+ }
329
+ return {
330
+ response: new HttpResponse({
331
+ statusCode: result.statusCode,
332
+ headers: responseHeaders,
333
+ body: createEmptyReadable(),
334
+ }),
335
+ };
336
+ }
337
+ const downloadStream = options?.downloadStream;
338
+ if (downloadStream) {
339
+ await this.ensureInitialized();
340
+ const id = this.nextRequestId++;
341
+ const serializedRequest = {
342
+ method: request.method,
343
+ protocol: request.protocol,
344
+ hostname: request.hostname,
345
+ port: request.port,
346
+ path: request.path,
347
+ query: request.query,
348
+ headers: request.headers,
349
+ };
350
+ const message = {
351
+ type: "httpDownloadStream",
352
+ id,
353
+ request: serializedRequest,
354
+ expectedSize: downloadStream.expectedSize,
355
+ rangeIndex: downloadStream.rangeIndex,
356
+ checksumAlgorithm: downloadStream.checksumAlgorithm,
357
+ };
358
+ const workerIndex = this.pickWorkerIndex();
359
+ this.workerInflightCounts[workerIndex]++;
360
+ const result = await new Promise((resolve, reject) => {
361
+ this.inflightStreamDownloads.set(id, { resolve, reject, workerIndex });
362
+ this.workers[workerIndex].postMessage(message);
363
+ });
364
+ if (result.errorBody !== undefined) {
365
+ return {
366
+ response: new HttpResponse({
367
+ statusCode: result.statusCode,
368
+ headers: result.headers,
369
+ body: Readable.from([Buffer.from(result.errorBody)]),
370
+ }),
371
+ };
372
+ }
373
+ const responseHeaders = {};
374
+ for (const [key, value] of Object.entries(result.headers)) {
375
+ if (!key.startsWith("x-amz-checksum-")) {
376
+ responseHeaders[key] = value;
377
+ }
378
+ }
379
+ if (downloadStream.resultToken) {
380
+ this.completedTransfers.set(downloadStream.resultToken, result);
381
+ }
382
+ return {
383
+ response: new HttpResponse({
384
+ statusCode: result.statusCode,
385
+ headers: responseHeaders,
386
+ body: createEmptyReadable(),
387
+ }),
388
+ };
389
+ }
390
+ const dataSource = options?.dataSource;
391
+ const partNumber = this.extractPartNumber(request);
392
+ if (!dataSource || !partNumber) {
393
+ return this.fallbackHandler.handle(request, options);
394
+ }
395
+ await this.ensureInitialized();
396
+ const id = this.nextRequestId++;
397
+ const serializedRequest = {
398
+ method: request.method,
399
+ protocol: request.protocol,
400
+ hostname: request.hostname,
401
+ port: request.port,
402
+ path: request.path,
403
+ query: request.query,
404
+ headers: request.headers,
405
+ };
406
+ if (dataSource.type === "file") {
407
+ const { filePath, partSize, totalFileSize, startOffset = 0, checksumAlgorithm, checksumHeader } = dataSource;
408
+ const partOffset = (partNumber - 1) * partSize;
409
+ const length = Math.min(partSize, totalFileSize - partOffset);
410
+ const offset = startOffset + partOffset;
411
+ const message = {
412
+ type: "httpRequestFromFile",
413
+ id,
414
+ request: serializedRequest,
415
+ filePath,
416
+ offset,
417
+ length,
418
+ checksumAlgorithm,
419
+ checksumHeader,
420
+ };
421
+ return this.dispatchToWorker(id, message);
422
+ }
423
+ const { sharedBuffer, startOffset = 0, partSize, totalSize, checksumAlgorithm, checksumHeader } = dataSource;
424
+ const partOffset = (partNumber - 1) * partSize;
425
+ const offset = startOffset + partOffset;
426
+ const length = Math.min(partSize, totalSize - partOffset);
427
+ const message = {
428
+ type: "httpRequestFromRAM",
429
+ id,
430
+ request: serializedRequest,
431
+ sharedBuffer,
432
+ offset,
433
+ length,
434
+ checksumAlgorithm,
435
+ checksumHeader,
436
+ };
437
+ return this.dispatchToWorker(id, message);
438
+ }
439
+ updateHttpClientConfig(_key, _value) { }
440
+ httpHandlerConfigs() {
441
+ return {};
442
+ }
443
+ destroy() {
444
+ for (const worker of this.workers) {
445
+ worker.postMessage({ type: "done" });
446
+ }
447
+ this.workers = [];
448
+ this.workerInflightCounts = [];
449
+ this.initialized = false;
450
+ this.initPromise = undefined;
451
+ this.fallbackHandler.destroy();
452
+ }
453
+ }