@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/README.md
CHANGED
|
@@ -5,202 +5,71 @@
|
|
|
5
5
|
[](https://flowscripter.github.io/pluggable-io-framework/index.html)
|
|
6
6
|
[](https://github.com/flowscripter/pluggable-io-framework/blob/main/LICENSE)
|
|
7
7
|
|
|
8
|
-
> A pluggable source/sink IO framework using
|
|
8
|
+
> A pluggable source/sink IO framework using
|
|
9
|
+
> [dynamic-plugin-framework](https://github.com/flowscripter/dynamic-plugin-framework)
|
|
9
10
|
|
|
10
11
|
## Key Features
|
|
11
12
|
|
|
12
|
-
-
|
|
13
|
-
`
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- `seekable` wraps a handle that supports `RangeReadable` with a single
|
|
41
|
-
logical stream whose read position can be jumped via `seek(offset)`,
|
|
42
|
-
instead of requiring a fresh stream per range.
|
|
43
|
-
- `locallyCached` wraps a `StreamHandle` factory so the underlying source
|
|
44
|
-
is read at most once - later calls replay cached chunks in memory
|
|
45
|
-
without touching the source again.
|
|
46
|
-
- See
|
|
47
|
-
[io-plugin-filesystem](https://github.com/flowscripter/io-plugin-filesystem)
|
|
48
|
-
for a reference local filesystem source/sink plugin.
|
|
49
|
-
|
|
50
|
-
## Bun Module Usage
|
|
51
|
-
|
|
52
|
-
Add the module:
|
|
53
|
-
|
|
54
|
-
`bun add @flowscripter/pluggable-io-framework`
|
|
55
|
-
|
|
56
|
-
Discover and use a provider:
|
|
13
|
+
- Protocol-aware locations: `file:///data/a.txt`, `s3://bucket/key`,
|
|
14
|
+
`https://host/a` and composite schemes such as `tams+https:` select the
|
|
15
|
+
installed provider plugin for that protocol. Bare paths default to `file`.
|
|
16
|
+
- A `ProviderRegistry` keyed by protocol and payload kind, with more than one
|
|
17
|
+
implementation of a protocol installed side by side (e.g. JS and native
|
|
18
|
+
`file` providers).
|
|
19
|
+
- Negotiation of the payload kind, memory domain and payload type between
|
|
20
|
+
source and sink, falling back to the cheapest registered payload converter
|
|
21
|
+
plugin.
|
|
22
|
+
- `copy`/`move` over explicit entry, container and glob pattern targets,
|
|
23
|
+
with `cp -r` semantics for containers.
|
|
24
|
+
- Direct provider-to-provider transfers, multipart transfers with negotiated
|
|
25
|
+
part sizes, zero-copy lease transfers into sink-provided buffers, or plain
|
|
26
|
+
streaming - chosen automatically.
|
|
27
|
+
- Graceful stop and cancellation, per-part retries, resumable writes and
|
|
28
|
+
automatic reconnection of live sources with gap reporting.
|
|
29
|
+
- Concurrency-bounded recursive and pattern transfers with progress
|
|
30
|
+
telemetry.
|
|
31
|
+
- Provider composition: a provider can resolve other installed providers.
|
|
32
|
+
- Stream decorators: `seekable` and `locallyCached`.
|
|
33
|
+
- Bun based, written in TypeScript, based on native JavaScript modules.
|
|
34
|
+
|
|
35
|
+
## Usage Examples
|
|
36
|
+
|
|
37
|
+
- [flowscripter-io-cli](https://github.com/flowscripter/flowscripter-io-cli)
|
|
38
|
+
is a CLI application based on this framework.
|
|
39
|
+
- [io-plugin-filesystem](https://github.com/flowscripter/io-plugin-filesystem)
|
|
40
|
+
is the reference `file` provider plugin.
|
|
57
41
|
|
|
58
42
|
```typescript
|
|
59
43
|
import {
|
|
60
44
|
DefaultPluginManager,
|
|
61
45
|
LocalFolderPluginRepository,
|
|
62
46
|
} from "@flowscripter/dynamic-plugin-framework";
|
|
63
|
-
import {
|
|
47
|
+
import { copy, ProviderRegistry } from "@flowscripter/pluggable-io-framework";
|
|
64
48
|
|
|
65
|
-
const
|
|
66
|
-
|
|
49
|
+
const registry = new ProviderRegistry(
|
|
50
|
+
new DefaultPluginManager([new LocalFolderPluginRepository("./plugins")]),
|
|
51
|
+
);
|
|
67
52
|
await registry.discover();
|
|
68
53
|
|
|
69
|
-
const
|
|
70
|
-
|
|
54
|
+
const { source, dest, options } = await registry.createProvidersForTransfer(
|
|
55
|
+
"file:///data/in",
|
|
56
|
+
"s3://bucket/out",
|
|
57
|
+
);
|
|
71
58
|
|
|
72
|
-
await copy(provider,
|
|
59
|
+
const result = await copy(source.provider, source.target, dest.provider, dest.target, {
|
|
60
|
+
...options,
|
|
73
61
|
telemetry: { onProgress: (event) => console.log(event) },
|
|
74
62
|
});
|
|
63
|
+
console.log(result.path);
|
|
75
64
|
```
|
|
76
65
|
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
The following example project is available:
|
|
80
|
-
|
|
81
|
-
- [flowscripter-io-cli](https://github.com/flowscripter/flowscripter-io-cli) is
|
|
82
|
-
an example CLI application based on this framework.
|
|
83
|
-
|
|
84
|
-
## Part-size negotiation
|
|
85
|
-
|
|
86
|
-
Before a multipart transfer, `copy()`/`move()` call `getPartSizeConstraints(totalSize)`
|
|
87
|
-
on both `source` and `sink` (when implemented - a missing implementation is
|
|
88
|
-
treated as unconstrained) and reconcile the two into a single part size:
|
|
89
|
-
the larger of the two minimums, clamped to the smaller of the two maximums,
|
|
90
|
-
bumped up if needed so the total number of parts never exceeds the smaller
|
|
91
|
-
of the two `maxParts` (e.g. S3's 10000-part cap on a very large file). If
|
|
92
|
-
the reconciled bounds are mutually infeasible, the transfer falls back to
|
|
93
|
-
plain streaming rather than failing.
|
|
94
|
-
|
|
95
|
-
## Recursive copy/move
|
|
96
|
-
|
|
97
|
-
When `sourcePath` is a folder, `copy()`/`move()` follow `cp -r`/`mv`
|
|
98
|
-
semantics for the destination: a `destPath` that doesn't exist becomes the
|
|
99
|
-
copy itself; an existing folder gets the source nested inside it as
|
|
100
|
-
`destPath/<source-basename>`; an existing file is rejected.
|
|
101
|
-
|
|
102
|
-
If the source provider declares `supportsRecursiveDirectTransfer: true` and
|
|
103
|
-
is direct-transfer-eligible with the sink, the whole folder is handed to a
|
|
104
|
-
single `directCopy`/`directMove` call. Otherwise the folder is listed
|
|
105
|
-
(`source.list(path, { recursive: true })`) and each entry is transferred
|
|
106
|
-
individually - folders via the optional `createFolder` capability (so empty
|
|
107
|
-
folders are preserved), files via the normal single-file path (which may
|
|
108
|
-
still resolve to a per-file `directCopy`/`directMove`). For a recursive
|
|
109
|
-
move without any direct-transfer capability, every entry is copied first;
|
|
110
|
-
the source folder is deleted as a single recursive `delete()` call only
|
|
111
|
-
after every entry has succeeded.
|
|
112
|
-
|
|
113
|
-
## Concurrency
|
|
114
|
-
|
|
115
|
-
Multipart parts and recursive-copy/move entries are bounded by a
|
|
116
|
-
`ConcurrencyLimiter` (`TransferOptions.concurrencyLimiter`), defaulting to
|
|
117
|
-
the exported `defaultConcurrencyLimiter` singleton - so two `copy()`/`move()`
|
|
118
|
-
calls in the same process share one cap unless a caller passes its own
|
|
119
|
-
isolated instance:
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
import { defaultConcurrencyLimiter } from "@flowscripter/pluggable-io-framework";
|
|
123
|
-
|
|
124
|
-
defaultConcurrencyLimiter.setMaxConcurrency(8);
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
## Retries
|
|
128
|
-
|
|
129
|
-
The non-direct transfer path (plain streaming, multipart, and the
|
|
130
|
-
copy-then-delete in a non-direct `move()`) is retried via `TransferOptions.retry`
|
|
131
|
-
(`{ maxRetries, backoffMs? }`, defaulting to `{ maxRetries: 3 }`) whenever a
|
|
132
|
-
provider throws `TransientIOError` from `pluggable-io-framework-api`. Any
|
|
133
|
-
other error - including a plain, unwrapped `Error` - is treated as
|
|
134
|
-
non-retryable. Direct provider calls (`directCopy`/`directMove`) are never
|
|
135
|
-
retried by the framework, since backends like the AWS SDK already retry
|
|
136
|
-
internally. The underlying `withRetry` utility is exported standalone for
|
|
137
|
-
other call sites.
|
|
138
|
-
|
|
139
|
-
## Development
|
|
140
|
-
|
|
141
|
-
Install dependencies:
|
|
142
|
-
|
|
143
|
-
`bun install`
|
|
144
|
-
|
|
145
|
-
Build (produces `dist/` for Node.js and TypeScript consumers; Bun uses raw source directly):
|
|
146
|
-
|
|
147
|
-
`bun run build`
|
|
148
|
-
|
|
149
|
-
Test:
|
|
150
|
-
|
|
151
|
-
`bun test`
|
|
152
|
-
|
|
153
|
-
Format:
|
|
154
|
-
|
|
155
|
-
`bunx oxfmt`
|
|
156
|
-
|
|
157
|
-
Lint:
|
|
158
|
-
|
|
159
|
-
`bunx oxlint index.ts src/ tests/`
|
|
160
|
-
|
|
161
|
-
Generate HTML API Documentation:
|
|
162
|
-
|
|
163
|
-
`bunx typedoc index.ts`
|
|
164
|
-
|
|
165
|
-
## Documentation
|
|
166
|
-
|
|
167
|
-
### Overview
|
|
168
|
-
|
|
169
|
-
```mermaid
|
|
170
|
-
sequenceDiagram
|
|
171
|
-
participant Host
|
|
172
|
-
participant ProviderRegistry
|
|
173
|
-
participant PluginManager
|
|
174
|
-
participant Source as IOProvider (source)
|
|
175
|
-
participant Sink as IOProvider (sink)
|
|
176
|
-
|
|
177
|
-
Host->>ProviderRegistry: discover()
|
|
178
|
-
ProviderRegistry->>PluginManager: registerExtensions(extensionPoint)
|
|
179
|
-
Host->>ProviderRegistry: createProvider(handle, config)
|
|
180
|
-
ProviderRegistry->>PluginManager: instantiate(handle)
|
|
181
|
-
ProviderRegistry-->>Host: IOProvider
|
|
182
|
-
|
|
183
|
-
Host->>Source: copy(source, path, sink, path)
|
|
184
|
-
alt path is a folder
|
|
185
|
-
alt supportsRecursiveDirectTransfer
|
|
186
|
-
Source->>Sink: directCopy(folderPath, folderPath)
|
|
187
|
-
else
|
|
188
|
-
Source-->>Sink: list + transfer entries (bounded by ConcurrencyLimiter)
|
|
189
|
-
end
|
|
190
|
-
else canDirectTransfer
|
|
191
|
-
Source->>Sink: directCopy(path, path)
|
|
192
|
-
else multipart eligible (negotiated part size)
|
|
193
|
-
Source-->>Sink: transfer Parts concurrently (bounded, retried on TransientIOError)
|
|
194
|
-
else
|
|
195
|
-
Source-->>Sink: stream ChunkRefs (retried on TransientIOError)
|
|
196
|
-
end
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
### API
|
|
200
|
-
|
|
201
|
-
Link to auto-generated API docs:
|
|
66
|
+
## Further Details
|
|
202
67
|
|
|
203
|
-
[
|
|
68
|
+
- [Key Concepts](./README/key-concepts.md)
|
|
69
|
+
- [Implementation Details](./README/implementation-details.md)
|
|
70
|
+
- [Plugins](./README/plugins.md)
|
|
71
|
+
- [Development](./README/development.md)
|
|
72
|
+
- [API Documentation](https://flowscripter.github.io/pluggable-io-framework/index.html)
|
|
204
73
|
|
|
205
74
|
## License
|
|
206
75
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
export * from "./src/ConcurrencyLimiter.ts";
|
|
2
|
-
export * from "./src/
|
|
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/
|
|
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";
|
|
7
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,6BAA6B,CAAC;AAC5C,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,yCAAyC,CAAC;AACxD,cAAc,mDAAmD,CAAC;AAClE,cAAc,mCAAmC,CAAC;AAClD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,oCAAoC,CAAC;AACnD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gDAAgD,CAAC;AAC/D,cAAc,mCAAmC,CAAC;AAClD,cAAc,kCAAkC,CAAC;AACjD,cAAc,2BAA2B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
export * from "./src/ConcurrencyLimiter.js";
|
|
2
|
-
export * from "./src/
|
|
3
|
-
export * from "./src/copyMove.js";
|
|
1
|
+
export * from "./src/concurrency/ConcurrencyLimiter.js";
|
|
2
|
+
export * from "./src/concurrency/mapAsyncIterableConcurrently.js";
|
|
4
3
|
export * from "./src/decorators/locallyCached.js";
|
|
5
4
|
export * from "./src/decorators/seekable.js";
|
|
6
|
-
export * from "./src/
|
|
5
|
+
export * from "./src/registry/detectProtocol.js";
|
|
6
|
+
export * from "./src/registry/ProviderRegistry.js";
|
|
7
|
+
export * from "./src/retry/RetryOptions.js";
|
|
8
|
+
export * from "./src/retry/withRetry.js";
|
|
9
|
+
export * from "./src/transfer/copyMove.js";
|
|
10
|
+
export * from "./src/transfer/rangeReadableMultipartReader.js";
|
|
11
|
+
export * from "./src/transfer/TransferOptions.js";
|
|
12
|
+
export * from "./src/transfer/TransferResult.js";
|
|
13
|
+
export * from "./src/util/globToRegex.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Minimal single-consumer async FIFO queue. */
|
|
2
|
+
export declare class AsyncChannel<T> implements AsyncIterable<T> {
|
|
3
|
+
#private;
|
|
4
|
+
push(item: T): void;
|
|
5
|
+
close(): void;
|
|
6
|
+
fail(error: unknown): void;
|
|
7
|
+
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=AsyncChannel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AsyncChannel.d.ts","sourceRoot":"","sources":["../../../src/concurrency/AsyncChannel.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,qBAAa,YAAY,CAAC,CAAC,CAAE,YAAW,aAAa,CAAC,CAAC,CAAC;;IAQtD,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAOlB;IAED,KAAK,IAAI,IAAI,CASZ;IAED,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAIzB;IAeD,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,CAEzC;CACF"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Minimal single-consumer async FIFO queue. */
|
|
2
|
+
export class AsyncChannel {
|
|
3
|
+
#items = [];
|
|
4
|
+
#waiters = [];
|
|
5
|
+
#closed = false;
|
|
6
|
+
#hasError = false;
|
|
7
|
+
#error;
|
|
8
|
+
push(item) {
|
|
9
|
+
const waiter = this.#waiters.shift();
|
|
10
|
+
if (waiter) {
|
|
11
|
+
waiter.resolve({ value: item, done: false });
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
this.#items.push(item);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
close() {
|
|
18
|
+
this.#closed = true;
|
|
19
|
+
for (const waiter of this.#waiters.splice(0)) {
|
|
20
|
+
if (this.#hasError) {
|
|
21
|
+
waiter.reject(this.#error);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
waiter.resolve({ value: undefined, done: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
fail(error) {
|
|
29
|
+
this.#hasError = true;
|
|
30
|
+
this.#error = error;
|
|
31
|
+
this.close();
|
|
32
|
+
}
|
|
33
|
+
async #next() {
|
|
34
|
+
if (this.#items.length > 0) {
|
|
35
|
+
return { value: this.#items.shift(), done: false };
|
|
36
|
+
}
|
|
37
|
+
if (this.#closed) {
|
|
38
|
+
if (this.#hasError) {
|
|
39
|
+
throw this.#error;
|
|
40
|
+
}
|
|
41
|
+
return { value: undefined, done: true };
|
|
42
|
+
}
|
|
43
|
+
return new Promise((resolve, reject) => this.#waiters.push({ resolve, reject }));
|
|
44
|
+
}
|
|
45
|
+
[Symbol.asyncIterator]() {
|
|
46
|
+
return { next: () => this.#next() };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -16,18 +16,7 @@ export declare class ConcurrencyLimiter {
|
|
|
16
16
|
* Process-wide default limiter, used by `copy()`/`move()` whenever
|
|
17
17
|
* `TransferOptions.concurrencyLimiter` is omitted - so any two calls in the
|
|
18
18
|
* same process share one cap unless a caller explicitly opts out with its
|
|
19
|
-
* own instance
|
|
19
|
+
* own instance.
|
|
20
20
|
*/
|
|
21
21
|
export declare const defaultConcurrencyLimiter: ConcurrencyLimiter;
|
|
22
|
-
/**
|
|
23
|
-
* Pulls items from `source` and runs `fn` on each, bounded by `limiter`:
|
|
24
|
-
* exactly `limiter.maxConcurrency` worker loops each pull-then-process one
|
|
25
|
-
* item at a time through `limiter.run`, so at most that many items are ever
|
|
26
|
-
* pulled/in-flight at once (important for sources like a multipart reader
|
|
27
|
-
* where pulling an item eagerly opens a file/network handle). Results are
|
|
28
|
-
* yielded in completion order, not source order. If `fn` throws, no new
|
|
29
|
-
* items are pulled but already-in-flight ones are allowed to finish before
|
|
30
|
-
* the error is rethrown from the returned iterable.
|
|
31
|
-
*/
|
|
32
|
-
export declare function mapAsyncIterableConcurrently<T, R>(source: AsyncIterable<T>, fn: (item: T) => Promise<R>, limiter: ConcurrencyLimiter): AsyncIterable<R>;
|
|
33
22
|
//# sourceMappingURL=ConcurrencyLimiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConcurrencyLimiter.d.ts","sourceRoot":"","sources":["../../../src/concurrency/ConcurrencyLimiter.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,qBAAa,kBAAkB;;IAK7B,YAAmB,cAAc,EAAE,MAAM,EAGxC;IAED,IAAW,cAAc,IAAI,MAAM,CAElC;IAEM,iBAAiB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAIrD;IAEY,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAQpD;CA0BF;AAED;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,oBAAkD,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const DEFAULT_MAX_CONCURRENCY = 4;
|
|
2
|
+
/**
|
|
3
|
+
* A semaphore bounding how many `run()` callbacks execute at once. A single
|
|
4
|
+
* shared instance (see {@link defaultConcurrencyLimiter}) enforces its cap
|
|
5
|
+
* *globally* across concurrently invoked `copy()`/`move()` calls, not just
|
|
6
|
+
* within one call - two folder copies started at the same time and sharing
|
|
7
|
+
* a limiter will never together exceed its `maxConcurrency`.
|
|
8
|
+
*/
|
|
9
|
+
export class ConcurrencyLimiter {
|
|
10
|
+
#maxConcurrency;
|
|
11
|
+
#active = 0;
|
|
12
|
+
#queue = [];
|
|
13
|
+
constructor(maxConcurrency) {
|
|
14
|
+
ConcurrencyLimiter.#validate(maxConcurrency);
|
|
15
|
+
this.#maxConcurrency = maxConcurrency;
|
|
16
|
+
}
|
|
17
|
+
get maxConcurrency() {
|
|
18
|
+
return this.#maxConcurrency;
|
|
19
|
+
}
|
|
20
|
+
setMaxConcurrency(maxConcurrency) {
|
|
21
|
+
ConcurrencyLimiter.#validate(maxConcurrency);
|
|
22
|
+
this.#maxConcurrency = maxConcurrency;
|
|
23
|
+
this.#drain();
|
|
24
|
+
}
|
|
25
|
+
async run(fn) {
|
|
26
|
+
await this.#acquire();
|
|
27
|
+
try {
|
|
28
|
+
return await fn();
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
this.#active -= 1;
|
|
32
|
+
this.#drain();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
#acquire() {
|
|
36
|
+
if (this.#active < this.#maxConcurrency) {
|
|
37
|
+
this.#active += 1;
|
|
38
|
+
return Promise.resolve();
|
|
39
|
+
}
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
this.#queue.push(() => {
|
|
42
|
+
this.#active += 1;
|
|
43
|
+
resolve();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
#drain() {
|
|
48
|
+
while (this.#active < this.#maxConcurrency && this.#queue.length > 0) {
|
|
49
|
+
this.#queue.shift()?.();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
static #validate(maxConcurrency) {
|
|
53
|
+
if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
|
|
54
|
+
throw new Error(`maxConcurrency must be a positive integer, got ${maxConcurrency}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Process-wide default limiter, used by `copy()`/`move()` whenever
|
|
60
|
+
* `TransferOptions.concurrencyLimiter` is omitted - so any two calls in the
|
|
61
|
+
* same process share one cap unless a caller explicitly opts out with its
|
|
62
|
+
* own instance.
|
|
63
|
+
*/
|
|
64
|
+
export const defaultConcurrencyLimiter = new ConcurrencyLimiter(DEFAULT_MAX_CONCURRENCY);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ConcurrencyLimiter } from "./ConcurrencyLimiter.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Pulls items from `source` and runs `fn` on each, bounded by `limiter`:
|
|
4
|
+
* exactly `limiter.maxConcurrency` worker loops each pull-then-process one
|
|
5
|
+
* item at a time through `limiter.run`, so at most that many items are ever
|
|
6
|
+
* pulled/in-flight at once (important for sources like a multipart reader
|
|
7
|
+
* where pulling an item eagerly opens a file/network handle). Results are
|
|
8
|
+
* yielded in completion order, not source order. If `fn` throws, no new
|
|
9
|
+
* items are pulled but already-in-flight ones are allowed to finish before
|
|
10
|
+
* the error is rethrown from the returned iterable.
|
|
11
|
+
*/
|
|
12
|
+
export declare function mapAsyncIterableConcurrently<T, R>(source: AsyncIterable<T>, fn: (item: T) => Promise<R>, limiter: ConcurrencyLimiter): AsyncIterable<R>;
|
|
13
|
+
//# sourceMappingURL=mapAsyncIterableConcurrently.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mapAsyncIterableConcurrently.d.ts","sourceRoot":"","sources":["../../../src/concurrency/mapAsyncIterableConcurrently.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAElE;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,EAAE,CAAC,EAC/C,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,EACxB,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,kBAAkB,GAC1B,aAAa,CAAC,CAAC,CAAC,CAgDlB"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AsyncChannel } from "./AsyncChannel.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pulls items from `source` and runs `fn` on each, bounded by `limiter`:
|
|
4
|
+
* exactly `limiter.maxConcurrency` worker loops each pull-then-process one
|
|
5
|
+
* item at a time through `limiter.run`, so at most that many items are ever
|
|
6
|
+
* pulled/in-flight at once (important for sources like a multipart reader
|
|
7
|
+
* where pulling an item eagerly opens a file/network handle). Results are
|
|
8
|
+
* yielded in completion order, not source order. If `fn` throws, no new
|
|
9
|
+
* items are pulled but already-in-flight ones are allowed to finish before
|
|
10
|
+
* the error is rethrown from the returned iterable.
|
|
11
|
+
*/
|
|
12
|
+
export function mapAsyncIterableConcurrently(source, fn, limiter) {
|
|
13
|
+
const channel = new AsyncChannel();
|
|
14
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
15
|
+
let stopped = false;
|
|
16
|
+
let firstError;
|
|
17
|
+
let errorRecorded = false;
|
|
18
|
+
function recordError(error) {
|
|
19
|
+
stopped = true;
|
|
20
|
+
if (!errorRecorded) {
|
|
21
|
+
errorRecorded = true;
|
|
22
|
+
firstError = error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function worker() {
|
|
26
|
+
for (;;) {
|
|
27
|
+
if (stopped)
|
|
28
|
+
return;
|
|
29
|
+
let next;
|
|
30
|
+
try {
|
|
31
|
+
next = await iterator.next();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
recordError(error);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (next.done)
|
|
38
|
+
return;
|
|
39
|
+
const value = next.value;
|
|
40
|
+
try {
|
|
41
|
+
const result = await limiter.run(() => fn(value));
|
|
42
|
+
channel.push(result);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
recordError(error);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
void (async () => {
|
|
51
|
+
const workerCount = Math.max(1, limiter.maxConcurrency);
|
|
52
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
53
|
+
if (errorRecorded) {
|
|
54
|
+
channel.fail(firstError);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
channel.close();
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
60
|
+
return channel;
|
|
61
|
+
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PayloadKind, StreamHandle } from "@flowscripter/pluggable-io-framework-api";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* source while caching every
|
|
6
|
-
* the cached
|
|
3
|
+
* A `StreamOpenerDecorator`: wraps a `StreamHandle` opener so the
|
|
4
|
+
* underlying source is read at most once - the first call drains the
|
|
5
|
+
* source while caching every item in memory; every subsequent call replays
|
|
6
|
+
* the cached items without touching the source again.
|
|
7
7
|
*
|
|
8
8
|
* A plain `StreamHandle` only exposes a single one-shot `ReadableStream`, so
|
|
9
|
-
* caching can't be a `StreamDecorator
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* what kind of source is behind it.
|
|
9
|
+
* caching can't be a `StreamDecorator` operating on an already-open handle
|
|
10
|
+
* (there would be nothing left to re-read on a second call) - it has to
|
|
11
|
+
* intercept the *open* operation itself.
|
|
13
12
|
*/
|
|
14
|
-
export declare function locallyCached<K extends
|
|
13
|
+
export declare function locallyCached<K extends PayloadKind>(open: () => Promise<StreamHandle<K>>): () => Promise<StreamHandle<K>>;
|
|
15
14
|
//# sourceMappingURL=locallyCached.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locallyCached.d.ts","sourceRoot":"","sources":["../../../src/decorators/locallyCached.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"locallyCached.d.ts","sourceRoot":"","sources":["../../../src/decorators/locallyCached.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,WAAW,EACX,YAAY,EACb,MAAM,0CAA0C,CAAC;AAElD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,WAAW,EACjD,IAAI,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GACnC,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAgDhC"}
|
|
@@ -1,48 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* source while caching every
|
|
5
|
-
* the cached
|
|
2
|
+
* A `StreamOpenerDecorator`: wraps a `StreamHandle` opener so the
|
|
3
|
+
* underlying source is read at most once - the first call drains the
|
|
4
|
+
* source while caching every item in memory; every subsequent call replays
|
|
5
|
+
* the cached items without touching the source again.
|
|
6
6
|
*
|
|
7
7
|
* A plain `StreamHandle` only exposes a single one-shot `ReadableStream`, so
|
|
8
|
-
* caching can't be a `StreamDecorator
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* what kind of source is behind it.
|
|
8
|
+
* caching can't be a `StreamDecorator` operating on an already-open handle
|
|
9
|
+
* (there would be nothing left to re-read on a second call) - it has to
|
|
10
|
+
* intercept the *open* operation itself.
|
|
12
11
|
*/
|
|
13
12
|
export function locallyCached(open) {
|
|
14
13
|
let cache;
|
|
15
|
-
function replay(
|
|
14
|
+
function replay(items) {
|
|
16
15
|
return new ReadableStream({
|
|
17
16
|
start(controller) {
|
|
18
|
-
for (const
|
|
19
|
-
controller.enqueue(
|
|
17
|
+
for (const item of items)
|
|
18
|
+
controller.enqueue(item);
|
|
20
19
|
controller.close();
|
|
21
20
|
},
|
|
22
21
|
});
|
|
23
22
|
}
|
|
23
|
+
function describe(handle, stream) {
|
|
24
|
+
return {
|
|
25
|
+
kind: handle.kind,
|
|
26
|
+
stream,
|
|
27
|
+
bounded: handle.bounded,
|
|
28
|
+
payloadType: handle.payloadType,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
24
31
|
return async () => {
|
|
25
32
|
if (cache) {
|
|
26
|
-
return
|
|
33
|
+
return describe(cache.handle, replay(cache.items));
|
|
27
34
|
}
|
|
28
35
|
const handle = await open();
|
|
29
36
|
const reader = handle.stream.getReader();
|
|
30
|
-
const
|
|
37
|
+
const items = [];
|
|
31
38
|
const stream = new ReadableStream({
|
|
32
39
|
async pull(controller) {
|
|
33
40
|
const { done, value } = await reader.read();
|
|
34
41
|
if (done) {
|
|
35
|
-
cache = {
|
|
42
|
+
cache = { handle, items };
|
|
36
43
|
controller.close();
|
|
37
44
|
return;
|
|
38
45
|
}
|
|
39
|
-
|
|
46
|
+
items.push(value);
|
|
40
47
|
controller.enqueue(value);
|
|
41
48
|
},
|
|
42
49
|
cancel(reason) {
|
|
43
50
|
return reader.cancel(reason);
|
|
44
51
|
},
|
|
45
52
|
});
|
|
46
|
-
return
|
|
53
|
+
return describe(handle, stream);
|
|
47
54
|
};
|
|
48
55
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PayloadKind, RangeReadable, Seekable, StreamHandle } from "@flowscripter/pluggable-io-framework-api";
|
|
2
2
|
/**
|
|
3
|
-
* Wraps a handle that already supports
|
|
4
|
-
* byte-range reads) with a
|
|
3
|
+
* Wraps a handle that already supports `RangeReadable` (arbitrary
|
|
4
|
+
* byte-range reads) with a `Seekable` capability: a single logical
|
|
5
5
|
* stream whose read position can be jumped via `seek(offset)`, rather than
|
|
6
6
|
* requiring the caller to open a fresh stream per range.
|
|
7
7
|
*
|
|
@@ -9,5 +9,5 @@ import type { ChunkKind, RangeReadable, Seekable, StreamHandle } from "@flowscri
|
|
|
9
9
|
* in flight - like any seekable stream, seeking and reading are sequential,
|
|
10
10
|
* not concurrent, operations on the same handle.
|
|
11
11
|
*/
|
|
12
|
-
export declare function seekable<K extends
|
|
12
|
+
export declare function seekable<K extends PayloadKind>(handle: StreamHandle<K> & RangeReadable<K>): StreamHandle<K> & Seekable;
|
|
13
13
|
//# sourceMappingURL=seekable.d.ts.map
|