@flowscripter/pluggable-io-framework 0.1.0 → 1.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 +9 -11
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/src/ProviderRegistry.d.ts.map +1 -1
- package/dist/src/chunkLength.d.ts +1 -1
- package/dist/src/chunkLength.d.ts.map +1 -1
- package/dist/src/chunkLength.js +2 -1
- package/dist/src/copyMove.d.ts +29 -0
- package/dist/src/copyMove.d.ts.map +1 -0
- package/dist/src/copyMove.js +102 -0
- package/package.json +4 -4
- package/src/chunkLength.ts +2 -2
- package/src/copyMove.ts +38 -13
package/README.md
CHANGED
|
@@ -5,9 +5,7 @@
|
|
|
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
|
|
9
|
-
> orchestration and telemetry, for files and folders across different
|
|
10
|
-
> backends (local filesystem, object storage, HTTP, etc.)
|
|
8
|
+
> A pluggable source/sink IO framework using https://github.com/flowscripter/dynamic-plugin-framework
|
|
11
9
|
|
|
12
10
|
## Key Features
|
|
13
11
|
|
|
@@ -26,15 +24,8 @@
|
|
|
26
24
|
size crosses a configurable threshold) or plain streaming.
|
|
27
25
|
- Reports progress via a global `TelemetryHooks` callback, tagged with a
|
|
28
26
|
per-operation correlation id.
|
|
29
|
-
- Standalone today - usable directly or via the
|
|
30
|
-
[flowscripter-io-cli](https://github.com/flowscripter/flowscripter-io-cli)
|
|
31
|
-
built on
|
|
32
|
-
[dynamic-cli-framework](https://github.com/flowscripter/dynamic-cli-framework).
|
|
33
|
-
When the full Flowscripter graph runtime exists, an `adapt` operator will
|
|
34
|
-
wrap these providers to bridge them into the processing graph - that
|
|
35
|
-
wrapping is out of scope for this repo.
|
|
36
27
|
- See
|
|
37
|
-
[
|
|
28
|
+
[io-plugin-filesystem](https://github.com/flowscripter/io-plugin-filesystem)
|
|
38
29
|
for a reference local filesystem source/sink plugin.
|
|
39
30
|
|
|
40
31
|
## Bun Module Usage
|
|
@@ -64,6 +55,13 @@ await copy(provider, "a.txt", provider, "b.txt", {
|
|
|
64
55
|
});
|
|
65
56
|
```
|
|
66
57
|
|
|
58
|
+
## Usage Example
|
|
59
|
+
|
|
60
|
+
The following example project is available:
|
|
61
|
+
|
|
62
|
+
- [flowscripter-io-cli](https://github.com/flowscripter/flowscripter-io-cli) is
|
|
63
|
+
an example CLI application based on this framework.
|
|
64
|
+
|
|
67
65
|
## Development
|
|
68
66
|
|
|
69
67
|
Install dependencies:
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProviderRegistry.d.ts","sourceRoot":"","sources":["../../src/ProviderRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAC;AAC5E,OAAO,EAEL,KAAK,UAAU,EAEhB,MAAM,0CAA0C,CAAC;AAElD;;;GAGG;AACH,qBAAa,gBAAgB;IACR,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAAb,aAAa,EAAE,aAAa;IAEnD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"ProviderRegistry.d.ts","sourceRoot":"","sources":["../../src/ProviderRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAC;AAC5E,OAAO,EAEL,KAAK,UAAU,EAEhB,MAAM,0CAA0C,CAAC;AAElD;;;GAGG;AACH,qBAAa,gBAAgB;IACR,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAAb,aAAa,EAAE,aAAa;IAEnD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAMzB,sBAAsB;IAMtB,cAAc,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC;CAK3F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunkLength.d.ts","sourceRoot":"","sources":["../../src/chunkLength.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"chunkLength.d.ts","sourceRoot":"","sources":["../../src/chunkLength.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,0CAA0C,CAAC;AAEpF,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAEnD"}
|
package/dist/src/chunkLength.js
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ChunkConverter, type IOProvider, type TelemetryHooks } from "@flowscripter/pluggable-io-framework-api";
|
|
2
|
+
export interface TransferOptions {
|
|
3
|
+
readonly telemetry?: TelemetryHooks;
|
|
4
|
+
/** Minimum file size (bytes) before multipart transfer is attempted over plain streaming. */
|
|
5
|
+
readonly multipartThreshold?: number;
|
|
6
|
+
/**
|
|
7
|
+
* Required only when `source.kind !== sink.kind` - pure-TS code can only
|
|
8
|
+
* convert chunks already of the target kind (see
|
|
9
|
+
* `identityChunkConverter`); a real js<->native conversion needs an
|
|
10
|
+
* FFI-capable converter supplied by a runtime-specific package.
|
|
11
|
+
*/
|
|
12
|
+
readonly chunkConverter?: ChunkConverter;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Copies `sourcePath` on `source` to `destPath` on `sink`.
|
|
16
|
+
*
|
|
17
|
+
* Uses `source.directCopy` when `source.canDirectTransfer?.(sink)` reports
|
|
18
|
+
* eligibility (same-provider direct transfer). Otherwise falls back to
|
|
19
|
+
* multipart transfer (when both sides support it and size crosses
|
|
20
|
+
* `options.multipartThreshold`) or plain streaming.
|
|
21
|
+
*/
|
|
22
|
+
export declare function copy(source: IOProvider, sourcePath: string, sink: IOProvider, destPath: string, options?: TransferOptions): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Moves `sourcePath` on `source` to `destPath` on `sink`. Uses
|
|
25
|
+
* `source.directMove` when eligible, otherwise performs a {@link copy}
|
|
26
|
+
* followed by deleting the source item.
|
|
27
|
+
*/
|
|
28
|
+
export declare function move(source: IOProvider, sourcePath: string, sink: IOProvider, destPath: string, options?: TransferOptions): Promise<void>;
|
|
29
|
+
//# sourceMappingURL=copyMove.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"copyMove.d.ts","sourceRoot":"","sources":["../../src/copyMove.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,cAAc,EAEnB,KAAK,UAAU,EAEf,KAAK,cAAc,EACpB,MAAM,0CAA0C,CAAC;AAGlD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,cAAc,CAAC;IACpC,6FAA6F;IAC7F,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;CAC1C;AAoGD;;;;;;;GAOG;AACH,wBAAsB,IAAI,CACxB,MAAM,EAAE,UAAU,EAClB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED;;;;GAIG;AACH,wBAAsB,IAAI,CACxB,MAAM,EAAE,UAAU,EAClB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAOf"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { adaptReadableStream, identityChunkConverter, } from "@flowscripter/pluggable-io-framework-api";
|
|
2
|
+
import { chunkLength } from "./chunkLength.js";
|
|
3
|
+
const DEFAULT_MULTIPART_THRESHOLD = 64 * 1024 * 1024;
|
|
4
|
+
async function streamingTransfer(source, sourcePath, sink, destPath, operationId, options, totalBytes) {
|
|
5
|
+
const readable = await source.getReadableStream(sourcePath);
|
|
6
|
+
const writable = await sink.getWritableStream(destPath);
|
|
7
|
+
// The source/sink kind mismatch is decided ONCE here, not per chunk.
|
|
8
|
+
const adaptedStream = adaptReadableStream(readable.stream, readable.kind, writable.kind, options.chunkConverter ?? identityChunkConverter);
|
|
9
|
+
const reader = adaptedStream.getReader();
|
|
10
|
+
const writer = writable.stream.getWriter();
|
|
11
|
+
let bytesProcessed = 0;
|
|
12
|
+
try {
|
|
13
|
+
for (;;) {
|
|
14
|
+
const { done, value } = await reader.read();
|
|
15
|
+
if (done)
|
|
16
|
+
break;
|
|
17
|
+
await writer.write(value);
|
|
18
|
+
bytesProcessed += chunkLength(value);
|
|
19
|
+
options.telemetry?.onProgress?.({ operationId, type: "copy", bytesProcessed, totalBytes });
|
|
20
|
+
}
|
|
21
|
+
await writer.close();
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
await writer.abort(error);
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function multipartTransfer(source, sourcePath, sink, destPath, operationId, options, totalBytes) {
|
|
29
|
+
let bytesProcessed = 0;
|
|
30
|
+
const writer = sink.getMultipartWriter(destPath);
|
|
31
|
+
async function* transferParts() {
|
|
32
|
+
for await (const part of source.getMultipartReader(sourcePath)) {
|
|
33
|
+
// Kind mismatch decided once per part, not once per chunk within it.
|
|
34
|
+
const adaptedStream = adaptReadableStream(part.stream, part.kind, sink.kind, options.chunkConverter ?? identityChunkConverter);
|
|
35
|
+
const reader = adaptedStream.getReader();
|
|
36
|
+
const chunks = [];
|
|
37
|
+
for (;;) {
|
|
38
|
+
const { done, value } = await reader.read();
|
|
39
|
+
if (done)
|
|
40
|
+
break;
|
|
41
|
+
chunks.push(value);
|
|
42
|
+
bytesProcessed += chunkLength(value);
|
|
43
|
+
options.telemetry?.onProgress?.({ operationId, type: "copy", bytesProcessed, totalBytes });
|
|
44
|
+
}
|
|
45
|
+
yield {
|
|
46
|
+
index: part.index,
|
|
47
|
+
offset: part.offset,
|
|
48
|
+
kind: sink.kind,
|
|
49
|
+
stream: new ReadableStream({
|
|
50
|
+
start(controller) {
|
|
51
|
+
for (const chunk of chunks)
|
|
52
|
+
controller.enqueue(chunk);
|
|
53
|
+
controller.close();
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
complete: () => part.complete(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
await writer.write(transferParts());
|
|
61
|
+
}
|
|
62
|
+
function canUseMultipart(source, sink, size, threshold) {
|
|
63
|
+
return (size !== undefined &&
|
|
64
|
+
size >= threshold &&
|
|
65
|
+
typeof source.getMultipartReader === "function" &&
|
|
66
|
+
typeof sink.getMultipartWriter === "function");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Copies `sourcePath` on `source` to `destPath` on `sink`.
|
|
70
|
+
*
|
|
71
|
+
* Uses `source.directCopy` when `source.canDirectTransfer?.(sink)` reports
|
|
72
|
+
* eligibility (same-provider direct transfer). Otherwise falls back to
|
|
73
|
+
* multipart transfer (when both sides support it and size crosses
|
|
74
|
+
* `options.multipartThreshold`) or plain streaming.
|
|
75
|
+
*/
|
|
76
|
+
export async function copy(source, sourcePath, sink, destPath, options = {}) {
|
|
77
|
+
const operationId = crypto.randomUUID();
|
|
78
|
+
if (source.canDirectTransfer?.(sink) && source.directCopy) {
|
|
79
|
+
await source.directCopy(sourcePath, destPath);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const properties = await source.getProperties(sourcePath);
|
|
83
|
+
const threshold = options.multipartThreshold ?? DEFAULT_MULTIPART_THRESHOLD;
|
|
84
|
+
if (canUseMultipart(source, sink, properties.size, threshold)) {
|
|
85
|
+
await multipartTransfer(source, sourcePath, sink, destPath, operationId, options, properties.size);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await streamingTransfer(source, sourcePath, sink, destPath, operationId, options, properties.size);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Moves `sourcePath` on `source` to `destPath` on `sink`. Uses
|
|
92
|
+
* `source.directMove` when eligible, otherwise performs a {@link copy}
|
|
93
|
+
* followed by deleting the source item.
|
|
94
|
+
*/
|
|
95
|
+
export async function move(source, sourcePath, sink, destPath, options = {}) {
|
|
96
|
+
if (source.canDirectTransfer?.(sink) && source.directMove) {
|
|
97
|
+
await source.directMove(sourcePath, destPath);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await copy(source, sourcePath, sink, destPath, options);
|
|
101
|
+
await source.delete(sourcePath);
|
|
102
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowscripter/pluggable-io-framework",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A pluggable source/sink IO framework using https://github.com/flowscripter/dynamic-plugin-framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
7
7
|
"io",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"test": "bun test"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@flowscripter/dynamic-plugin-framework": "
|
|
44
|
-
"@flowscripter/pluggable-io-framework-api": "
|
|
43
|
+
"@flowscripter/dynamic-plugin-framework": "^2.2.0",
|
|
44
|
+
"@flowscripter/pluggable-io-framework-api": "^1.0.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/bun": "^1.3.14",
|
package/src/chunkLength.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { ChunkKind, type ChunkRef } from "@flowscripter/pluggable-io-framework-api";
|
|
2
2
|
|
|
3
3
|
export function chunkLength(chunk: ChunkRef): number {
|
|
4
|
-
return chunk.kind ===
|
|
4
|
+
return chunk.kind === ChunkKind.Js ? chunk.data.byteLength : chunk.length;
|
|
5
5
|
}
|
package/src/copyMove.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {
|
|
2
|
+
adaptReadableStream,
|
|
3
|
+
identityChunkConverter,
|
|
4
|
+
type ChunkConverter,
|
|
5
|
+
type ChunkRef,
|
|
6
|
+
type IOProvider,
|
|
7
|
+
type Part,
|
|
8
|
+
type TelemetryHooks,
|
|
6
9
|
} from "@flowscripter/pluggable-io-framework-api";
|
|
7
10
|
import { chunkLength } from "./chunkLength.ts";
|
|
8
11
|
|
|
@@ -10,6 +13,13 @@ export interface TransferOptions {
|
|
|
10
13
|
readonly telemetry?: TelemetryHooks;
|
|
11
14
|
/** Minimum file size (bytes) before multipart transfer is attempted over plain streaming. */
|
|
12
15
|
readonly multipartThreshold?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Required only when `source.kind !== sink.kind` - pure-TS code can only
|
|
18
|
+
* convert chunks already of the target kind (see
|
|
19
|
+
* `identityChunkConverter`); a real js<->native conversion needs an
|
|
20
|
+
* FFI-capable converter supplied by a runtime-specific package.
|
|
21
|
+
*/
|
|
22
|
+
readonly chunkConverter?: ChunkConverter;
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
const DEFAULT_MULTIPART_THRESHOLD = 64 * 1024 * 1024;
|
|
@@ -20,12 +30,19 @@ async function streamingTransfer(
|
|
|
20
30
|
sink: IOProvider,
|
|
21
31
|
destPath: string,
|
|
22
32
|
operationId: string,
|
|
23
|
-
|
|
33
|
+
options: TransferOptions,
|
|
24
34
|
totalBytes: number | undefined,
|
|
25
35
|
): Promise<void> {
|
|
26
36
|
const readable = await source.getReadableStream(sourcePath);
|
|
27
37
|
const writable = await sink.getWritableStream(destPath);
|
|
28
|
-
|
|
38
|
+
// The source/sink kind mismatch is decided ONCE here, not per chunk.
|
|
39
|
+
const adaptedStream = adaptReadableStream(
|
|
40
|
+
readable.stream as ReadableStream<ChunkRef>,
|
|
41
|
+
readable.kind,
|
|
42
|
+
writable.kind,
|
|
43
|
+
options.chunkConverter ?? identityChunkConverter,
|
|
44
|
+
);
|
|
45
|
+
const reader = adaptedStream.getReader();
|
|
29
46
|
const writer = (writable.stream as WritableStream<ChunkRef>).getWriter();
|
|
30
47
|
let bytesProcessed = 0;
|
|
31
48
|
try {
|
|
@@ -34,7 +51,7 @@ async function streamingTransfer(
|
|
|
34
51
|
if (done) break;
|
|
35
52
|
await writer.write(value);
|
|
36
53
|
bytesProcessed += chunkLength(value);
|
|
37
|
-
telemetry?.onProgress?.({ operationId, type: "copy", bytesProcessed, totalBytes });
|
|
54
|
+
options.telemetry?.onProgress?.({ operationId, type: "copy", bytesProcessed, totalBytes });
|
|
38
55
|
}
|
|
39
56
|
await writer.close();
|
|
40
57
|
} catch (error) {
|
|
@@ -49,25 +66,33 @@ async function multipartTransfer(
|
|
|
49
66
|
sink: IOProvider,
|
|
50
67
|
destPath: string,
|
|
51
68
|
operationId: string,
|
|
52
|
-
|
|
69
|
+
options: TransferOptions,
|
|
53
70
|
totalBytes: number | undefined,
|
|
54
71
|
): Promise<void> {
|
|
55
72
|
let bytesProcessed = 0;
|
|
56
73
|
const writer = sink.getMultipartWriter(destPath);
|
|
57
74
|
async function* transferParts(): AsyncIterable<Part> {
|
|
58
75
|
for await (const part of source.getMultipartReader(sourcePath)) {
|
|
59
|
-
|
|
76
|
+
// Kind mismatch decided once per part, not once per chunk within it.
|
|
77
|
+
const adaptedStream = adaptReadableStream(
|
|
78
|
+
part.stream as ReadableStream<ChunkRef>,
|
|
79
|
+
part.kind,
|
|
80
|
+
sink.kind,
|
|
81
|
+
options.chunkConverter ?? identityChunkConverter,
|
|
82
|
+
);
|
|
83
|
+
const reader = adaptedStream.getReader();
|
|
60
84
|
const chunks: ChunkRef[] = [];
|
|
61
85
|
for (;;) {
|
|
62
86
|
const { done, value } = await reader.read();
|
|
63
87
|
if (done) break;
|
|
64
88
|
chunks.push(value);
|
|
65
89
|
bytesProcessed += chunkLength(value);
|
|
66
|
-
telemetry?.onProgress?.({ operationId, type: "copy", bytesProcessed, totalBytes });
|
|
90
|
+
options.telemetry?.onProgress?.({ operationId, type: "copy", bytesProcessed, totalBytes });
|
|
67
91
|
}
|
|
68
92
|
yield {
|
|
69
93
|
index: part.index,
|
|
70
94
|
offset: part.offset,
|
|
95
|
+
kind: sink.kind,
|
|
71
96
|
stream: new ReadableStream<ChunkRef>({
|
|
72
97
|
start(controller) {
|
|
73
98
|
for (const chunk of chunks) controller.enqueue(chunk);
|
|
@@ -124,7 +149,7 @@ export async function copy(
|
|
|
124
149
|
sink,
|
|
125
150
|
destPath,
|
|
126
151
|
operationId,
|
|
127
|
-
options
|
|
152
|
+
options,
|
|
128
153
|
properties.size,
|
|
129
154
|
);
|
|
130
155
|
return;
|
|
@@ -135,7 +160,7 @@ export async function copy(
|
|
|
135
160
|
sink,
|
|
136
161
|
destPath,
|
|
137
162
|
operationId,
|
|
138
|
-
options
|
|
163
|
+
options,
|
|
139
164
|
properties.size,
|
|
140
165
|
);
|
|
141
166
|
}
|