@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,186 @@
1
+ import { isRangeReadable, isResumableWritable, TransientIOError, } from "@flowscripter/pluggable-io-framework-api";
2
+ import { backoff, DEFAULT_RETRY } from "../retry/RetryOptions.js";
3
+ import { createAbortError, watchSignals } from "../util/abort.js";
4
+ import { applyPayloadConverter } from "../util/applyPayloadConverter.js";
5
+ import { itemLength } from "../util/itemLength.js";
6
+ /** Which side of the transfer a failure came from. */
7
+ class SideError extends Error {
8
+ side;
9
+ constructor(side, cause) {
10
+ super("transfer side failed", { cause });
11
+ this.side = side;
12
+ }
13
+ }
14
+ function ignore() { }
15
+ /**
16
+ * Pipes a readable handle into a writable handle item by item, honouring
17
+ * `signal` (abort) and `stop` (graceful close).
18
+ *
19
+ * On a `TransientIOError`:
20
+ * - bounded source with `RangeReadable`, and a writable exposing a resume
21
+ * token: the sink is reopened with the token and the source re-read from
22
+ * the sink's `startOffset`;
23
+ * - other bounded transfers restart from scratch;
24
+ * - unbounded source failure: the source is reopened, the sink kept open,
25
+ * the first new item flagged with `discontinuity`, and `onGap` reported;
26
+ * - unbounded sink failure: the sink is reopened with its resume token, or
27
+ * the transfer fails.
28
+ *
29
+ * `retry.maxRetries` counts consecutive failures and resets once an item is
30
+ * written.
31
+ */
32
+ export async function streamTransfer(input) {
33
+ const { source, sourceKey, sink, destKey, totalBytes, context } = input;
34
+ const { options, hooks, operationId, type } = context;
35
+ const retry = options.retry ?? DEFAULT_RETRY;
36
+ const bounded = input.readable.bounded !== false;
37
+ const watch = watchSignals(options.signal, options.stop);
38
+ let readable = input.readable;
39
+ let writable = input.writable;
40
+ const convert = (stream) => applyPayloadConverter(stream, readable.kind, sink.kind, options.converter);
41
+ let reader = convert(readable.stream).getReader();
42
+ let writer = writable.stream.getWriter();
43
+ let bytes = 0;
44
+ let items = 0;
45
+ let failures = 0;
46
+ let markGap = false;
47
+ let recover;
48
+ async function pump() {
49
+ for (;;) {
50
+ let read;
51
+ try {
52
+ read = await Promise.race([reader.read(), watch.fired]);
53
+ }
54
+ catch (error) {
55
+ throw new SideError("source", error);
56
+ }
57
+ if (read === "aborted")
58
+ throw createAbortError();
59
+ if (read === "stopped")
60
+ return "stopped";
61
+ if (read.done)
62
+ return "done";
63
+ let item = read.value;
64
+ if (markGap) {
65
+ item = { ...item, attributes: { ...item.attributes, discontinuity: true } };
66
+ markGap = false;
67
+ }
68
+ let written;
69
+ try {
70
+ written = await Promise.race([writer.write(item), watch.fired]);
71
+ }
72
+ catch (error) {
73
+ throw new SideError("sink", error);
74
+ }
75
+ if (written === "aborted")
76
+ throw createAbortError();
77
+ bytes += itemLength(item);
78
+ items += 1;
79
+ failures = 0;
80
+ hooks.onProgress?.({
81
+ operationId,
82
+ type,
83
+ bytesProcessed: bytes,
84
+ totalBytes: bounded ? totalBytes : undefined,
85
+ });
86
+ if (written === "stopped")
87
+ return "stopped";
88
+ }
89
+ }
90
+ try {
91
+ for (;;) {
92
+ try {
93
+ if (recover) {
94
+ const action = recover;
95
+ recover = undefined;
96
+ try {
97
+ await action.run();
98
+ }
99
+ catch (error) {
100
+ throw new SideError(action.side, error);
101
+ }
102
+ }
103
+ const outcome = await pump();
104
+ if (outcome === "stopped") {
105
+ await reader.cancel().catch(ignore);
106
+ }
107
+ await writer.close();
108
+ return { bytes, items, stopped: outcome === "stopped" };
109
+ }
110
+ catch (error) {
111
+ if (error instanceof DOMException && error.name === "AbortError") {
112
+ await reader.cancel(error).catch(ignore);
113
+ await writer.abort(error).catch(ignore);
114
+ throw error;
115
+ }
116
+ const side = error instanceof SideError ? error.side : "sink";
117
+ const cause = error instanceof SideError ? error.cause : error;
118
+ failures += 1;
119
+ const retryable = cause instanceof TransientIOError && failures <= retry.maxRetries;
120
+ if (retryable && !bounded && side === "source" && retry.onGap !== "fail") {
121
+ await reader.cancel().catch(ignore);
122
+ const attempt = failures;
123
+ recover = {
124
+ side: "source",
125
+ run: async () => {
126
+ await backoff(retry, attempt);
127
+ readable = await source.getReadableStream(sourceKey);
128
+ reader = convert(readable.stream).getReader();
129
+ markGap = true;
130
+ hooks.onGap?.(operationId, { atBytes: bytes, attempt });
131
+ },
132
+ };
133
+ continue;
134
+ }
135
+ const token = retryable && isResumableWritable(writable) ? writable.resumeToken() : undefined;
136
+ const canResume = token !== undefined && (!bounded || isRangeReadable(readable));
137
+ if (retryable && canResume) {
138
+ // The failed sink is released, not aborted, so a provider that
139
+ // discards partial output on abort keeps it for the resume.
140
+ writer.releaseLock();
141
+ if (bounded) {
142
+ await reader.cancel().catch(ignore);
143
+ }
144
+ const attempt = failures;
145
+ recover = {
146
+ side: "sink",
147
+ run: async () => {
148
+ await backoff(retry, attempt);
149
+ const resumed = await sink.getWritableStream(destKey, { resume: token });
150
+ writable = resumed;
151
+ writer = resumed.stream.getWriter();
152
+ if (bounded && isRangeReadable(readable)) {
153
+ const startOffset = resumed.startOffset ?? token.offset;
154
+ const end = totalBytes ?? Number.MAX_SAFE_INTEGER;
155
+ reader = convert((await readable.readRange(startOffset, end))).getReader();
156
+ bytes = startOffset;
157
+ }
158
+ },
159
+ };
160
+ continue;
161
+ }
162
+ await reader.cancel(cause).catch(ignore);
163
+ await writer.abort(cause).catch(ignore);
164
+ if (!retryable || !bounded) {
165
+ throw cause;
166
+ }
167
+ const attempt = failures;
168
+ recover = {
169
+ side: "source",
170
+ run: async () => {
171
+ await backoff(retry, attempt);
172
+ readable = await source.getReadableStream(sourceKey);
173
+ reader = convert(readable.stream).getReader();
174
+ writable = await sink.getWritableStream(destKey);
175
+ writer = writable.stream.getWriter();
176
+ bytes = 0;
177
+ items = 0;
178
+ },
179
+ };
180
+ }
181
+ }
182
+ }
183
+ finally {
184
+ watch.dispose();
185
+ }
186
+ }
@@ -0,0 +1,25 @@
1
+ import { type EntryProperties, type IOProvider } from "@flowscripter/pluggable-io-framework-api";
2
+ import type { TransferOptions } from "./TransferOptions.ts";
3
+ import type { TransferResult } from "./TransferResult.ts";
4
+ export interface TransferEntryInput {
5
+ readonly source: IOProvider;
6
+ readonly sourceKey: string;
7
+ readonly sink: IOProvider;
8
+ readonly destKey: string;
9
+ readonly properties: EntryProperties;
10
+ readonly options: TransferOptions;
11
+ readonly parentOperationId?: string;
12
+ /** `"move"` rejects unbounded sources and prefers `directMove`. */
13
+ readonly type: "copy" | "move";
14
+ /** Whether a non-direct move deletes the source entry afterwards. */
15
+ readonly deleteSource: boolean;
16
+ }
17
+ /** The path description before the transfer strategy is known. */
18
+ export declare function basePath(source: IOProvider, sink: IOProvider, options: TransferOptions): string;
19
+ /**
20
+ * Transfers one entry: a direct provider transfer when eligible, otherwise
21
+ * multipart, the lease path or plain streaming, after checking the payload
22
+ * type and (for a move) that the source is bounded.
23
+ */
24
+ export declare function transferEntry(input: TransferEntryInput): Promise<TransferResult>;
25
+ //# sourceMappingURL=transferEntry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transferEntry.d.ts","sourceRoot":"","sources":["../../../src/transfer/transferEntry.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,UAAU,EAOhB,MAAM,0CAA0C,CAAC;AAYlD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAI1D,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,qEAAqE;IACrE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC;AAED,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,eAAe,GAAG,MAAM,CAM/F;AAgBD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAmHtF"}
@@ -0,0 +1,136 @@
1
+ import { BYTES_PAYLOAD_TYPE, isBufferProvider, isFillReadable, isRangeReadable, PermanentIOError, } from "@flowscripter/pluggable-io-framework-api";
2
+ import { defaultConcurrencyLimiter } from "../concurrency/ConcurrencyLimiter.js";
3
+ import { DEFAULT_RETRY } from "../retry/RetryOptions.js";
4
+ import { withRetry } from "../retry/withRetry.js";
5
+ import { throwIfAborted } from "../util/abort.js";
6
+ import { describeConverter } from "../util/describePath.js";
7
+ import { withParentOperationId } from "../util/withParentOperationId.js";
8
+ import { leaseDepth, leaseTransfer } from "./leaseTransfer.js";
9
+ import { multipartTransfer } from "./multipartTransfer.js";
10
+ import { negotiatePartSize } from "./negotiatePartSize.js";
11
+ import { streamTransfer } from "./streamTransfer.js";
12
+ const DEFAULT_MULTIPART_THRESHOLD = 64 * 1024 * 1024;
13
+ /** The path description before the transfer strategy is known. */
14
+ export function basePath(source, sink, options) {
15
+ if (options.path !== undefined) {
16
+ return options.path;
17
+ }
18
+ const via = options.converter ? describeConverter(options.converter) : "";
19
+ return `${source.kind} -> ${sink.kind}${via}`;
20
+ }
21
+ function checkPayloadType(readable, options) {
22
+ const payloadType = readable.payloadType ?? BYTES_PAYLOAD_TYPE;
23
+ const accepted = options.writePayloadTypes ?? [BYTES_PAYLOAD_TYPE];
24
+ if (!accepted.includes(payloadType)) {
25
+ throw new PermanentIOError(`Source payload type "${payloadType}" is not accepted by the sink (sink accepts: ${accepted.join(", ")})`);
26
+ }
27
+ }
28
+ async function cancelStream(handle) {
29
+ await handle.stream.cancel().catch(() => { });
30
+ }
31
+ /**
32
+ * Transfers one entry: a direct provider transfer when eligible, otherwise
33
+ * multipart, the lease path or plain streaming, after checking the payload
34
+ * type and (for a move) that the source is bounded.
35
+ */
36
+ export async function transferEntry(input) {
37
+ const { source, sourceKey, sink, destKey, properties, options, type } = input;
38
+ throwIfAborted(options.signal);
39
+ const path = basePath(source, sink, options);
40
+ if (options.stop?.aborted) {
41
+ return { stopped: true, bytes: 0, items: 0, path };
42
+ }
43
+ const operationId = crypto.randomUUID();
44
+ const hooks = withParentOperationId(options.telemetry, input.parentOperationId);
45
+ const context = { operationId, hooks, options, type };
46
+ if (options.directTransfer !== false && source.canDirectTransfer?.(sink)) {
47
+ const direct = type === "move" ? source.directMove : source.directCopy;
48
+ if (direct) {
49
+ await direct.call(source, sourceKey, destKey, { operationId, hooks });
50
+ return { stopped: false, bytes: properties.size ?? 0, items: 0, path: `${path}, direct` };
51
+ }
52
+ }
53
+ if (input.deleteSource && typeof source.delete !== "function") {
54
+ throw new PermanentIOError("The source provider does not support delete, so it cannot move");
55
+ }
56
+ const retry = options.retry ?? DEFAULT_RETRY;
57
+ let readable = await withRetry(() => source.getReadableStream(sourceKey), retry);
58
+ try {
59
+ checkPayloadType(readable, options);
60
+ if (type === "move" && readable.bounded === false) {
61
+ throw new PermanentIOError("cannot move a live source");
62
+ }
63
+ }
64
+ catch (error) {
65
+ await cancelStream(readable);
66
+ throw error;
67
+ }
68
+ let outcome;
69
+ let strategy;
70
+ const size = properties.size;
71
+ const threshold = options.multipartThreshold ?? DEFAULT_MULTIPART_THRESHOLD;
72
+ const multipartWriter = readable.bounded !== false &&
73
+ size !== undefined &&
74
+ size >= threshold &&
75
+ isRangeReadable(readable) &&
76
+ typeof sink.getMultipartWriter === "function"
77
+ ? sink.getMultipartWriter.bind(sink)
78
+ : undefined;
79
+ const partSize = multipartWriter && size !== undefined ? negotiatePartSize(source, sink, size) : undefined;
80
+ if (multipartWriter &&
81
+ partSize !== undefined &&
82
+ size !== undefined &&
83
+ isRangeReadable(readable)) {
84
+ await cancelStream(readable);
85
+ const rangeReadable = readable;
86
+ outcome = await withRetry(() => multipartTransfer({
87
+ readable: rangeReadable,
88
+ sink,
89
+ writer: multipartWriter(destKey, partSize),
90
+ totalBytes: size,
91
+ partSize,
92
+ limiter: options.concurrencyLimiter ?? defaultConcurrencyLimiter,
93
+ context,
94
+ }), retry);
95
+ strategy = "multipart";
96
+ }
97
+ else {
98
+ let writable = await withRetry(() => sink.getWritableStream(destKey), retry);
99
+ if (!options.converter &&
100
+ isFillReadable(readable) &&
101
+ isBufferProvider(writable) &&
102
+ readable.domains.includes(writable.domain)) {
103
+ const depth = leaseDepth(writable, options.leaseDepth);
104
+ let attempt = 0;
105
+ outcome = await withRetry(async () => {
106
+ if (attempt > 0) {
107
+ readable = await source.getReadableStream(sourceKey);
108
+ writable = await sink.getWritableStream(destKey);
109
+ }
110
+ attempt += 1;
111
+ if (!isFillReadable(readable) || !isBufferProvider(writable)) {
112
+ throw new PermanentIOError("Reopened handles no longer support the lease path");
113
+ }
114
+ return leaseTransfer(readable, writable, depth, context);
115
+ }, retry);
116
+ strategy = `lease (depth ${depth})`;
117
+ }
118
+ else {
119
+ outcome = await streamTransfer({
120
+ source,
121
+ sourceKey,
122
+ readable,
123
+ sink,
124
+ destKey,
125
+ writable,
126
+ totalBytes: size,
127
+ context,
128
+ });
129
+ strategy = "stream";
130
+ }
131
+ }
132
+ if (input.deleteSource && !outcome.stopped) {
133
+ await withRetry(() => source.delete.call(source, sourceKey), retry);
134
+ }
135
+ return { ...outcome, path: `${path}, ${strategy}` };
136
+ }
@@ -0,0 +1,12 @@
1
+ /** The error a transfer rejects with when cancelled through `TransferOptions.signal`. */
2
+ export declare function createAbortError(): DOMException;
3
+ export declare function throwIfAborted(signal: AbortSignal | undefined): void;
4
+ /**
5
+ * Resolves with `"aborted"` or `"stopped"` as soon as `signal` or `stop`
6
+ * fires, and never otherwise. `dispose` removes the listeners.
7
+ */
8
+ export declare function watchSignals(signal: AbortSignal | undefined, stop: AbortSignal | undefined): {
9
+ readonly fired: Promise<"aborted" | "stopped">;
10
+ dispose(): void;
11
+ };
12
+ //# sourceMappingURL=abort.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abort.d.ts","sourceRoot":"","sources":["../../../src/util/abort.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,wBAAgB,gBAAgB,IAAI,YAAY,CAE/C;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CAIpE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,IAAI,EAAE,WAAW,GAAG,SAAS,GAC5B;IAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IAAC,OAAO,IAAI,IAAI,CAAA;CAAE,CAuBrE"}
@@ -0,0 +1,39 @@
1
+ /** The error a transfer rejects with when cancelled through `TransferOptions.signal`. */
2
+ export function createAbortError() {
3
+ return new DOMException("The transfer was aborted", "AbortError");
4
+ }
5
+ export function throwIfAborted(signal) {
6
+ if (signal?.aborted) {
7
+ throw createAbortError();
8
+ }
9
+ }
10
+ /**
11
+ * Resolves with `"aborted"` or `"stopped"` as soon as `signal` or `stop`
12
+ * fires, and never otherwise. `dispose` removes the listeners.
13
+ */
14
+ export function watchSignals(signal, stop) {
15
+ const cleanups = [];
16
+ const fired = new Promise((resolve) => {
17
+ for (const [target, outcome] of [
18
+ [signal, "aborted"],
19
+ [stop, "stopped"],
20
+ ]) {
21
+ if (!target)
22
+ continue;
23
+ if (target.aborted) {
24
+ resolve(outcome);
25
+ continue;
26
+ }
27
+ const listener = () => resolve(outcome);
28
+ target.addEventListener("abort", listener, { once: true });
29
+ cleanups.push(() => target.removeEventListener("abort", listener));
30
+ }
31
+ });
32
+ return {
33
+ fired,
34
+ dispose() {
35
+ for (const cleanup of cleanups)
36
+ cleanup();
37
+ },
38
+ };
39
+ }
@@ -0,0 +1,9 @@
1
+ import { type Item, type PayloadConverter, type PayloadKind } from "@flowscripter/pluggable-io-framework-api";
2
+ /**
3
+ * Converts every item of `stream` for a sink of `toKind`. With a converter,
4
+ * each item goes through `converter.convert`, including when the kinds match
5
+ * and only the memory domain differs. Without one, a kind mismatch fails on
6
+ * the first item.
7
+ */
8
+ export declare function applyPayloadConverter(stream: ReadableStream<Item>, fromKind: PayloadKind, toKind: PayloadKind, converter: PayloadConverter | undefined): ReadableStream<Item>;
9
+ //# sourceMappingURL=applyPayloadConverter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applyPayloadConverter.d.ts","sourceRoot":"","sources":["../../../src/util/applyPayloadConverter.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,IAAI,EACT,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,0CAA0C,CAAC;AAElD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,cAAc,CAAC,IAAI,CAAC,EAC5B,QAAQ,EAAE,WAAW,EACrB,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,gBAAgB,GAAG,SAAS,GACtC,cAAc,CAAC,IAAI,CAAC,CAkBtB"}
@@ -0,0 +1,26 @@
1
+ import { adaptReadableStream, } from "@flowscripter/pluggable-io-framework-api";
2
+ /**
3
+ * Converts every item of `stream` for a sink of `toKind`. With a converter,
4
+ * each item goes through `converter.convert`, including when the kinds match
5
+ * and only the memory domain differs. Without one, a kind mismatch fails on
6
+ * the first item.
7
+ */
8
+ export function applyPayloadConverter(stream, fromKind, toKind, converter) {
9
+ if (!converter) {
10
+ return adaptReadableStream(stream, fromKind, toKind);
11
+ }
12
+ const reader = stream.getReader();
13
+ return new ReadableStream({
14
+ async pull(controller) {
15
+ const { done, value } = await reader.read();
16
+ if (done) {
17
+ controller.close();
18
+ return;
19
+ }
20
+ controller.enqueue(converter.convert(value));
21
+ },
22
+ cancel(reason) {
23
+ return reader.cancel(reason);
24
+ },
25
+ });
26
+ }
@@ -0,0 +1,6 @@
1
+ import type { PayloadConverter, PayloadKind } from "@flowscripter/pluggable-io-framework-api";
2
+ /** `kind` or `kind[domain]`. */
3
+ export declare function describeKind(kind: PayloadKind, domain?: string): string;
4
+ /** ` via from->to (zero-copy)` or ` via from->to (copy)`. */
5
+ export declare function describeConverter(converter: PayloadConverter): string;
6
+ //# sourceMappingURL=describePath.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describePath.d.ts","sourceRoot":"","sources":["../../../src/util/describePath.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,0CAA0C,CAAC;AAE9F,gCAAgC;AAChC,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,6DAA6D;AAC7D,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,gBAAgB,GAAG,MAAM,CAGrE"}
@@ -0,0 +1,9 @@
1
+ /** `kind` or `kind[domain]`. */
2
+ export function describeKind(kind, domain) {
3
+ return domain === undefined ? kind : `${kind}[${domain}]`;
4
+ }
5
+ /** ` via from->to (zero-copy)` or ` via from->to (copy)`. */
6
+ export function describeConverter(converter) {
7
+ const cost = converter.cost === 0 ? "zero-copy" : "copy";
8
+ return ` via ${describeKind(converter.from.kind, converter.from.domain)}->${describeKind(converter.to.kind, converter.to.domain)} (${cost})`;
9
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Translates a glob into an anchored regular expression matched against a
3
+ * single entry name: `*` matches any run of characters except `/`, `?`
4
+ * matches one such character, and `[...]` is a character class (`[!...]`
5
+ * negates it). Everything else matches literally.
6
+ */
7
+ export declare function globToRegex(glob: string): RegExp;
8
+ //# sourceMappingURL=globToRegex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"globToRegex.d.ts","sourceRoot":"","sources":["../../../src/util/globToRegex.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyBhD"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Translates a glob into an anchored regular expression matched against a
3
+ * single entry name: `*` matches any run of characters except `/`, `?`
4
+ * matches one such character, and `[...]` is a character class (`[!...]`
5
+ * negates it). Everything else matches literally.
6
+ */
7
+ export function globToRegex(glob) {
8
+ let source = "";
9
+ for (let i = 0; i < glob.length; i += 1) {
10
+ const char = glob[i];
11
+ if (char === "*") {
12
+ source += "[^/]*";
13
+ }
14
+ else if (char === "?") {
15
+ source += "[^/]";
16
+ }
17
+ else if (char === "[") {
18
+ const close = glob.indexOf("]", i + 2);
19
+ if (close === -1) {
20
+ source += "\\[";
21
+ continue;
22
+ }
23
+ let body = glob.slice(i + 1, close);
24
+ if (body.startsWith("!")) {
25
+ body = `^${body.slice(1)}`;
26
+ }
27
+ source += `[${body.replace(/\\/g, "\\\\")}]`;
28
+ i = close;
29
+ }
30
+ else {
31
+ source += char.replace(/[.+^${}()|\\/\]]/g, "\\$&");
32
+ }
33
+ }
34
+ return new RegExp(`^${source}$`);
35
+ }
@@ -0,0 +1,4 @@
1
+ import { type Item } from "@flowscripter/pluggable-io-framework-api";
2
+ /** The payload size of an item in bytes. */
3
+ export declare function itemLength(item: Item): number;
4
+ //# sourceMappingURL=itemLength.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"itemLength.d.ts","sourceRoot":"","sources":["../../../src/util/itemLength.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,EAAe,MAAM,0CAA0C,CAAC;AAElF,4CAA4C;AAC5C,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAE7C"}
@@ -0,0 +1,5 @@
1
+ import { PayloadKind } from "@flowscripter/pluggable-io-framework-api";
2
+ /** The payload size of an item in bytes. */
3
+ export function itemLength(item) {
4
+ return item.payload.kind === PayloadKind.Js ? item.payload.data.byteLength : item.payload.length;
5
+ }
@@ -0,0 +1,8 @@
1
+ import type { IOProvider } from "@flowscripter/pluggable-io-framework-api";
2
+ /** Joins with `/` - the default when a provider has no `joinKey`. */
3
+ export declare function defaultJoinKey(containerKey: string, name: string): string;
4
+ /** Joins a child name onto a container key using the provider's own rule. */
5
+ export declare function joinKey(provider: IOProvider, containerKey: string, name: string): string;
6
+ /** The last segment of a key, treating both `/` and `\` as separators. */
7
+ export declare function baseName(key: string): string;
8
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../../src/util/keys.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAC;AAE3E,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAIzE;AAED,6EAA6E;AAC7E,wBAAgB,OAAO,CAAC,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAIxF;AAED,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG5C"}
@@ -0,0 +1,19 @@
1
+ /** Joins with `/` - the default when a provider has no `joinKey`. */
2
+ export function defaultJoinKey(containerKey, name) {
3
+ if (name === "")
4
+ return containerKey;
5
+ if (containerKey === "")
6
+ return name;
7
+ return `${containerKey.replace(/\/+$/, "")}/${name}`;
8
+ }
9
+ /** Joins a child name onto a container key using the provider's own rule. */
10
+ export function joinKey(provider, containerKey, name) {
11
+ return provider.joinKey
12
+ ? provider.joinKey(containerKey, name)
13
+ : defaultJoinKey(containerKey, name);
14
+ }
15
+ /** The last segment of a key, treating both `/` and `\` as separators. */
16
+ export function baseName(key) {
17
+ const segments = key.split(/[\\/]+/).filter((segment) => segment !== "");
18
+ return segments.at(-1) ?? key;
19
+ }
@@ -0,0 +1,4 @@
1
+ import type { TelemetryHooks } from "@flowscripter/pluggable-io-framework-api";
2
+ /** Wraps `hooks` so any `onProgress` event passing through gets `parentOperationId` filled in, unless the emitter already set one (nested hierarchy). */
3
+ export declare function withParentOperationId(hooks: TelemetryHooks | undefined, parentOperationId: string | undefined): TelemetryHooks;
4
+ //# sourceMappingURL=withParentOperationId.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withParentOperationId.d.ts","sourceRoot":"","sources":["../../../src/util/withParentOperationId.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0CAA0C,CAAC;AAE/E,yJAAyJ;AACzJ,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,cAAc,GAAG,SAAS,EACjC,iBAAiB,EAAE,MAAM,GAAG,SAAS,GACpC,cAAc,CAUhB"}
@@ -0,0 +1,12 @@
1
+ /** Wraps `hooks` so any `onProgress` event passing through gets `parentOperationId` filled in, unless the emitter already set one (nested hierarchy). */
2
+ export function withParentOperationId(hooks, parentOperationId) {
3
+ if (!hooks || parentOperationId === undefined) {
4
+ return hooks ?? {};
5
+ }
6
+ return {
7
+ ...hooks,
8
+ onProgress: hooks.onProgress
9
+ ? (event) => hooks.onProgress?.({ parentOperationId, ...event })
10
+ : undefined,
11
+ };
12
+ }
package/index.ts CHANGED
@@ -1,6 +1,13 @@
1
- export * from "./src/ConcurrencyLimiter.ts";
2
- export * from "./src/ProviderRegistry.ts";
3
- export * from "./src/copyMove.ts";
1
+ export * from "./src/concurrency/ConcurrencyLimiter.ts";
2
+ export * from "./src/concurrency/mapAsyncIterableConcurrently.ts";
4
3
  export * from "./src/decorators/locallyCached.ts";
5
4
  export * from "./src/decorators/seekable.ts";
6
- export * from "./src/withRetry.ts";
5
+ export * from "./src/registry/detectProtocol.ts";
6
+ export * from "./src/registry/ProviderRegistry.ts";
7
+ export * from "./src/retry/RetryOptions.ts";
8
+ export * from "./src/retry/withRetry.ts";
9
+ export * from "./src/transfer/copyMove.ts";
10
+ export * from "./src/transfer/rangeReadableMultipartReader.ts";
11
+ export * from "./src/transfer/TransferOptions.ts";
12
+ export * from "./src/transfer/TransferResult.ts";
13
+ export * from "./src/util/globToRegex.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/pluggable-io-framework",
3
- "version": "1.2.7",
3
+ "version": "2.0.0",
4
4
  "description": "A pluggable source/sink IO framework using https://github.com/flowscripter/dynamic-plugin-framework",
5
5
  "keywords": [
6
6
  "bun",
@@ -41,12 +41,13 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@flowscripter/dynamic-plugin-framework": "^2.2.14",
44
- "@flowscripter/pluggable-io-framework-api": "^1.1.6"
44
+ "@flowscripter/pluggable-io-framework-api": "^3.0.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/bun": "^1.4.2",
48
48
  "oxfmt": "0.71.0",
49
- "oxlint": "1.86.0"
49
+ "oxlint": "1.86.0",
50
+ "zod": "^4.6.5"
50
51
  },
51
52
  "peerDependencies": {
52
53
  "typescript": "^7.0.2"