@flowscripter/pluggable-io-framework 1.2.7 → 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.
Files changed (142) hide show
  1. package/README.md +47 -178
  2. package/dist/index.d.ts +11 -4
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +11 -4
  5. package/dist/src/concurrency/AsyncChannel.d.ts +9 -0
  6. package/dist/src/concurrency/AsyncChannel.d.ts.map +1 -0
  7. package/dist/src/concurrency/AsyncChannel.js +48 -0
  8. package/dist/src/{ConcurrencyLimiter.d.ts → concurrency/ConcurrencyLimiter.d.ts} +1 -12
  9. package/dist/src/concurrency/ConcurrencyLimiter.d.ts.map +1 -0
  10. package/dist/src/concurrency/ConcurrencyLimiter.js +64 -0
  11. package/dist/src/concurrency/mapAsyncIterableConcurrently.d.ts +13 -0
  12. package/dist/src/concurrency/mapAsyncIterableConcurrently.d.ts.map +1 -0
  13. package/dist/src/concurrency/mapAsyncIterableConcurrently.js +61 -0
  14. package/dist/src/decorators/locallyCached.d.ts +9 -10
  15. package/dist/src/decorators/locallyCached.d.ts.map +1 -1
  16. package/dist/src/decorators/locallyCached.js +23 -16
  17. package/dist/src/decorators/seekable.d.ts +4 -4
  18. package/dist/src/decorators/seekable.d.ts.map +1 -1
  19. package/dist/src/decorators/seekable.js +2 -2
  20. package/dist/src/registry/ProviderRegistry.d.ts +52 -0
  21. package/dist/src/registry/ProviderRegistry.d.ts.map +1 -0
  22. package/dist/src/registry/ProviderRegistry.js +148 -0
  23. package/dist/src/registry/detectProtocol.d.ts +8 -0
  24. package/dist/src/registry/detectProtocol.d.ts.map +1 -0
  25. package/dist/src/registry/detectProtocol.js +15 -0
  26. package/dist/src/registry/negotiateTransfer.d.ts +29 -0
  27. package/dist/src/registry/negotiateTransfer.d.ts.map +1 -0
  28. package/dist/src/registry/negotiateTransfer.js +69 -0
  29. package/dist/src/retry/RetryOptions.d.ts +19 -0
  30. package/dist/src/retry/RetryOptions.d.ts.map +1 -0
  31. package/dist/src/retry/RetryOptions.js +9 -0
  32. package/dist/src/{withRetry.d.ts → retry/withRetry.d.ts} +1 -5
  33. package/dist/src/retry/withRetry.d.ts.map +1 -0
  34. package/dist/src/{withRetry.js → retry/withRetry.js} +2 -5
  35. package/dist/src/transfer/EntryContext.d.ts +16 -0
  36. package/dist/src/transfer/EntryContext.d.ts.map +1 -0
  37. package/dist/src/transfer/EntryContext.js +1 -0
  38. package/dist/src/transfer/TransferOptions.d.ts +45 -0
  39. package/dist/src/transfer/TransferOptions.d.ts.map +1 -0
  40. package/dist/src/transfer/TransferOptions.js +1 -0
  41. package/dist/src/transfer/TransferResult.d.ts +9 -0
  42. package/dist/src/transfer/TransferResult.d.ts.map +1 -0
  43. package/dist/src/transfer/TransferResult.js +1 -0
  44. package/dist/src/transfer/aggregateResults.d.ts +4 -0
  45. package/dist/src/transfer/aggregateResults.d.ts.map +1 -0
  46. package/dist/src/transfer/aggregateResults.js +9 -0
  47. package/dist/src/transfer/copyMove.d.ts +23 -0
  48. package/dist/src/transfer/copyMove.d.ts.map +1 -0
  49. package/dist/src/transfer/copyMove.js +68 -0
  50. package/dist/src/transfer/leaseTransfer.d.ts +14 -0
  51. package/dist/src/transfer/leaseTransfer.d.ts.map +1 -0
  52. package/dist/src/transfer/leaseTransfer.js +74 -0
  53. package/dist/src/transfer/multipartTransfer.d.ts +23 -0
  54. package/dist/src/transfer/multipartTransfer.d.ts.map +1 -0
  55. package/dist/src/transfer/multipartTransfer.js +94 -0
  56. package/dist/src/transfer/negotiatePartSize.d.ts +10 -0
  57. package/dist/src/transfer/negotiatePartSize.d.ts.map +1 -0
  58. package/dist/src/transfer/negotiatePartSize.js +27 -0
  59. package/dist/src/transfer/patternTransfer.d.ts +12 -0
  60. package/dist/src/transfer/patternTransfer.d.ts.map +1 -0
  61. package/dist/src/transfer/patternTransfer.js +61 -0
  62. package/dist/src/transfer/rangeReadableMultipartReader.d.ts +8 -0
  63. package/dist/src/transfer/rangeReadableMultipartReader.d.ts.map +1 -0
  64. package/dist/src/transfer/rangeReadableMultipartReader.js +19 -0
  65. package/dist/src/transfer/recursiveTransfer.d.ts +13 -0
  66. package/dist/src/transfer/recursiveTransfer.d.ts.map +1 -0
  67. package/dist/src/transfer/recursiveTransfer.js +112 -0
  68. package/dist/src/transfer/streamTransfer.d.ts +32 -0
  69. package/dist/src/transfer/streamTransfer.d.ts.map +1 -0
  70. package/dist/src/transfer/streamTransfer.js +186 -0
  71. package/dist/src/transfer/transferEntry.d.ts +25 -0
  72. package/dist/src/transfer/transferEntry.d.ts.map +1 -0
  73. package/dist/src/transfer/transferEntry.js +136 -0
  74. package/dist/src/util/abort.d.ts +12 -0
  75. package/dist/src/util/abort.d.ts.map +1 -0
  76. package/dist/src/util/abort.js +39 -0
  77. package/dist/src/util/applyPayloadConverter.d.ts +9 -0
  78. package/dist/src/util/applyPayloadConverter.d.ts.map +1 -0
  79. package/dist/src/util/applyPayloadConverter.js +26 -0
  80. package/dist/src/util/describePath.d.ts +6 -0
  81. package/dist/src/util/describePath.d.ts.map +1 -0
  82. package/dist/src/util/describePath.js +9 -0
  83. package/dist/src/util/globToRegex.d.ts +8 -0
  84. package/dist/src/util/globToRegex.d.ts.map +1 -0
  85. package/dist/src/util/globToRegex.js +35 -0
  86. package/dist/src/util/itemLength.d.ts +4 -0
  87. package/dist/src/util/itemLength.d.ts.map +1 -0
  88. package/dist/src/util/itemLength.js +5 -0
  89. package/dist/src/util/keys.d.ts +8 -0
  90. package/dist/src/util/keys.d.ts.map +1 -0
  91. package/dist/src/util/keys.js +19 -0
  92. package/dist/src/util/withParentOperationId.d.ts +4 -0
  93. package/dist/src/util/withParentOperationId.d.ts.map +1 -0
  94. package/dist/src/util/withParentOperationId.js +12 -0
  95. package/index.ts +11 -4
  96. package/package.json +4 -3
  97. package/src/concurrency/AsyncChannel.ts +52 -0
  98. package/src/concurrency/ConcurrencyLimiter.ts +72 -0
  99. package/src/concurrency/mapAsyncIterableConcurrently.ts +66 -0
  100. package/src/decorators/locallyCached.ts +30 -22
  101. package/src/decorators/seekable.ts +7 -7
  102. package/src/registry/ProviderRegistry.ts +241 -0
  103. package/src/registry/detectProtocol.ts +16 -0
  104. package/src/registry/negotiateTransfer.ts +121 -0
  105. package/src/retry/RetryOptions.ts +26 -0
  106. package/src/{withRetry.ts → retry/withRetry.ts} +2 -12
  107. package/src/transfer/EntryContext.ts +17 -0
  108. package/src/transfer/TransferOptions.ts +45 -0
  109. package/src/transfer/TransferResult.ts +8 -0
  110. package/src/transfer/aggregateResults.ts +15 -0
  111. package/src/transfer/copyMove.ts +113 -0
  112. package/src/transfer/leaseTransfer.ts +93 -0
  113. package/src/transfer/multipartTransfer.ts +119 -0
  114. package/src/transfer/negotiatePartSize.ts +40 -0
  115. package/src/transfer/patternTransfer.ts +84 -0
  116. package/src/transfer/rangeReadableMultipartReader.ts +30 -0
  117. package/src/transfer/recursiveTransfer.ts +147 -0
  118. package/src/transfer/streamTransfer.ts +214 -0
  119. package/src/transfer/transferEntry.ts +185 -0
  120. package/src/util/abort.ts +42 -0
  121. package/src/util/applyPayloadConverter.ts +37 -0
  122. package/src/util/describePath.ts +12 -0
  123. package/src/util/globToRegex.ts +32 -0
  124. package/src/util/itemLength.ts +6 -0
  125. package/src/util/keys.ts +21 -0
  126. package/src/util/withParentOperationId.ts +17 -0
  127. package/dist/src/ConcurrencyLimiter.d.ts.map +0 -1
  128. package/dist/src/ConcurrencyLimiter.js +0 -161
  129. package/dist/src/ProviderRegistry.d.ts +0 -14
  130. package/dist/src/ProviderRegistry.d.ts.map +0 -1
  131. package/dist/src/ProviderRegistry.js +0 -22
  132. package/dist/src/chunkLength.d.ts +0 -3
  133. package/dist/src/chunkLength.d.ts.map +0 -1
  134. package/dist/src/chunkLength.js +0 -4
  135. package/dist/src/copyMove.d.ts +0 -46
  136. package/dist/src/copyMove.d.ts.map +0 -1
  137. package/dist/src/copyMove.js +0 -280
  138. package/dist/src/withRetry.d.ts.map +0 -1
  139. package/src/ConcurrencyLimiter.ts +0 -178
  140. package/src/ProviderRegistry.ts +0 -32
  141. package/src/chunkLength.ts +0 -5
  142. package/src/copyMove.ts +0 -458
@@ -0,0 +1,68 @@
1
+ import { PermanentIOError, } from "@flowscripter/pluggable-io-framework-api";
2
+ import { baseName, joinKey } from "../util/keys.js";
3
+ import { patternTransfer } from "./patternTransfer.js";
4
+ import { recursiveTransfer } from "./recursiveTransfer.js";
5
+ import { transferEntry } from "./transferEntry.js";
6
+ async function checkSourceVariant(source, key, expectContainer, variant) {
7
+ const properties = await source.getProperties(key);
8
+ if (properties.isContainer !== expectContainer) {
9
+ const actual = properties.isContainer ? "a container" : "an entry";
10
+ throw new PermanentIOError(`Source "${key}" is declared as ${variant} but is ${actual}`);
11
+ }
12
+ return properties;
13
+ }
14
+ async function transfer(source, sourceTarget, sink, destTarget, options, type) {
15
+ if (destTarget.kind === "pattern") {
16
+ throw new PermanentIOError("A destination cannot be a pattern");
17
+ }
18
+ if (sourceTarget.kind === "entry") {
19
+ const properties = await checkSourceVariant(source, sourceTarget.key, false, "an entry");
20
+ const destKey = destTarget.kind === "entry"
21
+ ? destTarget.key
22
+ : joinKey(sink, destTarget.key, baseName(sourceTarget.key));
23
+ return transferEntry({
24
+ source,
25
+ sourceKey: sourceTarget.key,
26
+ sink,
27
+ destKey,
28
+ properties,
29
+ options,
30
+ type,
31
+ deleteSource: type === "move",
32
+ });
33
+ }
34
+ if (destTarget.kind === "entry") {
35
+ throw new PermanentIOError(sourceTarget.kind === "container"
36
+ ? "Cannot transfer a container onto an entry"
37
+ : "Cannot transfer a pattern of entries onto a single entry");
38
+ }
39
+ if (sourceTarget.kind === "container") {
40
+ await checkSourceVariant(source, sourceTarget.key, true, "a container");
41
+ return recursiveTransfer(source, sourceTarget.key, sink, destTarget.key, options, type);
42
+ }
43
+ await checkSourceVariant(source, sourceTarget.containerKey, true, "a pattern container");
44
+ return patternTransfer(source, sourceTarget.containerKey, sourceTarget.pattern, sink, destTarget.key, options, type);
45
+ }
46
+ /**
47
+ * Copies `sourceTarget` on `source` to `destTarget` on `sink`:
48
+ * - entry -> entry writes exactly the destination key;
49
+ * - entry -> container writes `<container>/<source basename>`;
50
+ * - container -> container copies recursively with `cp -r` semantics;
51
+ * - pattern -> container copies each matching entry;
52
+ * - container or pattern -> entry, and any pattern destination, are rejected.
53
+ *
54
+ * Each entry uses `directCopy` when eligible (and `options.directTransfer`
55
+ * is not `false`), otherwise multipart, the lease path or plain streaming.
56
+ */
57
+ export function copy(source, sourceTarget, sink, destTarget, options = {}) {
58
+ return transfer(source, sourceTarget, sink, destTarget, options, "copy");
59
+ }
60
+ /**
61
+ * Moves `sourceTarget` on `source` to `destTarget` on `sink`, with the same
62
+ * target rules as {@link copy}. Non-direct moves copy and then delete the
63
+ * source. Unbounded sources are rejected, and a stopped move never deletes
64
+ * its source.
65
+ */
66
+ export function move(source, sourceTarget, sink, destTarget, options = {}) {
67
+ return transfer(source, sourceTarget, sink, destTarget, options, "move");
68
+ }
@@ -0,0 +1,14 @@
1
+ import type { BufferProvider, FillReadable, StreamHandle } from "@flowscripter/pluggable-io-framework-api";
2
+ import type { EntryContext, EntryOutcome } from "./EntryContext.ts";
3
+ export declare const DEFAULT_LEASE_DEPTH = 2;
4
+ /** The number of leases the engine keeps outstanding for `writable`. */
5
+ export declare function leaseDepth(writable: BufferProvider, requested: number | undefined): number;
6
+ /**
7
+ * Moves data by having the source fill buffers handed out by the sink:
8
+ * `acquire -> readInto -> commit`, with up to `depth` leases outstanding so
9
+ * the next read overlaps earlier commits. `readInto` calls are serialised
10
+ * and commits happen in acquisition order. Every lease not yet committed is
11
+ * released on abort, error or end of stream.
12
+ */
13
+ export declare function leaseTransfer(readable: StreamHandle & FillReadable, writable: StreamHandle & BufferProvider, depth: number, context: EntryContext): Promise<EntryOutcome>;
14
+ //# sourceMappingURL=leaseTransfer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leaseTransfer.d.ts","sourceRoot":"","sources":["../../../src/transfer/leaseTransfer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,cAAc,EACd,YAAY,EAEZ,YAAY,EACb,MAAM,0CAA0C,CAAC;AAElD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEpE,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAK1F;AAID;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,YAAY,GAAG,YAAY,EACrC,QAAQ,EAAE,YAAY,GAAG,cAAc,EACvC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,YAAY,CAAC,CA0DvB"}
@@ -0,0 +1,74 @@
1
+ import { createAbortError } from "../util/abort.js";
2
+ export const DEFAULT_LEASE_DEPTH = 2;
3
+ /** The number of leases the engine keeps outstanding for `writable`. */
4
+ export function leaseDepth(writable, requested) {
5
+ return Math.max(1, Math.min(requested ?? DEFAULT_LEASE_DEPTH, writable.maxOutstanding ?? Infinity));
6
+ }
7
+ function ignore() { }
8
+ /**
9
+ * Moves data by having the source fill buffers handed out by the sink:
10
+ * `acquire -> readInto -> commit`, with up to `depth` leases outstanding so
11
+ * the next read overlaps earlier commits. `readInto` calls are serialised
12
+ * and commits happen in acquisition order. Every lease not yet committed is
13
+ * released on abort, error or end of stream.
14
+ */
15
+ export async function leaseTransfer(readable, writable, depth, context) {
16
+ const { options, hooks, operationId, type } = context;
17
+ const outstanding = new Set();
18
+ const pending = [];
19
+ let commitChain = Promise.resolve();
20
+ let bytes = 0;
21
+ let items = 0;
22
+ let stopped = false;
23
+ const writer = writable.stream.getWriter();
24
+ try {
25
+ for (;;) {
26
+ if (options.signal?.aborted)
27
+ throw createAbortError();
28
+ if (options.stop?.aborted) {
29
+ stopped = true;
30
+ break;
31
+ }
32
+ while (pending.length >= depth) {
33
+ await pending.shift();
34
+ }
35
+ const lease = await writable.acquire();
36
+ outstanding.add(lease);
37
+ const length = await readable.readInto(lease);
38
+ if (length === null) {
39
+ outstanding.delete(lease);
40
+ lease.release();
41
+ break;
42
+ }
43
+ bytes += length;
44
+ items += 1;
45
+ const committed = commitChain.then(async () => {
46
+ await lease.commit(length);
47
+ outstanding.delete(lease);
48
+ });
49
+ committed.catch(ignore);
50
+ commitChain = committed;
51
+ pending.push(committed);
52
+ hooks.onProgress?.({
53
+ operationId,
54
+ type,
55
+ bytesProcessed: bytes,
56
+ totalBytes: undefined,
57
+ });
58
+ }
59
+ await commitChain;
60
+ await writer.close();
61
+ if (!stopped) {
62
+ await readable.stream.cancel().catch(ignore);
63
+ }
64
+ return { bytes, items, stopped };
65
+ }
66
+ catch (error) {
67
+ for (const lease of outstanding) {
68
+ lease.release();
69
+ }
70
+ await writer.abort(error).catch(ignore);
71
+ await readable.stream.cancel(error).catch(ignore);
72
+ throw error;
73
+ }
74
+ }
@@ -0,0 +1,23 @@
1
+ import { type IOProvider, type Part, type RangeReadable, type StreamHandle } from "@flowscripter/pluggable-io-framework-api";
2
+ import type { ConcurrencyLimiter } from "../concurrency/ConcurrencyLimiter.ts";
3
+ import type { EntryContext, EntryOutcome } from "./EntryContext.ts";
4
+ export interface MultipartTransferInput {
5
+ readonly readable: StreamHandle & RangeReadable;
6
+ readonly sink: IOProvider;
7
+ readonly writer: {
8
+ write(parts: AsyncIterable<Part>): Promise<void>;
9
+ };
10
+ readonly totalBytes: number;
11
+ readonly partSize: number;
12
+ readonly limiter: ConcurrencyLimiter;
13
+ readonly context: EntryContext;
14
+ }
15
+ /**
16
+ * Reads parts with `readRange` and hands them to the sink's multipart
17
+ * writer, bounded by `limiter`. A part whose read fails with a
18
+ * `TransientIOError` is re-read on its own, up to `retry.maxRetries` times,
19
+ * within the same upload. `stop` ends the transfer after the parts already
20
+ * started; `signal` aborts it.
21
+ */
22
+ export declare function multipartTransfer(input: MultipartTransferInput): Promise<EntryOutcome>;
23
+ //# sourceMappingURL=multipartTransfer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multipartTransfer.d.ts","sourceRoot":"","sources":["../../../src/transfer/multipartTransfer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAEf,KAAK,IAAI,EACT,KAAK,aAAa,EAClB,KAAK,YAAY,EAElB,MAAM,0CAA0C,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAM/E,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGpE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,YAAY,GAAG,aAAa,CAAC;IAChD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;IACtE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;CAChC;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,YAAY,CAAC,CAoF5F"}
@@ -0,0 +1,94 @@
1
+ import { TransientIOError, } from "@flowscripter/pluggable-io-framework-api";
2
+ import { mapAsyncIterableConcurrently } from "../concurrency/mapAsyncIterableConcurrently.js";
3
+ import { backoff, DEFAULT_RETRY } from "../retry/RetryOptions.js";
4
+ import { createAbortError } from "../util/abort.js";
5
+ import { applyPayloadConverter } from "../util/applyPayloadConverter.js";
6
+ import { itemLength } from "../util/itemLength.js";
7
+ import { rangeReadableMultipartReader } from "./rangeReadableMultipartReader.js";
8
+ /**
9
+ * Reads parts with `readRange` and hands them to the sink's multipart
10
+ * writer, bounded by `limiter`. A part whose read fails with a
11
+ * `TransientIOError` is re-read on its own, up to `retry.maxRetries` times,
12
+ * within the same upload. `stop` ends the transfer after the parts already
13
+ * started; `signal` aborts it.
14
+ */
15
+ export async function multipartTransfer(input) {
16
+ const { readable, sink, writer, totalBytes, partSize, limiter, context } = input;
17
+ const { options, hooks, operationId, type } = context;
18
+ const retry = options.retry ?? DEFAULT_RETRY;
19
+ let bytes = 0;
20
+ let items = 0;
21
+ let stopped = false;
22
+ async function readPart(sourcePart) {
23
+ const partOperationId = crypto.randomUUID();
24
+ const end = Math.min(sourcePart.offset + partSize, totalBytes);
25
+ let stream = sourcePart.stream;
26
+ let attempt = 0;
27
+ for (;;) {
28
+ const reader = applyPayloadConverter(stream, readable.kind, sink.kind, options.converter).getReader();
29
+ const partItems = [];
30
+ let partBytes = 0;
31
+ try {
32
+ for (;;) {
33
+ if (options.signal?.aborted) {
34
+ await reader.cancel().catch(() => { });
35
+ throw createAbortError();
36
+ }
37
+ const { done, value } = await reader.read();
38
+ if (done)
39
+ break;
40
+ partItems.push(value);
41
+ partBytes += itemLength(value);
42
+ hooks.onProgress?.({
43
+ operationId: partOperationId,
44
+ parentOperationId: operationId,
45
+ type,
46
+ bytesProcessed: partBytes,
47
+ });
48
+ }
49
+ bytes += partBytes;
50
+ items += partItems.length;
51
+ hooks.onProgress?.({ operationId, type, bytesProcessed: bytes, totalBytes });
52
+ return partItems;
53
+ }
54
+ catch (error) {
55
+ if (!(error instanceof TransientIOError) || attempt >= retry.maxRetries) {
56
+ throw error;
57
+ }
58
+ attempt += 1;
59
+ await backoff(retry, attempt);
60
+ stream = (await readable.readRange(sourcePart.offset, end));
61
+ }
62
+ }
63
+ }
64
+ async function transferPart(sourcePart) {
65
+ const partItems = await readPart(sourcePart);
66
+ return {
67
+ index: sourcePart.index,
68
+ offset: sourcePart.offset,
69
+ kind: sink.kind,
70
+ stream: new ReadableStream({
71
+ start(controller) {
72
+ for (const item of partItems)
73
+ controller.enqueue(item);
74
+ controller.close();
75
+ },
76
+ }),
77
+ complete: () => sourcePart.complete(),
78
+ };
79
+ }
80
+ async function* sourceParts() {
81
+ for await (const part of rangeReadableMultipartReader(readable, totalBytes, partSize)) {
82
+ if (options.signal?.aborted)
83
+ throw createAbortError();
84
+ if (options.stop?.aborted) {
85
+ stopped = true;
86
+ await part.stream.cancel().catch(() => { });
87
+ return;
88
+ }
89
+ yield part;
90
+ }
91
+ }
92
+ await writer.write(mapAsyncIterableConcurrently(sourceParts(), transferPart, limiter));
93
+ return { bytes, items, stopped };
94
+ }
@@ -0,0 +1,10 @@
1
+ import type { IOProvider } from "@flowscripter/pluggable-io-framework-api";
2
+ /**
3
+ * Reconciles `source`'s and `sink`'s {@link PartSizeConstraints} for a
4
+ * transfer of `totalSize` bytes into a single part size satisfying both.
5
+ * Returns `undefined` when the bounds are mutually infeasible (source's
6
+ * minimum exceeds sink's maximum) - the caller should fall back to plain
7
+ * streaming rather than throwing.
8
+ */
9
+ export declare function negotiatePartSize(source: IOProvider, sink: IOProvider, totalSize: number): number | undefined;
10
+ //# sourceMappingURL=negotiatePartSize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"negotiatePartSize.d.ts","sourceRoot":"","sources":["../../../src/transfer/negotiatePartSize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAuB,MAAM,0CAA0C,CAAC;AAWhG;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAiBpB"}
@@ -0,0 +1,27 @@
1
+ const DEFAULT_PART_SIZE = 8 * 1024 * 1024;
2
+ const UNCONSTRAINED_PART_SIZE = {
3
+ minPartSize: 0,
4
+ maxPartSize: Infinity,
5
+ maxParts: Infinity,
6
+ defaultPartSize: DEFAULT_PART_SIZE,
7
+ };
8
+ /**
9
+ * Reconciles `source`'s and `sink`'s {@link PartSizeConstraints} for a
10
+ * transfer of `totalSize` bytes into a single part size satisfying both.
11
+ * Returns `undefined` when the bounds are mutually infeasible (source's
12
+ * minimum exceeds sink's maximum) - the caller should fall back to plain
13
+ * streaming rather than throwing.
14
+ */
15
+ export function negotiatePartSize(source, sink, totalSize) {
16
+ const sourceConstraints = source.getPartSizeConstraints?.(totalSize) ?? UNCONSTRAINED_PART_SIZE;
17
+ const sinkConstraints = sink.getPartSizeConstraints?.(totalSize) ?? UNCONSTRAINED_PART_SIZE;
18
+ const minPartSize = Math.max(sourceConstraints.minPartSize, sinkConstraints.minPartSize);
19
+ const maxPartSize = Math.min(sourceConstraints.maxPartSize, sinkConstraints.maxPartSize);
20
+ const maxParts = Math.min(sourceConstraints.maxParts, sinkConstraints.maxParts);
21
+ if (minPartSize > maxPartSize) {
22
+ return undefined;
23
+ }
24
+ const byMaxParts = Math.ceil(totalSize / maxParts);
25
+ const candidate = Math.max(minPartSize, sourceConstraints.defaultPartSize, sinkConstraints.defaultPartSize, byMaxParts);
26
+ return Math.min(candidate, maxPartSize);
27
+ }
@@ -0,0 +1,12 @@
1
+ import { type IOProvider } from "@flowscripter/pluggable-io-framework-api";
2
+ import type { TransferOptions } from "./TransferOptions.ts";
3
+ import type { TransferResult } from "./TransferResult.ts";
4
+ /**
5
+ * Transfers every entry directly inside `containerKey` whose name matches
6
+ * the glob `pattern` into the destination container, one single-entry
7
+ * transfer per match, bounded by the concurrency limiter. Matching
8
+ * containers are skipped. A move deletes each source entry after it is
9
+ * transferred.
10
+ */
11
+ export declare function patternTransfer(source: IOProvider, containerKey: string, pattern: string, sink: IOProvider, destContainerKey: string, options: TransferOptions, type: "copy" | "move"): Promise<TransferResult>;
12
+ //# sourceMappingURL=patternTransfer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patternTransfer.d.ts","sourceRoot":"","sources":["../../../src/transfer/patternTransfer.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAEhB,MAAM,0CAA0C,CAAC;AAQlD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,UAAU,EAClB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,MAAM,GAAG,MAAM,GACpB,OAAO,CAAC,cAAc,CAAC,CAqDzB"}
@@ -0,0 +1,61 @@
1
+ import { PermanentIOError, } from "@flowscripter/pluggable-io-framework-api";
2
+ import { defaultConcurrencyLimiter } from "../concurrency/ConcurrencyLimiter.js";
3
+ import { mapAsyncIterableConcurrently } from "../concurrency/mapAsyncIterableConcurrently.js";
4
+ import { throwIfAborted } from "../util/abort.js";
5
+ import { globToRegex } from "../util/globToRegex.js";
6
+ import { joinKey } from "../util/keys.js";
7
+ import { aggregateResults } from "./aggregateResults.js";
8
+ import { basePath, transferEntry } from "./transferEntry.js";
9
+ /**
10
+ * Transfers every entry directly inside `containerKey` whose name matches
11
+ * the glob `pattern` into the destination container, one single-entry
12
+ * transfer per match, bounded by the concurrency limiter. Matching
13
+ * containers are skipped. A move deletes each source entry after it is
14
+ * transferred.
15
+ */
16
+ export async function patternTransfer(source, containerKey, pattern, sink, destContainerKey, options, type) {
17
+ if (typeof source.list !== "function") {
18
+ throw new PermanentIOError("The source provider does not support listing containers");
19
+ }
20
+ const operationId = crypto.randomUUID();
21
+ const limiter = options.concurrencyLimiter ?? defaultConcurrencyLimiter;
22
+ let discovered = 0;
23
+ let completed = 0;
24
+ const reportProgress = () => {
25
+ options.telemetry?.onProgress?.({
26
+ operationId,
27
+ type,
28
+ bytesProcessed: 0,
29
+ entriesProcessed: completed,
30
+ totalEntries: discovered,
31
+ });
32
+ };
33
+ async function processEntry(entry) {
34
+ throwIfAborted(options.signal);
35
+ if (options.stop?.aborted || entry.properties.isContainer) {
36
+ return undefined;
37
+ }
38
+ discovered += 1;
39
+ reportProgress();
40
+ const result = await transferEntry({
41
+ source,
42
+ sourceKey: joinKey(source, containerKey, entry.path),
43
+ sink,
44
+ destKey: joinKey(sink, destContainerKey, entry.path),
45
+ properties: entry.properties,
46
+ options,
47
+ parentOperationId: operationId,
48
+ type,
49
+ deleteSource: type === "move",
50
+ });
51
+ completed += 1;
52
+ reportProgress();
53
+ return result;
54
+ }
55
+ const results = [];
56
+ for await (const result of mapAsyncIterableConcurrently(source.list(containerKey, { recursive: false, regex: globToRegex(pattern) }), processEntry, limiter)) {
57
+ if (result)
58
+ results.push(result);
59
+ }
60
+ return aggregateResults(results, basePath(source, sink, options), options.stop?.aborted ?? false);
61
+ }
@@ -0,0 +1,8 @@
1
+ import type { Part, PayloadKind, RangeReadable, StreamHandle } from "@flowscripter/pluggable-io-framework-api";
2
+ /**
3
+ * Splits a `RangeReadable` handle into parts of `partSize` bytes covering
4
+ * `totalSize`. Each part's stream is opened with `readRange(start, end)`
5
+ * (`end` exclusive) when the part is pulled.
6
+ */
7
+ export declare function rangeReadableMultipartReader<K extends PayloadKind>(handle: StreamHandle<K> & RangeReadable<K>, totalSize: number, partSize: number): AsyncGenerator<Part<K>>;
8
+ //# sourceMappingURL=rangeReadableMultipartReader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rangeReadableMultipartReader.d.ts","sourceRoot":"","sources":["../../../src/transfer/rangeReadableMultipartReader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,WAAW,EACX,aAAa,EACb,YAAY,EACb,MAAM,0CAA0C,CAAC;AAElD;;;;GAIG;AACH,wBAAuB,4BAA4B,CAAC,CAAC,SAAS,WAAW,EACvE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,EAC1C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAazB"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Splits a `RangeReadable` handle into parts of `partSize` bytes covering
3
+ * `totalSize`. Each part's stream is opened with `readRange(start, end)`
4
+ * (`end` exclusive) when the part is pulled.
5
+ */
6
+ export async function* rangeReadableMultipartReader(handle, totalSize, partSize) {
7
+ const partCount = Math.max(1, Math.ceil(totalSize / partSize));
8
+ for (let index = 0; index < partCount; index += 1) {
9
+ const start = index * partSize;
10
+ const end = Math.min(start + partSize, totalSize);
11
+ yield {
12
+ index,
13
+ offset: start,
14
+ kind: handle.kind,
15
+ stream: await handle.readRange(start, end),
16
+ complete: async () => { },
17
+ };
18
+ }
19
+ }
@@ -0,0 +1,13 @@
1
+ import { type IOProvider } from "@flowscripter/pluggable-io-framework-api";
2
+ import type { TransferOptions } from "./TransferOptions.ts";
3
+ import type { TransferResult } from "./TransferResult.ts";
4
+ /**
5
+ * Transfers a whole container. Uses a container-aware
6
+ * `directCopy`/`directMove` when the source declares
7
+ * `supportsRecursiveDirectTransfer`; otherwise lists the container and
8
+ * transfers each entry, bounded by the concurrency limiter. A non-direct
9
+ * move deletes the source container once, after every entry succeeded and
10
+ * only if the transfer was not stopped.
11
+ */
12
+ export declare function recursiveTransfer(source: IOProvider, sourceKey: string, sink: IOProvider, destKey: string, options: TransferOptions, type: "copy" | "move"): Promise<TransferResult>;
13
+ //# sourceMappingURL=recursiveTransfer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recursiveTransfer.d.ts","sourceRoot":"","sources":["../../../src/transfer/recursiveTransfer.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAEhB,MAAM,0CAA0C,CAAC;AASlD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA8B1D;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,MAAM,GAAG,MAAM,GACpB,OAAO,CAAC,cAAc,CAAC,CAuFzB"}
@@ -0,0 +1,112 @@
1
+ import { PermanentIOError, } from "@flowscripter/pluggable-io-framework-api";
2
+ import { defaultConcurrencyLimiter } from "../concurrency/ConcurrencyLimiter.js";
3
+ import { mapAsyncIterableConcurrently } from "../concurrency/mapAsyncIterableConcurrently.js";
4
+ import { DEFAULT_RETRY } from "../retry/RetryOptions.js";
5
+ import { withRetry } from "../retry/withRetry.js";
6
+ import { throwIfAborted } from "../util/abort.js";
7
+ import { baseName, joinKey } from "../util/keys.js";
8
+ import { aggregateResults } from "./aggregateResults.js";
9
+ import { basePath, transferEntry } from "./transferEntry.js";
10
+ /**
11
+ * Resolves the effective destination root for a recursive transfer,
12
+ * following `cp -r`/`mv` semantics: a destination that doesn't exist becomes
13
+ * the copy itself; an existing container gets the source nested inside it
14
+ * as `dest/<source-basename>`; an existing entry is rejected.
15
+ */
16
+ async function resolveRecursiveDestRoot(sourceKey, sink, destKey) {
17
+ let destProperties;
18
+ try {
19
+ destProperties = await sink.getProperties(destKey);
20
+ }
21
+ catch {
22
+ destProperties = undefined;
23
+ }
24
+ if (destProperties === undefined) {
25
+ return destKey;
26
+ }
27
+ if (!destProperties.isContainer) {
28
+ throw new PermanentIOError(`Cannot copy/move container "${sourceKey}" onto existing entry "${destKey}"`);
29
+ }
30
+ return joinKey(sink, destKey, baseName(sourceKey));
31
+ }
32
+ /**
33
+ * Transfers a whole container. Uses a container-aware
34
+ * `directCopy`/`directMove` when the source declares
35
+ * `supportsRecursiveDirectTransfer`; otherwise lists the container and
36
+ * transfers each entry, bounded by the concurrency limiter. A non-direct
37
+ * move deletes the source container once, after every entry succeeded and
38
+ * only if the transfer was not stopped.
39
+ */
40
+ export async function recursiveTransfer(source, sourceKey, sink, destKey, options, type) {
41
+ const path = basePath(source, sink, options);
42
+ const resolvedDestRoot = await resolveRecursiveDestRoot(sourceKey, sink, destKey);
43
+ if (options.directTransfer !== false &&
44
+ source.canDirectTransfer?.(sink) &&
45
+ source.supportsRecursiveDirectTransfer) {
46
+ const direct = type === "move" ? source.directMove : source.directCopy;
47
+ if (direct) {
48
+ throwIfAborted(options.signal);
49
+ const telemetry = { operationId: crypto.randomUUID(), hooks: options.telemetry ?? {} };
50
+ await direct.call(source, sourceKey, resolvedDestRoot, telemetry);
51
+ return { stopped: false, bytes: 0, items: 0, path: `${path}, direct` };
52
+ }
53
+ }
54
+ if (typeof source.list !== "function") {
55
+ throw new PermanentIOError("The source provider does not support listing containers");
56
+ }
57
+ if (type === "move" && typeof source.delete !== "function") {
58
+ throw new PermanentIOError("The source provider does not support delete, so it cannot move");
59
+ }
60
+ const recursiveOperationId = crypto.randomUUID();
61
+ const limiter = options.concurrencyLimiter ?? defaultConcurrencyLimiter;
62
+ let discovered = 0;
63
+ let completed = 0;
64
+ const reportProgress = () => {
65
+ options.telemetry?.onProgress?.({
66
+ operationId: recursiveOperationId,
67
+ type,
68
+ bytesProcessed: 0,
69
+ entriesProcessed: completed,
70
+ totalEntries: discovered,
71
+ });
72
+ };
73
+ async function processEntry(entry) {
74
+ throwIfAborted(options.signal);
75
+ if (options.stop?.aborted) {
76
+ return undefined;
77
+ }
78
+ discovered += 1;
79
+ reportProgress();
80
+ const entryDestKey = joinKey(sink, resolvedDestRoot, entry.path);
81
+ let result;
82
+ if (entry.properties.isContainer) {
83
+ await sink.createContainer?.(entryDestKey);
84
+ }
85
+ else {
86
+ result = await transferEntry({
87
+ source,
88
+ sourceKey: joinKey(source, sourceKey, entry.path),
89
+ sink,
90
+ destKey: entryDestKey,
91
+ properties: entry.properties,
92
+ options,
93
+ parentOperationId: recursiveOperationId,
94
+ type,
95
+ deleteSource: false,
96
+ });
97
+ }
98
+ completed += 1;
99
+ reportProgress();
100
+ return result;
101
+ }
102
+ const results = [];
103
+ for await (const result of mapAsyncIterableConcurrently(source.list(sourceKey, { recursive: true }), processEntry, limiter)) {
104
+ if (result)
105
+ results.push(result);
106
+ }
107
+ const outcome = aggregateResults(results, path, options.stop?.aborted ?? false);
108
+ if (type === "move" && !outcome.stopped) {
109
+ await withRetry(() => source.delete.call(source, sourceKey), options.retry ?? DEFAULT_RETRY);
110
+ }
111
+ return outcome;
112
+ }
@@ -0,0 +1,32 @@
1
+ import { type IOProvider, type StreamHandle } from "@flowscripter/pluggable-io-framework-api";
2
+ import type { EntryContext, EntryOutcome } from "./EntryContext.ts";
3
+ export interface StreamTransferInput {
4
+ readonly source: IOProvider;
5
+ readonly sourceKey: string;
6
+ readonly readable: StreamHandle;
7
+ readonly sink: IOProvider;
8
+ readonly destKey: string;
9
+ readonly writable: StreamHandle;
10
+ /** Total bytes for a bounded source, when known. */
11
+ readonly totalBytes?: number;
12
+ readonly context: EntryContext;
13
+ }
14
+ /**
15
+ * Pipes a readable handle into a writable handle item by item, honouring
16
+ * `signal` (abort) and `stop` (graceful close).
17
+ *
18
+ * On a `TransientIOError`:
19
+ * - bounded source with `RangeReadable`, and a writable exposing a resume
20
+ * token: the sink is reopened with the token and the source re-read from
21
+ * the sink's `startOffset`;
22
+ * - other bounded transfers restart from scratch;
23
+ * - unbounded source failure: the source is reopened, the sink kept open,
24
+ * the first new item flagged with `discontinuity`, and `onGap` reported;
25
+ * - unbounded sink failure: the sink is reopened with its resume token, or
26
+ * the transfer fails.
27
+ *
28
+ * `retry.maxRetries` counts consecutive failures and resets once an item is
29
+ * written.
30
+ */
31
+ export declare function streamTransfer(input: StreamTransferInput): Promise<EntryOutcome>;
32
+ //# sourceMappingURL=streamTransfer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streamTransfer.d.ts","sourceRoot":"","sources":["../../../src/transfer/streamTransfer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAIf,KAAK,YAAY,EAElB,MAAM,0CAA0C,CAAC;AAKlD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAYpE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,oDAAoD;IACpD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;CAChC;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,CAAC,CA8JtF"}