@flowscripter/pluggable-io-framework 1.2.7 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -178
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -4
- package/dist/src/concurrency/AsyncChannel.d.ts +9 -0
- package/dist/src/concurrency/AsyncChannel.d.ts.map +1 -0
- package/dist/src/concurrency/AsyncChannel.js +48 -0
- package/dist/src/{ConcurrencyLimiter.d.ts → concurrency/ConcurrencyLimiter.d.ts} +1 -12
- package/dist/src/concurrency/ConcurrencyLimiter.d.ts.map +1 -0
- package/dist/src/concurrency/ConcurrencyLimiter.js +64 -0
- package/dist/src/concurrency/mapAsyncIterableConcurrently.d.ts +13 -0
- package/dist/src/concurrency/mapAsyncIterableConcurrently.d.ts.map +1 -0
- package/dist/src/concurrency/mapAsyncIterableConcurrently.js +61 -0
- package/dist/src/decorators/locallyCached.d.ts +9 -10
- package/dist/src/decorators/locallyCached.d.ts.map +1 -1
- package/dist/src/decorators/locallyCached.js +23 -16
- package/dist/src/decorators/seekable.d.ts +4 -4
- package/dist/src/decorators/seekable.d.ts.map +1 -1
- package/dist/src/decorators/seekable.js +2 -2
- package/dist/src/registry/ProviderRegistry.d.ts +52 -0
- package/dist/src/registry/ProviderRegistry.d.ts.map +1 -0
- package/dist/src/registry/ProviderRegistry.js +148 -0
- package/dist/src/registry/detectProtocol.d.ts +8 -0
- package/dist/src/registry/detectProtocol.d.ts.map +1 -0
- package/dist/src/registry/detectProtocol.js +15 -0
- package/dist/src/registry/negotiateTransfer.d.ts +29 -0
- package/dist/src/registry/negotiateTransfer.d.ts.map +1 -0
- package/dist/src/registry/negotiateTransfer.js +69 -0
- package/dist/src/retry/RetryOptions.d.ts +19 -0
- package/dist/src/retry/RetryOptions.d.ts.map +1 -0
- package/dist/src/retry/RetryOptions.js +9 -0
- package/dist/src/{withRetry.d.ts → retry/withRetry.d.ts} +1 -5
- package/dist/src/retry/withRetry.d.ts.map +1 -0
- package/dist/src/{withRetry.js → retry/withRetry.js} +2 -5
- package/dist/src/transfer/EntryContext.d.ts +16 -0
- package/dist/src/transfer/EntryContext.d.ts.map +1 -0
- package/dist/src/transfer/EntryContext.js +1 -0
- package/dist/src/transfer/TransferOptions.d.ts +45 -0
- package/dist/src/transfer/TransferOptions.d.ts.map +1 -0
- package/dist/src/transfer/TransferOptions.js +1 -0
- package/dist/src/transfer/TransferResult.d.ts +9 -0
- package/dist/src/transfer/TransferResult.d.ts.map +1 -0
- package/dist/src/transfer/TransferResult.js +1 -0
- package/dist/src/transfer/aggregateResults.d.ts +4 -0
- package/dist/src/transfer/aggregateResults.d.ts.map +1 -0
- package/dist/src/transfer/aggregateResults.js +9 -0
- package/dist/src/transfer/copyMove.d.ts +23 -0
- package/dist/src/transfer/copyMove.d.ts.map +1 -0
- package/dist/src/transfer/copyMove.js +68 -0
- package/dist/src/transfer/leaseTransfer.d.ts +14 -0
- package/dist/src/transfer/leaseTransfer.d.ts.map +1 -0
- package/dist/src/transfer/leaseTransfer.js +74 -0
- package/dist/src/transfer/multipartTransfer.d.ts +23 -0
- package/dist/src/transfer/multipartTransfer.d.ts.map +1 -0
- package/dist/src/transfer/multipartTransfer.js +94 -0
- package/dist/src/transfer/negotiatePartSize.d.ts +10 -0
- package/dist/src/transfer/negotiatePartSize.d.ts.map +1 -0
- package/dist/src/transfer/negotiatePartSize.js +27 -0
- package/dist/src/transfer/patternTransfer.d.ts +12 -0
- package/dist/src/transfer/patternTransfer.d.ts.map +1 -0
- package/dist/src/transfer/patternTransfer.js +61 -0
- package/dist/src/transfer/rangeReadableMultipartReader.d.ts +8 -0
- package/dist/src/transfer/rangeReadableMultipartReader.d.ts.map +1 -0
- package/dist/src/transfer/rangeReadableMultipartReader.js +19 -0
- package/dist/src/transfer/recursiveTransfer.d.ts +13 -0
- package/dist/src/transfer/recursiveTransfer.d.ts.map +1 -0
- package/dist/src/transfer/recursiveTransfer.js +112 -0
- package/dist/src/transfer/streamTransfer.d.ts +32 -0
- package/dist/src/transfer/streamTransfer.d.ts.map +1 -0
- package/dist/src/transfer/streamTransfer.js +186 -0
- package/dist/src/transfer/transferEntry.d.ts +25 -0
- package/dist/src/transfer/transferEntry.d.ts.map +1 -0
- package/dist/src/transfer/transferEntry.js +136 -0
- package/dist/src/util/abort.d.ts +12 -0
- package/dist/src/util/abort.d.ts.map +1 -0
- package/dist/src/util/abort.js +39 -0
- package/dist/src/util/applyPayloadConverter.d.ts +9 -0
- package/dist/src/util/applyPayloadConverter.d.ts.map +1 -0
- package/dist/src/util/applyPayloadConverter.js +26 -0
- package/dist/src/util/describePath.d.ts +6 -0
- package/dist/src/util/describePath.d.ts.map +1 -0
- package/dist/src/util/describePath.js +9 -0
- package/dist/src/util/globToRegex.d.ts +8 -0
- package/dist/src/util/globToRegex.d.ts.map +1 -0
- package/dist/src/util/globToRegex.js +35 -0
- package/dist/src/util/itemLength.d.ts +4 -0
- package/dist/src/util/itemLength.d.ts.map +1 -0
- package/dist/src/util/itemLength.js +5 -0
- package/dist/src/util/keys.d.ts +8 -0
- package/dist/src/util/keys.d.ts.map +1 -0
- package/dist/src/util/keys.js +19 -0
- package/dist/src/util/withParentOperationId.d.ts +4 -0
- package/dist/src/util/withParentOperationId.d.ts.map +1 -0
- package/dist/src/util/withParentOperationId.js +12 -0
- package/index.ts +11 -4
- package/package.json +6 -5
- package/src/concurrency/AsyncChannel.ts +52 -0
- package/src/concurrency/ConcurrencyLimiter.ts +72 -0
- package/src/concurrency/mapAsyncIterableConcurrently.ts +66 -0
- package/src/decorators/locallyCached.ts +30 -22
- package/src/decorators/seekable.ts +7 -7
- package/src/registry/ProviderRegistry.ts +241 -0
- package/src/registry/detectProtocol.ts +16 -0
- package/src/registry/negotiateTransfer.ts +121 -0
- package/src/retry/RetryOptions.ts +26 -0
- package/src/{withRetry.ts → retry/withRetry.ts} +2 -12
- package/src/transfer/EntryContext.ts +17 -0
- package/src/transfer/TransferOptions.ts +45 -0
- package/src/transfer/TransferResult.ts +8 -0
- package/src/transfer/aggregateResults.ts +15 -0
- package/src/transfer/copyMove.ts +113 -0
- package/src/transfer/leaseTransfer.ts +93 -0
- package/src/transfer/multipartTransfer.ts +119 -0
- package/src/transfer/negotiatePartSize.ts +40 -0
- package/src/transfer/patternTransfer.ts +84 -0
- package/src/transfer/rangeReadableMultipartReader.ts +30 -0
- package/src/transfer/recursiveTransfer.ts +147 -0
- package/src/transfer/streamTransfer.ts +214 -0
- package/src/transfer/transferEntry.ts +185 -0
- package/src/util/abort.ts +42 -0
- package/src/util/applyPayloadConverter.ts +37 -0
- package/src/util/describePath.ts +12 -0
- package/src/util/globToRegex.ts +32 -0
- package/src/util/itemLength.ts +6 -0
- package/src/util/keys.ts +21 -0
- package/src/util/withParentOperationId.ts +17 -0
- package/dist/src/ConcurrencyLimiter.d.ts.map +0 -1
- package/dist/src/ConcurrencyLimiter.js +0 -161
- package/dist/src/ProviderRegistry.d.ts +0 -14
- package/dist/src/ProviderRegistry.d.ts.map +0 -1
- package/dist/src/ProviderRegistry.js +0 -22
- package/dist/src/chunkLength.d.ts +0 -3
- package/dist/src/chunkLength.d.ts.map +0 -1
- package/dist/src/chunkLength.js +0 -4
- package/dist/src/copyMove.d.ts +0 -46
- package/dist/src/copyMove.d.ts.map +0 -1
- package/dist/src/copyMove.js +0 -280
- package/dist/src/withRetry.d.ts.map +0 -1
- package/src/ConcurrencyLimiter.ts +0 -178
- package/src/ProviderRegistry.ts +0 -32
- package/src/chunkLength.ts +0 -5
- package/src/copyMove.ts +0 -458
package/dist/src/copyMove.js
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { adaptReadableStream, identityChunkConverter, } from "@flowscripter/pluggable-io-framework-api";
|
|
2
|
-
import { chunkLength } from "./chunkLength.js";
|
|
3
|
-
import { defaultConcurrencyLimiter, mapAsyncIterableConcurrently, } from "./ConcurrencyLimiter.js";
|
|
4
|
-
import { withRetry } from "./withRetry.js";
|
|
5
|
-
const DEFAULT_MULTIPART_THRESHOLD = 64 * 1024 * 1024;
|
|
6
|
-
const DEFAULT_PART_SIZE = 8 * 1024 * 1024;
|
|
7
|
-
const DEFAULT_RETRY = { maxRetries: 3 };
|
|
8
|
-
const UNCONSTRAINED_PART_SIZE = {
|
|
9
|
-
minPartSize: 0,
|
|
10
|
-
maxPartSize: Infinity,
|
|
11
|
-
maxParts: Infinity,
|
|
12
|
-
defaultPartSize: DEFAULT_PART_SIZE,
|
|
13
|
-
};
|
|
14
|
-
/** Wraps `hooks` so any `onProgress` event passing through gets `parentOperationId` filled in, unless the emitter already set one (nested hierarchy). */
|
|
15
|
-
function withParentOperationId(hooks, parentOperationId) {
|
|
16
|
-
if (!hooks || parentOperationId === undefined) {
|
|
17
|
-
return hooks ?? {};
|
|
18
|
-
}
|
|
19
|
-
return {
|
|
20
|
-
...hooks,
|
|
21
|
-
onProgress: hooks.onProgress
|
|
22
|
-
? (event) => hooks.onProgress?.({ parentOperationId, ...event })
|
|
23
|
-
: undefined,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
function joinPath(base, relative) {
|
|
27
|
-
if (relative === "")
|
|
28
|
-
return base;
|
|
29
|
-
if (base === "")
|
|
30
|
-
return relative;
|
|
31
|
-
return `${base.replace(/\/+$/, "")}/${relative}`;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Reconciles `source`'s and `sink`'s {@link PartSizeConstraints} for a
|
|
35
|
-
* transfer of `totalSize` bytes into a single part size satisfying both.
|
|
36
|
-
* Returns `undefined` when the bounds are mutually infeasible (source's
|
|
37
|
-
* minimum exceeds sink's maximum) - the caller should fall back to plain
|
|
38
|
-
* streaming rather than throwing.
|
|
39
|
-
*/
|
|
40
|
-
function negotiatePartSize(source, sink, totalSize) {
|
|
41
|
-
const sourceConstraints = source.getPartSizeConstraints?.(totalSize) ?? UNCONSTRAINED_PART_SIZE;
|
|
42
|
-
const sinkConstraints = sink.getPartSizeConstraints?.(totalSize) ?? UNCONSTRAINED_PART_SIZE;
|
|
43
|
-
const minPartSize = Math.max(sourceConstraints.minPartSize, sinkConstraints.minPartSize);
|
|
44
|
-
const maxPartSize = Math.min(sourceConstraints.maxPartSize, sinkConstraints.maxPartSize);
|
|
45
|
-
const maxParts = Math.min(sourceConstraints.maxParts, sinkConstraints.maxParts);
|
|
46
|
-
if (minPartSize > maxPartSize) {
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
const byMaxParts = Math.ceil(totalSize / maxParts);
|
|
50
|
-
const candidate = Math.max(minPartSize, sourceConstraints.defaultPartSize, sinkConstraints.defaultPartSize, byMaxParts);
|
|
51
|
-
return Math.min(candidate, maxPartSize);
|
|
52
|
-
}
|
|
53
|
-
function canUseMultipart(source, sink) {
|
|
54
|
-
return (typeof source.getMultipartReader === "function" && typeof sink.getMultipartWriter === "function");
|
|
55
|
-
}
|
|
56
|
-
async function streamingTransfer(source, sourcePath, sink, destPath, operationId, parentOperationId, options, totalBytes) {
|
|
57
|
-
const hooks = withParentOperationId(options.telemetry, parentOperationId);
|
|
58
|
-
const readable = await source.getReadableStream(sourcePath);
|
|
59
|
-
const writable = await sink.getWritableStream(destPath);
|
|
60
|
-
// The source/sink kind mismatch is decided ONCE here, not per chunk.
|
|
61
|
-
const adaptedStream = adaptReadableStream(readable.stream, readable.kind, writable.kind, options.chunkConverter ?? identityChunkConverter);
|
|
62
|
-
const reader = adaptedStream.getReader();
|
|
63
|
-
const writer = writable.stream.getWriter();
|
|
64
|
-
let bytesProcessed = 0;
|
|
65
|
-
try {
|
|
66
|
-
for (;;) {
|
|
67
|
-
const { done, value } = await reader.read();
|
|
68
|
-
if (done)
|
|
69
|
-
break;
|
|
70
|
-
await writer.write(value);
|
|
71
|
-
bytesProcessed += chunkLength(value);
|
|
72
|
-
hooks.onProgress?.({ operationId, type: "copy", bytesProcessed, totalBytes });
|
|
73
|
-
}
|
|
74
|
-
await writer.close();
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
await writer.abort(error);
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
async function multipartTransfer(source, sourcePath, sink, destPath, operationId, parentOperationId, partSize, limiter, options, totalBytes) {
|
|
82
|
-
const hooks = withParentOperationId(options.telemetry, parentOperationId);
|
|
83
|
-
let bytesProcessed = 0;
|
|
84
|
-
async function transferPart(sourcePart) {
|
|
85
|
-
const partOperationId = crypto.randomUUID();
|
|
86
|
-
// Kind mismatch decided once per part, not once per chunk within it.
|
|
87
|
-
const adaptedStream = adaptReadableStream(sourcePart.stream, sourcePart.kind, sink.kind, options.chunkConverter ?? identityChunkConverter);
|
|
88
|
-
const reader = adaptedStream.getReader();
|
|
89
|
-
const chunks = [];
|
|
90
|
-
let partBytesProcessed = 0;
|
|
91
|
-
for (;;) {
|
|
92
|
-
const { done, value } = await reader.read();
|
|
93
|
-
if (done)
|
|
94
|
-
break;
|
|
95
|
-
chunks.push(value);
|
|
96
|
-
const length = chunkLength(value);
|
|
97
|
-
partBytesProcessed += length;
|
|
98
|
-
bytesProcessed += length;
|
|
99
|
-
hooks.onProgress?.({
|
|
100
|
-
operationId: partOperationId,
|
|
101
|
-
parentOperationId: operationId,
|
|
102
|
-
type: "copy",
|
|
103
|
-
bytesProcessed: partBytesProcessed,
|
|
104
|
-
});
|
|
105
|
-
hooks.onProgress?.({ operationId, type: "copy", bytesProcessed, totalBytes });
|
|
106
|
-
}
|
|
107
|
-
return {
|
|
108
|
-
index: sourcePart.index,
|
|
109
|
-
offset: sourcePart.offset,
|
|
110
|
-
kind: sink.kind,
|
|
111
|
-
stream: new ReadableStream({
|
|
112
|
-
start(controller) {
|
|
113
|
-
for (const chunk of chunks)
|
|
114
|
-
controller.enqueue(chunk);
|
|
115
|
-
controller.close();
|
|
116
|
-
},
|
|
117
|
-
}),
|
|
118
|
-
complete: () => sourcePart.complete(),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
const writer = sink.getMultipartWriter(destPath, partSize);
|
|
122
|
-
const transferredParts = mapAsyncIterableConcurrently(source.getMultipartReader(sourcePath, partSize), transferPart, limiter);
|
|
123
|
-
await writer.write(transferredParts);
|
|
124
|
-
}
|
|
125
|
-
async function copySingle(source, sourcePath, sink, destPath, options, parentOperationId, properties) {
|
|
126
|
-
const operationId = crypto.randomUUID();
|
|
127
|
-
if (source.canDirectTransfer?.(sink) && source.directCopy) {
|
|
128
|
-
await source.directCopy(sourcePath, destPath, {
|
|
129
|
-
operationId,
|
|
130
|
-
hooks: withParentOperationId(options.telemetry, parentOperationId),
|
|
131
|
-
});
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
const threshold = options.multipartThreshold ?? DEFAULT_MULTIPART_THRESHOLD;
|
|
135
|
-
const size = properties.size;
|
|
136
|
-
const partSize = size !== undefined && size >= threshold && canUseMultipart(source, sink)
|
|
137
|
-
? negotiatePartSize(source, sink, size)
|
|
138
|
-
: undefined;
|
|
139
|
-
const limiter = options.concurrencyLimiter ?? defaultConcurrencyLimiter;
|
|
140
|
-
const retryOptions = options.retry ?? DEFAULT_RETRY;
|
|
141
|
-
await withRetry(async () => {
|
|
142
|
-
if (partSize !== undefined) {
|
|
143
|
-
await multipartTransfer(source, sourcePath, sink, destPath, operationId, parentOperationId, partSize, limiter, options, size);
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
await streamingTransfer(source, sourcePath, sink, destPath, operationId, parentOperationId, options, size);
|
|
147
|
-
}
|
|
148
|
-
}, retryOptions);
|
|
149
|
-
}
|
|
150
|
-
async function moveSingle(source, sourcePath, sink, destPath, options, parentOperationId, properties) {
|
|
151
|
-
if (source.canDirectTransfer?.(sink) && source.directMove) {
|
|
152
|
-
const operationId = crypto.randomUUID();
|
|
153
|
-
await source.directMove(sourcePath, destPath, {
|
|
154
|
-
operationId,
|
|
155
|
-
hooks: withParentOperationId(options.telemetry, parentOperationId),
|
|
156
|
-
});
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
await copySingle(source, sourcePath, sink, destPath, options, parentOperationId, properties);
|
|
160
|
-
await withRetry(() => source.delete(sourcePath), options.retry ?? DEFAULT_RETRY);
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Resolves the effective destination root for a recursive transfer,
|
|
164
|
-
* following `cp -r`/`mv` semantics: a destination that doesn't exist becomes
|
|
165
|
-
* the copy itself; an existing folder gets the source nested inside it as
|
|
166
|
-
* `dest/<source-basename>`; an existing file is rejected.
|
|
167
|
-
*/
|
|
168
|
-
async function resolveRecursiveDestRoot(sourcePath, sink, destPath) {
|
|
169
|
-
let destProperties;
|
|
170
|
-
try {
|
|
171
|
-
destProperties = await sink.getProperties(destPath);
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
destProperties = undefined;
|
|
175
|
-
}
|
|
176
|
-
if (destProperties === undefined) {
|
|
177
|
-
return destPath;
|
|
178
|
-
}
|
|
179
|
-
if (!destProperties.isFolder) {
|
|
180
|
-
throw new Error(`Cannot copy/move folder "${sourcePath}" onto existing file "${destPath}"`);
|
|
181
|
-
}
|
|
182
|
-
const baseName = sourcePath.replace(/\/+$/, "").split("/").pop() || sourcePath;
|
|
183
|
-
return joinPath(destPath, baseName);
|
|
184
|
-
}
|
|
185
|
-
async function recursiveTransfer(source, sourcePath, sink, destPath, options, mode) {
|
|
186
|
-
const resolvedDestRoot = await resolveRecursiveDestRoot(sourcePath, sink, destPath);
|
|
187
|
-
if (source.canDirectTransfer?.(sink) && source.supportsRecursiveDirectTransfer) {
|
|
188
|
-
const telemetry = { operationId: crypto.randomUUID(), hooks: options.telemetry ?? {} };
|
|
189
|
-
if (mode === "copy" && source.directCopy) {
|
|
190
|
-
await source.directCopy(sourcePath, resolvedDestRoot, telemetry);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
if (mode === "move" && source.directMove) {
|
|
194
|
-
await source.directMove(sourcePath, resolvedDestRoot, telemetry);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
const recursiveOperationId = crypto.randomUUID();
|
|
199
|
-
const limiter = options.concurrencyLimiter ?? defaultConcurrencyLimiter;
|
|
200
|
-
let discovered = 0;
|
|
201
|
-
let completed = 0;
|
|
202
|
-
const reportProgress = () => {
|
|
203
|
-
options.telemetry?.onProgress?.({
|
|
204
|
-
operationId: recursiveOperationId,
|
|
205
|
-
type: mode,
|
|
206
|
-
bytesProcessed: 0,
|
|
207
|
-
itemsProcessed: completed,
|
|
208
|
-
totalItems: discovered,
|
|
209
|
-
});
|
|
210
|
-
};
|
|
211
|
-
async function processEntry(entry) {
|
|
212
|
-
discovered += 1;
|
|
213
|
-
reportProgress();
|
|
214
|
-
const entrySourcePath = joinPath(sourcePath, entry.path);
|
|
215
|
-
const entryDestPath = joinPath(resolvedDestRoot, entry.path);
|
|
216
|
-
if (entry.properties.isFolder) {
|
|
217
|
-
await sink.createFolder?.(entryDestPath);
|
|
218
|
-
}
|
|
219
|
-
else if (mode === "copy") {
|
|
220
|
-
await copySingle(source, entrySourcePath, sink, entryDestPath, options, recursiveOperationId, entry.properties);
|
|
221
|
-
}
|
|
222
|
-
else if (source.canDirectTransfer?.(sink) && source.directMove) {
|
|
223
|
-
// A per-file directMove deletes its source atomically as part of the
|
|
224
|
-
// provider's own operation - safe to do immediately. The non-direct
|
|
225
|
-
// case below must NOT delete per entry: only after every entry
|
|
226
|
-
// succeeds does the caller delete the whole source root once.
|
|
227
|
-
const operationId = crypto.randomUUID();
|
|
228
|
-
await source.directMove(entrySourcePath, entryDestPath, {
|
|
229
|
-
operationId,
|
|
230
|
-
hooks: withParentOperationId(options.telemetry, recursiveOperationId),
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
await copySingle(source, entrySourcePath, sink, entryDestPath, options, recursiveOperationId, entry.properties);
|
|
235
|
-
}
|
|
236
|
-
completed += 1;
|
|
237
|
-
reportProgress();
|
|
238
|
-
}
|
|
239
|
-
for await (const _entry of mapAsyncIterableConcurrently(source.list(sourcePath, { recursive: true }), processEntry, limiter)) {
|
|
240
|
-
// work happens inside processEntry - nothing to do with the (void) result.
|
|
241
|
-
}
|
|
242
|
-
if (mode === "move") {
|
|
243
|
-
await withRetry(() => source.delete(sourcePath), options.retry ?? DEFAULT_RETRY);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Copies `sourcePath` on `source` to `destPath` on `sink`. When `sourcePath`
|
|
248
|
-
* is a folder, recurses: uses a folder-aware `directCopy` when the source
|
|
249
|
-
* declares `supportsRecursiveDirectTransfer`, otherwise lists the folder and
|
|
250
|
-
* transfers each entry (bounded by `options.concurrencyLimiter`), following
|
|
251
|
-
* `cp -r` target semantics (see {@link resolveRecursiveDestRoot}).
|
|
252
|
-
*
|
|
253
|
-
* For a single file: uses `source.directCopy` when
|
|
254
|
-
* `source.canDirectTransfer?.(sink)` reports eligibility. Otherwise falls
|
|
255
|
-
* back to multipart transfer (when both sides support it, size crosses
|
|
256
|
-
* `options.multipartThreshold`, and part-size negotiation succeeds) or plain
|
|
257
|
-
* streaming - retried per `options.retry` on `TransientIOError`.
|
|
258
|
-
*/
|
|
259
|
-
export async function copy(source, sourcePath, sink, destPath, options = {}) {
|
|
260
|
-
const properties = await source.getProperties(sourcePath);
|
|
261
|
-
if (properties.isFolder) {
|
|
262
|
-
await recursiveTransfer(source, sourcePath, sink, destPath, options, "copy");
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
await copySingle(source, sourcePath, sink, destPath, options, undefined, properties);
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Moves `sourcePath` on `source` to `destPath` on `sink`. Folder handling
|
|
269
|
-
* mirrors {@link copy}. For a single file, uses `source.directMove` when
|
|
270
|
-
* eligible, otherwise performs a {@link copy} followed by deleting the
|
|
271
|
-
* source item (each retried independently per `options.retry`).
|
|
272
|
-
*/
|
|
273
|
-
export async function move(source, sourcePath, sink, destPath, options = {}) {
|
|
274
|
-
const properties = await source.getProperties(sourcePath);
|
|
275
|
-
if (properties.isFolder) {
|
|
276
|
-
await recursiveTransfer(source, sourcePath, sink, destPath, options, "move");
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
await moveSingle(source, sourcePath, sink, destPath, options, undefined, properties);
|
|
280
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"withRetry.d.ts","sourceRoot":"","sources":["../../src/withRetry.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,sGAAsG;IACtG,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CAClD;AAMD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC,CAc1F"}
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
const DEFAULT_MAX_CONCURRENCY = 4;
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A semaphore bounding how many `run()` callbacks execute at once. A single
|
|
5
|
-
* shared instance (see {@link defaultConcurrencyLimiter}) enforces its cap
|
|
6
|
-
* *globally* across concurrently invoked `copy()`/`move()` calls, not just
|
|
7
|
-
* within one call - two folder copies started at the same time and sharing
|
|
8
|
-
* a limiter will never together exceed its `maxConcurrency`.
|
|
9
|
-
*/
|
|
10
|
-
export class ConcurrencyLimiter {
|
|
11
|
-
#maxConcurrency: number;
|
|
12
|
-
#active = 0;
|
|
13
|
-
#queue: (() => void)[] = [];
|
|
14
|
-
|
|
15
|
-
public constructor(maxConcurrency: number) {
|
|
16
|
-
ConcurrencyLimiter.#validate(maxConcurrency);
|
|
17
|
-
this.#maxConcurrency = maxConcurrency;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
public get maxConcurrency(): number {
|
|
21
|
-
return this.#maxConcurrency;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
public setMaxConcurrency(maxConcurrency: number): void {
|
|
25
|
-
ConcurrencyLimiter.#validate(maxConcurrency);
|
|
26
|
-
this.#maxConcurrency = maxConcurrency;
|
|
27
|
-
this.#drain();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
public async run<T>(fn: () => Promise<T>): Promise<T> {
|
|
31
|
-
await this.#acquire();
|
|
32
|
-
try {
|
|
33
|
-
return await fn();
|
|
34
|
-
} finally {
|
|
35
|
-
this.#active -= 1;
|
|
36
|
-
this.#drain();
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
#acquire(): Promise<void> {
|
|
41
|
-
if (this.#active < this.#maxConcurrency) {
|
|
42
|
-
this.#active += 1;
|
|
43
|
-
return Promise.resolve();
|
|
44
|
-
}
|
|
45
|
-
return new Promise((resolve) => {
|
|
46
|
-
this.#queue.push(() => {
|
|
47
|
-
this.#active += 1;
|
|
48
|
-
resolve();
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
#drain(): void {
|
|
54
|
-
while (this.#active < this.#maxConcurrency && this.#queue.length > 0) {
|
|
55
|
-
this.#queue.shift()?.();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
static #validate(maxConcurrency: number): void {
|
|
60
|
-
if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
|
|
61
|
-
throw new Error(`maxConcurrency must be a positive integer, got ${maxConcurrency}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Process-wide default limiter, used by `copy()`/`move()` whenever
|
|
68
|
-
* `TransferOptions.concurrencyLimiter` is omitted - so any two calls in the
|
|
69
|
-
* same process share one cap unless a caller explicitly opts out with its
|
|
70
|
-
* own instance (e.g. for test isolation).
|
|
71
|
-
*/
|
|
72
|
-
export const defaultConcurrencyLimiter = new ConcurrencyLimiter(DEFAULT_MAX_CONCURRENCY);
|
|
73
|
-
|
|
74
|
-
/** Minimal single-consumer async FIFO queue backing {@link mapAsyncIterableConcurrently}. */
|
|
75
|
-
class AsyncChannel<T> implements AsyncIterable<T> {
|
|
76
|
-
#items: T[] = [];
|
|
77
|
-
#waiters: { resolve: (result: IteratorResult<T>) => void; reject: (error: unknown) => void }[] =
|
|
78
|
-
[];
|
|
79
|
-
#closed = false;
|
|
80
|
-
#hasError = false;
|
|
81
|
-
#error: unknown;
|
|
82
|
-
|
|
83
|
-
push(item: T): void {
|
|
84
|
-
const waiter = this.#waiters.shift();
|
|
85
|
-
if (waiter) {
|
|
86
|
-
waiter.resolve({ value: item, done: false });
|
|
87
|
-
} else {
|
|
88
|
-
this.#items.push(item);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
close(): void {
|
|
93
|
-
this.#closed = true;
|
|
94
|
-
for (const waiter of this.#waiters.splice(0)) {
|
|
95
|
-
if (this.#hasError) {
|
|
96
|
-
waiter.reject(this.#error);
|
|
97
|
-
} else {
|
|
98
|
-
waiter.resolve({ value: undefined as never, done: true });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
fail(error: unknown): void {
|
|
104
|
-
this.#hasError = true;
|
|
105
|
-
this.#error = error;
|
|
106
|
-
this.close();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async #next(): Promise<IteratorResult<T>> {
|
|
110
|
-
if (this.#items.length > 0) {
|
|
111
|
-
return { value: this.#items.shift() as T, done: false };
|
|
112
|
-
}
|
|
113
|
-
if (this.#closed) {
|
|
114
|
-
if (this.#hasError) {
|
|
115
|
-
throw this.#error;
|
|
116
|
-
}
|
|
117
|
-
return { value: undefined as never, done: true };
|
|
118
|
-
}
|
|
119
|
-
return new Promise((resolve, reject) => this.#waiters.push({ resolve, reject }));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
123
|
-
return { next: () => this.#next() };
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Pulls items from `source` and runs `fn` on each, bounded by `limiter`:
|
|
129
|
-
* exactly `limiter.maxConcurrency` worker loops each pull-then-process one
|
|
130
|
-
* item at a time through `limiter.run`, so at most that many items are ever
|
|
131
|
-
* pulled/in-flight at once (important for sources like a multipart reader
|
|
132
|
-
* where pulling an item eagerly opens a file/network handle). Results are
|
|
133
|
-
* yielded in completion order, not source order. If `fn` throws, no new
|
|
134
|
-
* items are pulled but already-in-flight ones are allowed to finish before
|
|
135
|
-
* the error is rethrown from the returned iterable.
|
|
136
|
-
*/
|
|
137
|
-
export function mapAsyncIterableConcurrently<T, R>(
|
|
138
|
-
source: AsyncIterable<T>,
|
|
139
|
-
fn: (item: T) => Promise<R>,
|
|
140
|
-
limiter: ConcurrencyLimiter,
|
|
141
|
-
): AsyncIterable<R> {
|
|
142
|
-
const channel = new AsyncChannel<R>();
|
|
143
|
-
const iterator = source[Symbol.asyncIterator]();
|
|
144
|
-
let stopped = false;
|
|
145
|
-
let firstError: unknown;
|
|
146
|
-
let errorRecorded = false;
|
|
147
|
-
|
|
148
|
-
async function worker(): Promise<void> {
|
|
149
|
-
for (;;) {
|
|
150
|
-
if (stopped) return;
|
|
151
|
-
const { done, value } = await iterator.next();
|
|
152
|
-
if (done) return;
|
|
153
|
-
try {
|
|
154
|
-
const result = await limiter.run(() => fn(value));
|
|
155
|
-
channel.push(result);
|
|
156
|
-
} catch (error) {
|
|
157
|
-
stopped = true;
|
|
158
|
-
if (!errorRecorded) {
|
|
159
|
-
errorRecorded = true;
|
|
160
|
-
firstError = error;
|
|
161
|
-
}
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
void (async () => {
|
|
168
|
-
const workerCount = Math.max(1, limiter.maxConcurrency);
|
|
169
|
-
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
170
|
-
if (errorRecorded) {
|
|
171
|
-
channel.fail(firstError);
|
|
172
|
-
} else {
|
|
173
|
-
channel.close();
|
|
174
|
-
}
|
|
175
|
-
})();
|
|
176
|
-
|
|
177
|
-
return channel;
|
|
178
|
-
}
|
package/src/ProviderRegistry.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { PluginManager } from "@flowscripter/dynamic-plugin-framework";
|
|
2
|
-
import {
|
|
3
|
-
PLUGGABLE_IO_FRAMEWORK_PROVIDER_FACTORY_EXTENSION_POINT,
|
|
4
|
-
type IOProvider,
|
|
5
|
-
type IOProviderFactory,
|
|
6
|
-
} from "@flowscripter/pluggable-io-framework-api";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Wraps a `dynamic-plugin-framework` {@link PluginManager} to discover and
|
|
10
|
-
* instantiate pluggable-io-framework source/sink provider plugins.
|
|
11
|
-
*/
|
|
12
|
-
export class ProviderRegistry {
|
|
13
|
-
public constructor(private readonly pluginManager: PluginManager) {}
|
|
14
|
-
|
|
15
|
-
public async discover(): Promise<void> {
|
|
16
|
-
await this.pluginManager.registerExtensions(
|
|
17
|
-
PLUGGABLE_IO_FRAMEWORK_PROVIDER_FACTORY_EXTENSION_POINT,
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
public async listAvailableProviders() {
|
|
22
|
-
return this.pluginManager.getRegisteredExtensions(
|
|
23
|
-
PLUGGABLE_IO_FRAMEWORK_PROVIDER_FACTORY_EXTENSION_POINT,
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
public async createProvider(extensionHandle: string, config: unknown): Promise<IOProvider> {
|
|
28
|
-
const factory = (await this.pluginManager.instantiate(extensionHandle)) as IOProviderFactory;
|
|
29
|
-
const validatedConfig = factory.configSchema.parse(config);
|
|
30
|
-
return factory.createProvider(validatedConfig);
|
|
31
|
-
}
|
|
32
|
-
}
|