@flowscripter/pluggable-io-framework 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,9 +5,7 @@
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 - provider discovery, copy/move
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,13 +24,6 @@
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
  [pluggable-io-framework-plugin-filesystem](https://github.com/flowscripter/pluggable-io-framework-plugin-filesystem)
38
29
  for a reference local filesystem source/sink plugin.
@@ -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:
@@ -0,0 +1,3 @@
1
+ export * from "./src/ProviderRegistry.ts";
2
+ export * from "./src/copyMove.ts";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -0,0 +1,2 @@
1
+ export * from "./src/ProviderRegistry.js";
2
+ export * from "./src/copyMove.js";
@@ -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;IAIzB,sBAAsB;IAItB,cAAc,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC;CAK3F"}
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,3 +1,3 @@
1
- import type { ChunkRef } from "@flowscripter/pluggable-io-framework-api";
1
+ import { type ChunkRef } from "@flowscripter/pluggable-io-framework-api";
2
2
  export declare function chunkLength(chunk: ChunkRef): number;
3
3
  //# sourceMappingURL=chunkLength.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"chunkLength.d.ts","sourceRoot":"","sources":["../../src/chunkLength.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0CAA0C,CAAC;AAEzE,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAEnD"}
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"}
@@ -1,3 +1,4 @@
1
+ import { ChunkKind } from "@flowscripter/pluggable-io-framework-api";
1
2
  export function chunkLength(chunk) {
2
- return chunk.kind === "js" ? chunk.data.byteLength : chunk.length;
3
+ return chunk.kind === ChunkKind.Js ? chunk.data.byteLength : chunk.length;
3
4
  }
@@ -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.0",
4
- "description": "Pluggable source/sink IO framework - provider discovery, copy/move orchestration, decorators, telemetry",
3
+ "version": "1.0.0",
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": "file:../dynamic-plugin-framework",
44
- "@flowscripter/pluggable-io-framework-api": "file:../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",
@@ -1,5 +1,5 @@
1
- import type { ChunkRef } from "@flowscripter/pluggable-io-framework-api";
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 === "js" ? chunk.data.byteLength : chunk.length;
4
+ return chunk.kind === ChunkKind.Js ? chunk.data.byteLength : chunk.length;
5
5
  }
package/src/copyMove.ts CHANGED
@@ -1,8 +1,11 @@
1
- import type {
2
- ChunkRef,
3
- IOProvider,
4
- Part,
5
- TelemetryHooks,
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
- telemetry: TelemetryHooks | undefined,
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
- const reader = (readable.stream as ReadableStream<ChunkRef>).getReader();
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
- telemetry: TelemetryHooks | undefined,
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
- const reader = (part.stream as ReadableStream<ChunkRef>).getReader();
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.telemetry,
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.telemetry,
163
+ options,
139
164
  properties.size,
140
165
  );
141
166
  }