@flowscripter/pluggable-io-framework 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Flowscripter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # pluggable-io-framework
2
+
3
+ [![version](https://img.shields.io/github/v/release/flowscripter/pluggable-io-framework?sort=semver)](https://github.com/flowscripter/pluggable-io-framework/releases)
4
+ [![build](https://img.shields.io/github/actions/workflow/status/flowscripter/pluggable-io-framework/release-bun-library.yml)](https://github.com/flowscripter/pluggable-io-framework/actions/workflows/release-bun-library.yml)
5
+ [![docs](https://img.shields.io/badge/docs-API-blue)](https://flowscripter.github.io/pluggable-io-framework/index.html)
6
+ [![license: MIT](https://img.shields.io/github/license/flowscripter/pluggable-io-framework)](https://github.com/flowscripter/pluggable-io-framework/blob/main/LICENSE)
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.)
11
+
12
+ ## Key Features
13
+
14
+ - Discovers and instantiates source/sink provider plugins (implementing the
15
+ `IOProviderFactory`/`IOProvider` contract from
16
+ [pluggable-io-framework-api](https://github.com/flowscripter/pluggable-io-framework-api))
17
+ via
18
+ [dynamic-plugin-framework](https://github.com/flowscripter/dynamic-plugin-framework),
19
+ including config validation against each plugin's Zod schema.
20
+ - Copy/move orchestration:
21
+ - Uses a provider's `directCopy`/`directMove` when
22
+ `canDirectTransfer` reports the source and sink are the same
23
+ underlying provider (e.g. same filesystem mount, same object storage
24
+ bucket).
25
+ - Otherwise transfers via multipart (when both sides support it and file
26
+ size crosses a configurable threshold) or plain streaming.
27
+ - Reports progress via a global `TelemetryHooks` callback, tagged with a
28
+ 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
+ - See
37
+ [pluggable-io-framework-plugin-filesystem](https://github.com/flowscripter/pluggable-io-framework-plugin-filesystem)
38
+ for a reference local filesystem source/sink plugin.
39
+
40
+ ## Bun Module Usage
41
+
42
+ Add the module:
43
+
44
+ `bun add @flowscripter/pluggable-io-framework`
45
+
46
+ Discover and use a provider:
47
+
48
+ ```typescript
49
+ import {
50
+ DefaultPluginManager,
51
+ LocalFolderPluginRepository,
52
+ } from "@flowscripter/dynamic-plugin-framework";
53
+ import { ProviderRegistry, copy } from "@flowscripter/pluggable-io-framework";
54
+
55
+ const pluginManager = new DefaultPluginManager([new LocalFolderPluginRepository("./plugins")]);
56
+ const registry = new ProviderRegistry(pluginManager);
57
+ await registry.discover();
58
+
59
+ const [extension] = await registry.listAvailableProviders();
60
+ const provider = await registry.createProvider(extension.extensionHandle, { rootPath: "/data" });
61
+
62
+ await copy(provider, "a.txt", provider, "b.txt", {
63
+ telemetry: { onProgress: (event) => console.log(event) },
64
+ });
65
+ ```
66
+
67
+ ## Development
68
+
69
+ Install dependencies:
70
+
71
+ `bun install`
72
+
73
+ Build (produces `dist/` for Node.js and TypeScript consumers; Bun uses raw source directly):
74
+
75
+ `bun run build`
76
+
77
+ Test:
78
+
79
+ `bun test`
80
+
81
+ Format:
82
+
83
+ `bunx oxfmt`
84
+
85
+ Lint:
86
+
87
+ `bunx oxlint index.ts src/ tests/`
88
+
89
+ Generate HTML API Documentation:
90
+
91
+ `bunx typedoc index.ts`
92
+
93
+ ## Documentation
94
+
95
+ ### Overview
96
+
97
+ ```mermaid
98
+ sequenceDiagram
99
+ participant Host
100
+ participant ProviderRegistry
101
+ participant PluginManager
102
+ participant Source as IOProvider (source)
103
+ participant Sink as IOProvider (sink)
104
+
105
+ Host->>ProviderRegistry: discover()
106
+ ProviderRegistry->>PluginManager: registerExtensions(extensionPoint)
107
+ Host->>ProviderRegistry: createProvider(handle, config)
108
+ ProviderRegistry->>PluginManager: instantiate(handle)
109
+ ProviderRegistry-->>Host: IOProvider
110
+
111
+ Host->>Source: copy(source, path, sink, path)
112
+ alt canDirectTransfer
113
+ Source->>Sink: directCopy(path, path)
114
+ else multipart eligible
115
+ Source-->>Sink: transfer Parts concurrently
116
+ else
117
+ Source-->>Sink: stream ChunkRefs
118
+ end
119
+ ```
120
+
121
+ ### API
122
+
123
+ Link to auto-generated API docs:
124
+
125
+ [API Documentation](https://flowscripter.github.io/pluggable-io-framework/index.html)
126
+
127
+ ## License
128
+
129
+ MIT © Flowscripter
@@ -0,0 +1,14 @@
1
+ import type { PluginManager } from "@flowscripter/dynamic-plugin-framework";
2
+ import { type IOProvider } from "@flowscripter/pluggable-io-framework-api";
3
+ /**
4
+ * Wraps a `dynamic-plugin-framework` {@link PluginManager} to discover and
5
+ * instantiate pluggable-io-framework source/sink provider plugins.
6
+ */
7
+ export declare class ProviderRegistry {
8
+ private readonly pluginManager;
9
+ constructor(pluginManager: PluginManager);
10
+ discover(): Promise<void>;
11
+ listAvailableProviders(): Promise<readonly import("@flowscripter/dynamic-plugin-framework").ExtensionInfo[]>;
12
+ createProvider(extensionHandle: string, config: unknown): Promise<IOProvider>;
13
+ }
14
+ //# sourceMappingURL=ProviderRegistry.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,22 @@
1
+ import { PLUGGABLE_IO_FRAMEWORK_PROVIDER_FACTORY_EXTENSION_POINT, } from "@flowscripter/pluggable-io-framework-api";
2
+ /**
3
+ * Wraps a `dynamic-plugin-framework` {@link PluginManager} to discover and
4
+ * instantiate pluggable-io-framework source/sink provider plugins.
5
+ */
6
+ export class ProviderRegistry {
7
+ pluginManager;
8
+ constructor(pluginManager) {
9
+ this.pluginManager = pluginManager;
10
+ }
11
+ async discover() {
12
+ await this.pluginManager.registerExtensions(PLUGGABLE_IO_FRAMEWORK_PROVIDER_FACTORY_EXTENSION_POINT);
13
+ }
14
+ async listAvailableProviders() {
15
+ return this.pluginManager.getRegisteredExtensions(PLUGGABLE_IO_FRAMEWORK_PROVIDER_FACTORY_EXTENSION_POINT);
16
+ }
17
+ async createProvider(extensionHandle, config) {
18
+ const factory = (await this.pluginManager.instantiate(extensionHandle));
19
+ const validatedConfig = factory.configSchema.parse(config);
20
+ return factory.createProvider(validatedConfig);
21
+ }
22
+ }
@@ -0,0 +1,3 @@
1
+ import type { ChunkRef } from "@flowscripter/pluggable-io-framework-api";
2
+ export declare function chunkLength(chunk: ChunkRef): number;
3
+ //# sourceMappingURL=chunkLength.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,3 @@
1
+ export function chunkLength(chunk) {
2
+ return chunk.kind === "js" ? chunk.data.byteLength : chunk.length;
3
+ }
package/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./src/ProviderRegistry.ts";
2
+ export * from "./src/copyMove.ts";
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
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",
5
+ "keywords": [
6
+ "bun",
7
+ "io",
8
+ "plugin",
9
+ "streaming"
10
+ ],
11
+ "homepage": "https://github.com/flowscripter/pluggable-io-framework#readme",
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/flowscripter/pluggable-io-framework.git"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src",
20
+ "index.ts",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "type": "module",
25
+ "main": "dist/index.js",
26
+ "module": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "bun": "./index.ts",
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ }
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.build.json",
40
+ "test": "bun test"
41
+ },
42
+ "dependencies": {
43
+ "@flowscripter/dynamic-plugin-framework": "file:../dynamic-plugin-framework",
44
+ "@flowscripter/pluggable-io-framework-api": "file:../pluggable-io-framework-api"
45
+ },
46
+ "devDependencies": {
47
+ "@types/bun": "^1.3.14",
48
+ "oxfmt": "0.57.0",
49
+ "oxlint": "1.72.0"
50
+ },
51
+ "peerDependencies": {
52
+ "typescript": "^6.0.3"
53
+ },
54
+ "typedocOptions": {
55
+ "readme": "none",
56
+ "exclude": [
57
+ "tests/**"
58
+ ]
59
+ }
60
+ }
@@ -0,0 +1,32 @@
1
+ import type { PluginManager } from "@flowscripter/dynamic-plugin-framework";
2
+ import {
3
+ PLUGGABLE_IO_FRAMEWORK_PROVIDER_FACTORY_EXTENSION_POINT,
4
+ type IOProvider,
5
+ type IOProviderFactory,
6
+ } from "@flowscripter/pluggable-io-framework-api";
7
+
8
+ /**
9
+ * Wraps a `dynamic-plugin-framework` {@link PluginManager} to discover and
10
+ * instantiate pluggable-io-framework source/sink provider plugins.
11
+ */
12
+ export class ProviderRegistry {
13
+ public constructor(private readonly pluginManager: PluginManager) {}
14
+
15
+ public async discover(): Promise<void> {
16
+ await this.pluginManager.registerExtensions(
17
+ PLUGGABLE_IO_FRAMEWORK_PROVIDER_FACTORY_EXTENSION_POINT,
18
+ );
19
+ }
20
+
21
+ public async listAvailableProviders() {
22
+ return this.pluginManager.getRegisteredExtensions(
23
+ PLUGGABLE_IO_FRAMEWORK_PROVIDER_FACTORY_EXTENSION_POINT,
24
+ );
25
+ }
26
+
27
+ public async createProvider(extensionHandle: string, config: unknown): Promise<IOProvider> {
28
+ const factory = (await this.pluginManager.instantiate(extensionHandle)) as IOProviderFactory;
29
+ const validatedConfig = factory.configSchema.parse(config);
30
+ return factory.createProvider(validatedConfig);
31
+ }
32
+ }
@@ -0,0 +1,5 @@
1
+ import type { ChunkRef } from "@flowscripter/pluggable-io-framework-api";
2
+
3
+ export function chunkLength(chunk: ChunkRef): number {
4
+ return chunk.kind === "js" ? chunk.data.byteLength : chunk.length;
5
+ }
@@ -0,0 +1,161 @@
1
+ import type {
2
+ ChunkRef,
3
+ IOProvider,
4
+ Part,
5
+ TelemetryHooks,
6
+ } from "@flowscripter/pluggable-io-framework-api";
7
+ import { chunkLength } from "./chunkLength.ts";
8
+
9
+ export interface TransferOptions {
10
+ readonly telemetry?: TelemetryHooks;
11
+ /** Minimum file size (bytes) before multipart transfer is attempted over plain streaming. */
12
+ readonly multipartThreshold?: number;
13
+ }
14
+
15
+ const DEFAULT_MULTIPART_THRESHOLD = 64 * 1024 * 1024;
16
+
17
+ async function streamingTransfer(
18
+ source: IOProvider,
19
+ sourcePath: string,
20
+ sink: IOProvider,
21
+ destPath: string,
22
+ operationId: string,
23
+ telemetry: TelemetryHooks | undefined,
24
+ totalBytes: number | undefined,
25
+ ): Promise<void> {
26
+ const readable = await source.getReadableStream(sourcePath);
27
+ const writable = await sink.getWritableStream(destPath);
28
+ const reader = (readable.stream as ReadableStream<ChunkRef>).getReader();
29
+ const writer = (writable.stream as WritableStream<ChunkRef>).getWriter();
30
+ let bytesProcessed = 0;
31
+ try {
32
+ for (;;) {
33
+ const { done, value } = await reader.read();
34
+ if (done) break;
35
+ await writer.write(value);
36
+ bytesProcessed += chunkLength(value);
37
+ telemetry?.onProgress?.({ operationId, type: "copy", bytesProcessed, totalBytes });
38
+ }
39
+ await writer.close();
40
+ } catch (error) {
41
+ await writer.abort(error);
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ async function multipartTransfer(
47
+ source: IOProvider,
48
+ sourcePath: string,
49
+ sink: IOProvider,
50
+ destPath: string,
51
+ operationId: string,
52
+ telemetry: TelemetryHooks | undefined,
53
+ totalBytes: number | undefined,
54
+ ): Promise<void> {
55
+ let bytesProcessed = 0;
56
+ const writer = sink.getMultipartWriter(destPath);
57
+ async function* transferParts(): AsyncIterable<Part> {
58
+ for await (const part of source.getMultipartReader(sourcePath)) {
59
+ const reader = (part.stream as ReadableStream<ChunkRef>).getReader();
60
+ const chunks: ChunkRef[] = [];
61
+ for (;;) {
62
+ const { done, value } = await reader.read();
63
+ if (done) break;
64
+ chunks.push(value);
65
+ bytesProcessed += chunkLength(value);
66
+ telemetry?.onProgress?.({ operationId, type: "copy", bytesProcessed, totalBytes });
67
+ }
68
+ yield {
69
+ index: part.index,
70
+ offset: part.offset,
71
+ stream: new ReadableStream<ChunkRef>({
72
+ start(controller) {
73
+ for (const chunk of chunks) controller.enqueue(chunk);
74
+ controller.close();
75
+ },
76
+ }),
77
+ complete: () => part.complete(),
78
+ };
79
+ }
80
+ }
81
+ await writer.write(transferParts());
82
+ }
83
+
84
+ function canUseMultipart(
85
+ source: IOProvider,
86
+ sink: IOProvider,
87
+ size: number | undefined,
88
+ threshold: number,
89
+ ): boolean {
90
+ return (
91
+ size !== undefined &&
92
+ size >= threshold &&
93
+ typeof source.getMultipartReader === "function" &&
94
+ typeof sink.getMultipartWriter === "function"
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Copies `sourcePath` on `source` to `destPath` on `sink`.
100
+ *
101
+ * Uses `source.directCopy` when `source.canDirectTransfer?.(sink)` reports
102
+ * eligibility (same-provider direct transfer). Otherwise falls back to
103
+ * multipart transfer (when both sides support it and size crosses
104
+ * `options.multipartThreshold`) or plain streaming.
105
+ */
106
+ export async function copy(
107
+ source: IOProvider,
108
+ sourcePath: string,
109
+ sink: IOProvider,
110
+ destPath: string,
111
+ options: TransferOptions = {},
112
+ ): Promise<void> {
113
+ const operationId = crypto.randomUUID();
114
+ if (source.canDirectTransfer?.(sink) && source.directCopy) {
115
+ await source.directCopy(sourcePath, destPath);
116
+ return;
117
+ }
118
+ const properties = await source.getProperties(sourcePath);
119
+ const threshold = options.multipartThreshold ?? DEFAULT_MULTIPART_THRESHOLD;
120
+ if (canUseMultipart(source, sink, properties.size, threshold)) {
121
+ await multipartTransfer(
122
+ source,
123
+ sourcePath,
124
+ sink,
125
+ destPath,
126
+ operationId,
127
+ options.telemetry,
128
+ properties.size,
129
+ );
130
+ return;
131
+ }
132
+ await streamingTransfer(
133
+ source,
134
+ sourcePath,
135
+ sink,
136
+ destPath,
137
+ operationId,
138
+ options.telemetry,
139
+ properties.size,
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Moves `sourcePath` on `source` to `destPath` on `sink`. Uses
145
+ * `source.directMove` when eligible, otherwise performs a {@link copy}
146
+ * followed by deleting the source item.
147
+ */
148
+ export async function move(
149
+ source: IOProvider,
150
+ sourcePath: string,
151
+ sink: IOProvider,
152
+ destPath: string,
153
+ options: TransferOptions = {},
154
+ ): Promise<void> {
155
+ if (source.canDirectTransfer?.(sink) && source.directMove) {
156
+ await source.directMove(sourcePath, destPath);
157
+ return;
158
+ }
159
+ await copy(source, sourcePath, sink, destPath, options);
160
+ await source.delete(sourcePath);
161
+ }