@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.
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 +6 -5
  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
package/README.md CHANGED
@@ -5,202 +5,71 @@
5
5
  [![docs](https://img.shields.io/badge/docs-API-blue)](https://flowscripter.github.io/pluggable-io-framework/index.html)
6
6
  [![license: MIT](https://img.shields.io/github/license/flowscripter/pluggable-io-framework)](https://github.com/flowscripter/pluggable-io-framework/blob/main/LICENSE)
7
7
 
8
- > A pluggable source/sink IO framework using https://github.com/flowscripter/dynamic-plugin-framework
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
- - Discovers and instantiates source/sink provider plugins (implementing the
13
- `IOProviderFactory`/`IOProvider` contract from
14
- [pluggable-io-framework-api](https://github.com/flowscripter/pluggable-io-framework-api))
15
- via
16
- [dynamic-plugin-framework](https://github.com/flowscripter/dynamic-plugin-framework),
17
- including config validation against each plugin's Zod schema.
18
- - Copy/move orchestration:
19
- - Uses a provider's `directCopy`/`directMove` when
20
- `canDirectTransfer` reports the source and sink are the same
21
- underlying provider (e.g. same filesystem mount, same object storage
22
- bucket).
23
- - Otherwise transfers via multipart (when both sides support it and file
24
- size crosses a configurable threshold) or plain streaming.
25
- - Multipart part size is negotiated between source and sink via
26
- `getPartSizeConstraints` (e.g. reconciling S3's minimum part size and
27
- 10000-part cap on both ends) - see [Part-size negotiation](#part-size-negotiation).
28
- - A folder `sourcePath` recurses: a folder-aware `directCopy`/`directMove`
29
- when the source declares `supportsRecursiveDirectTransfer`, otherwise a
30
- concurrency-bounded listing-driven transfer following `cp -r` target
31
- semantics - see [Recursive copy/move](#recursive-copymove).
32
- - Concurrent multipart parts and recursive entries are bounded by a shared
33
- `ConcurrencyLimiter` - see [Concurrency](#concurrency).
34
- - The non-direct transfer path is retried on `TransientIOError` - see
35
- [Retries](#retries).
36
- - Reports progress via a global `TelemetryHooks` callback, tagged with a
37
- per-operation correlation id; multipart parts and recursive entries
38
- report their own child stream tagged with `parentOperationId`.
39
- - Stream decorators:
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 { ProviderRegistry, copy } from "@flowscripter/pluggable-io-framework";
47
+ import { copy, ProviderRegistry } from "@flowscripter/pluggable-io-framework";
64
48
 
65
- const pluginManager = new DefaultPluginManager([new LocalFolderPluginRepository("./plugins")]);
66
- const registry = new ProviderRegistry(pluginManager);
49
+ const registry = new ProviderRegistry(
50
+ new DefaultPluginManager([new LocalFolderPluginRepository("./plugins")]),
51
+ );
67
52
  await registry.discover();
68
53
 
69
- const [extension] = await registry.listAvailableProviders();
70
- const provider = await registry.createProvider(extension.extensionHandle, { rootPath: "/data" });
54
+ const { source, dest, options } = await registry.createProvidersForTransfer(
55
+ "file:///data/in",
56
+ "s3://bucket/out",
57
+ );
71
58
 
72
- await copy(provider, "a.txt", provider, "b.txt", {
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
- ## Usage Example
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
- [API Documentation](https://flowscripter.github.io/pluggable-io-framework/index.html)
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/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";
7
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,mCAAmC,CAAC;AAClD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oBAAoB,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/ProviderRegistry.js";
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/withRetry.js";
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 (e.g. for test isolation).
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 { ChunkKind, StreamHandle } from "@flowscripter/pluggable-io-framework-api";
1
+ import type { PayloadKind, StreamHandle } from "@flowscripter/pluggable-io-framework-api";
2
2
  /**
3
- * Wraps a `StreamHandle` factory (e.g. `() => provider.getReadableStream(path)`)
4
- * so the underlying source is read at most once - the first call drains the
5
- * source while caching every chunk in memory; every subsequent call replays
6
- * the cached chunks without touching the source again.
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<K, C>` operating on an already-open
10
- * handle (there would be nothing left to re-read on a second call) - it has
11
- * to intercept the *open* operation itself, provider-agnostic regardless of
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 ChunkKind>(open: () => Promise<StreamHandle<K>>): () => Promise<StreamHandle<K>>;
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,EACV,SAAS,EAET,YAAY,EACb,MAAM,0CAA0C,CAAC;AAElD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,SAAS,EAC/C,IAAI,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GACnC,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAuChC"}
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
- * Wraps a `StreamHandle` factory (e.g. `() => provider.getReadableStream(path)`)
3
- * so the underlying source is read at most once - the first call drains the
4
- * source while caching every chunk in memory; every subsequent call replays
5
- * the cached chunks without touching the source again.
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<K, C>` operating on an already-open
9
- * handle (there would be nothing left to re-read on a second call) - it has
10
- * to intercept the *open* operation itself, provider-agnostic regardless of
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(chunks) {
14
+ function replay(items) {
16
15
  return new ReadableStream({
17
16
  start(controller) {
18
- for (const chunk of chunks)
19
- controller.enqueue(chunk);
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 { kind: cache.kind, stream: replay(cache.chunks) };
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 chunks = [];
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 = { kind: handle.kind, chunks };
42
+ cache = { handle, items };
36
43
  controller.close();
37
44
  return;
38
45
  }
39
- chunks.push(value);
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 { kind: handle.kind, stream };
53
+ return describe(handle, stream);
47
54
  };
48
55
  }
@@ -1,7 +1,7 @@
1
- import type { ChunkKind, RangeReadable, Seekable, StreamHandle } from "@flowscripter/pluggable-io-framework-api";
1
+ import type { PayloadKind, RangeReadable, Seekable, StreamHandle } from "@flowscripter/pluggable-io-framework-api";
2
2
  /**
3
- * Wraps a handle that already supports {@link RangeReadable} (arbitrary
4
- * byte-range reads) with a {@link Seekable} capability: a single logical
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 ChunkKind>(handle: StreamHandle<K> & RangeReadable<K>): StreamHandle<K> & Seekable;
12
+ export declare function seekable<K extends PayloadKind>(handle: StreamHandle<K> & RangeReadable<K>): StreamHandle<K> & Seekable;
13
13
  //# sourceMappingURL=seekable.d.ts.map