@flowscripter/pluggable-io-framework-api 1.0.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 CHANGED
@@ -5,63 +5,47 @@
5
5
  [![docs](https://img.shields.io/badge/docs-API-blue)](https://flowscripter.github.io/pluggable-io-framework-api/index.html)
6
6
  [![license: MIT](https://img.shields.io/github/license/flowscripter/pluggable-io-framework-api)](https://github.com/flowscripter/pluggable-io-framework-api/blob/main/LICENSE)
7
7
 
8
- > API contracts for [pluggable-io-framework](https://github.com/flowscripter/pluggable-io-framework)
9
- > source/sink provider plugins
8
+ > API for the https://github.com/flowscripter/pluggable-io-framework
10
9
 
11
10
  ## Key Features
12
11
 
13
12
  - Defines the `IOProviderFactory`/`IOProvider` contract that source/sink
14
- plugins (e.g. local filesystem, object storage) implement, discovered and
13
+ plugins (e.g. local filesystem, object storage) implement which are then discovered and
15
14
  loaded via
16
15
  [dynamic-plugin-framework](https://github.com/flowscripter/dynamic-plugin-framework).
17
- - Config and per-item property schemas are defined with
18
- [Zod](https://zod.dev) (source of truth), portable to JSON Schema via
19
- `zod-to-json-schema` for hand-edited config files or non-TS validation.
20
- - `ChunkRef`: a tagged-union stream payload (`js` vs `native`) that carries
21
- memory ownership/origin with it, enabling zero-copy handoff to/from
16
+ - `IOProvider`, `StreamHandle` and `Part` are tagged with the single
17
+ `ChunkKind` ("js" or "native") a provider natively produces/consumes -
18
+ streams are homogeneous, so consumers never test each chunk's kind. A
19
+ mismatch between two linked streams is decided once per link via
20
+ `adaptReadableStream`, not once per chunk.
21
+ - `JsChunk`/`NativeChunk`: a tagged-union stream payload that carries memory
22
+ ownership/origin with it, enabling zero-copy handoff to/from
22
23
  Rust-FFI-backed providers and decorators, with a small adapter to the
23
24
  standard Web Streams `ReadableStream<Uint8Array>`/`WritableStream<Uint8Array>`
24
25
  for interop (`fetch`, `pipeTo`, etc.).
26
+ - Capabilities beyond plain streaming (e.g. `seekable`) are modeled as
27
+ small interfaces (`Seekable`, `RangeReadable`) with co-located type guards
28
+ (`isSeekable`, `isRangeReadable`).
25
29
  - Well-known item properties (`size`, `lastModified`, `isFolder`,
26
- `contentType`) guaranteed by every provider, plus a provider-specific
27
- `properties` extension bag for anything else (etag, storage class, custom
28
- tags).
29
- - Multipart transfer modeled as a stream of independently readable/writable
30
- `Part` handles, so parts can be processed concurrently.
31
- - Telemetry as a global `TelemetryHooks` object supplied once at
32
- initialisation; every operation reports through it tagged with a
33
- correlation id.
34
- - Pure TypeScript, minimal dependencies, no concrete provider
35
- implementations - see
30
+ `contentType`) are default for every provider.
31
+ - A provider-specific
32
+ `properties` extension bag supports other properties (e.g. etag, storage class, custom tags).
33
+ - Provider config and per-item property schemas are defined with
34
+ [Zod](https://zod.dev).
35
+ - Multipart transfers are modeled as a stream of independently readable/writable
36
+ `Part` handles allowing parts to be processed concurrently.
37
+ - A global `TelemetryHooks` object is supplied once at
38
+ initialisation and every operation reports through it tagged with a
39
+ correlation ID.
40
+ - Disposal is `Symbol.asyncDispose` (TC39 explicit resource management) -
41
+ `await using provider = await factory.createProvider(config)` disposes
42
+ deterministically, including on thrown errors.
43
+ - See
36
44
  [pluggable-io-framework](https://github.com/flowscripter/pluggable-io-framework)
37
45
  for orchestration and
38
46
  [pluggable-io-framework-plugin-filesystem](https://github.com/flowscripter/pluggable-io-framework-plugin-filesystem)
39
47
  for a reference implementation.
40
48
 
41
- ## Bun Module Usage
42
-
43
- Add the module:
44
-
45
- `bun add @flowscripter/pluggable-io-framework-api`
46
-
47
- Implement a provider factory:
48
-
49
- ```typescript
50
- import { z } from "zod";
51
- import type { IOProviderFactory } from "@flowscripter/pluggable-io-framework-api";
52
-
53
- const configSchema = z.object({ rootPath: z.string() });
54
- const propertySchema = z.object({ etag: z.string().optional() });
55
-
56
- const factory: IOProviderFactory<z.infer<typeof configSchema>> = {
57
- configSchema,
58
- propertySchema,
59
- async createProvider(config) {
60
- // return an IOProvider implementation
61
- },
62
- };
63
- ```
64
-
65
49
  ## Development
66
50
 
67
51
  Install dependencies:
@@ -95,10 +79,10 @@ Generate HTML API Documentation:
95
79
  ```mermaid
96
80
  classDiagram
97
81
  IOProviderFactory --> IOProvider : creates
98
- IOProvider --> StreamHandle : returns
99
- IOProvider --> Part : returns (multipart)
100
- StreamHandle --> ChunkRef : streams
101
- Part --> ChunkRef : streams
82
+ IOProvider --> StreamHandle : returns (kind K)
83
+ IOProvider --> Part : returns (multipart, kind K)
84
+ StreamHandle --> JsChunk : streams (kind "js")
85
+ StreamHandle --> NativeChunk : streams (kind "native")
102
86
  IOProvider --> ItemProperties : returns
103
87
 
104
88
  class IOProviderFactory {
@@ -107,7 +91,8 @@ classDiagram
107
91
  +createProvider(config)
108
92
  }
109
93
  class IOProvider {
110
- +dispose()
94
+ +kind: K
95
+ +[Symbol.asyncDispose]()
111
96
  +list(path, options)
112
97
  +getProperties(path)
113
98
  +setProperties(path, properties)
@@ -1,40 +1,58 @@
1
1
  /**
2
- * A chunk of stream payload, tagged with its memory origin/ownership.
3
- *
4
- * This is the actual unit flowing through {@link StreamHandle} and
5
- * {@link Part} streams - not a TS-vs-Rust-specific type. A pure-TS pipeline
6
- * is simply a stream of `kind: "js"` chunks end to end.
7
- *
8
- * Tagging origin lets a consumer choose a zero-copy pointer handoff when
9
- * compatible (js -> rust always; rust -> rust always) or fall back to an
10
- * explicit copy when required (rust -> js, since JS code cannot safely
11
- * retain a raw pointer past the call that produced it).
12
- *
13
- * `attributes` is deliberately shaped close to the future Flowscripter
14
- * runtime's Item (attributes + payload) so this type is a natural fit if/when
15
- * an `adapt` operator wraps these streams later. It is unused today.
2
+ * The two possible chunk memory origins.
16
3
  */
17
- export type ChunkRef = {
18
- readonly kind: "js";
4
+ export declare enum ChunkKind {
5
+ Js = "js",
6
+ Native = "native"
7
+ }
8
+ /** A chunk whose payload lives in a normal JS-managed Uint8Array. */
9
+ export interface JsChunk {
10
+ readonly kind: ChunkKind.Js;
19
11
  readonly data: Uint8Array;
20
12
  readonly attributes?: Readonly<Record<string, unknown>>;
21
- } | {
22
- readonly kind: "native";
13
+ }
14
+ /**
15
+ * A chunk whose payload lives in memory owned outside the JS heap (e.g. a
16
+ * Rust-allocated buffer). `release()` must be called once nothing needs the
17
+ * buffer - ownership/lifetime is explicit rather than GC'd, since JS code
18
+ * cannot safely retain a raw pointer past the point its owner frees it.
19
+ */
20
+ export interface NativeChunk {
21
+ readonly kind: ChunkKind.Native;
23
22
  readonly ptr: number;
24
23
  readonly length: number;
25
24
  release(): void;
26
25
  readonly attributes?: Readonly<Record<string, unknown>>;
27
- };
26
+ }
27
+ /**
28
+ * A chunk of stream payload, tagged with its memory origin/ownership.
29
+ */
30
+ export type ChunkRef = JsChunk | NativeChunk;
31
+ /** The concrete chunk type produced by a stream tagged with a given kind. */
32
+ export type ChunkOfKind<K extends ChunkKind> = Extract<ChunkRef, {
33
+ kind: K;
34
+ }>;
35
+ /**
36
+ * Converts a single chunk to the target kind. Pure-TS code can only ever
37
+ * implement the identity case (same kind in, same kind out) - a real
38
+ * js<->native conversion needs FFI-capable pointer access and must be
39
+ * supplied by a runtime-specific package (e.g. via `bun:ffi`).
40
+ */
41
+ export type ChunkConverter = (chunk: ChunkRef, toKind: ChunkKind) => ChunkRef;
42
+ /** Identity converter - only handles chunks already of the requested kind. */
43
+ export declare const identityChunkConverter: ChunkConverter;
28
44
  /**
29
- * Copy a chunk's bytes into a plain Uint8Array, regardless of origin.
30
- * The only copy incurred is for `kind: "native"` chunks.
45
+ * Adapts a homogeneous stream of one kind to another, using `convert`. When
46
+ * `fromKind === toKind` the stream is passed straight through untouched (no
47
+ * per-chunk work at all). This is the ONE place a kind mismatch is decided -
48
+ * once per stream link, not once per chunk.
31
49
  */
32
- export declare function toUint8Array(chunk: ChunkRef): Uint8Array;
50
+ export declare function adaptReadableStream<From extends ChunkKind, To extends ChunkKind>(stream: ReadableStream<ChunkOfKind<From>>, fromKind: From, toKind: To, convert?: ChunkConverter): ReadableStream<ChunkOfKind<To>>;
33
51
  /**
34
52
  * Adapter to the standard Web Streams interop surface (fetch, pipeTo
35
53
  * external consumers). This is the one clearly-marked copy boundary -
36
54
  * internal source/sink/decorator code speaks {@link ChunkRef} directly.
37
55
  */
38
- export declare function toWebReadableStream(source: ReadableStream<ChunkRef>): ReadableStream<Uint8Array>;
39
- export declare function fromWebReadableStream(source: ReadableStream<Uint8Array>): ReadableStream<ChunkRef>;
56
+ export declare function toWebReadableStream(source: ReadableStream<JsChunk>): ReadableStream<Uint8Array>;
57
+ export declare function fromWebReadableStream(source: ReadableStream<Uint8Array>): ReadableStream<JsChunk>;
40
58
  //# sourceMappingURL=ChunkRef.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ChunkRef.d.ts","sourceRoot":"","sources":["../../src/ChunkRef.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,QAAQ,GAChB;IACE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACzD,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,OAAO,IAAI,IAAI,CAAC;IAChB,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACzD,CAAC;AAEN;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,UAAU,CAOxD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,GAAG,cAAc,CAAC,UAAU,CAAC,CAehG;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GACjC,cAAc,CAAC,QAAQ,CAAC,CAe1B"}
1
+ {"version":3,"file":"ChunkRef.d.ts","sourceRoot":"","sources":["../../src/ChunkRef.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,oBAAY,SAAS;IACnB,EAAE,OAAO;IACT,MAAM,WAAW;CAClB;AAED,qEAAqE;AACrE,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACzD;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,OAAO,IAAI,IAAI,CAAC;IAChB,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACzD;AAED;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,WAAW,CAAC;AAE7C,6EAA6E;AAC7E,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,SAAS,IAAI,OAAO,CAAC,QAAQ,EAAE;IAAE,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC,CAAC;AAE9E;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,KAAK,QAAQ,CAAC;AAE9E,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,EAAE,cAOpC,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,SAAS,SAAS,EAAE,EAAE,SAAS,SAAS,EAC9E,MAAM,EAAE,cAAc,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,EACzC,QAAQ,EAAE,IAAI,EACd,MAAM,EAAE,EAAE,EACV,OAAO,GAAE,cAAuC,GAC/C,cAAc,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAkBjC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,CAAC,OAAO,CAAC,GAAG,cAAc,CAAC,UAAU,CAAC,CAgB/F;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,cAAc,CAAC,OAAO,CAAC,CAejG"}
@@ -1,12 +1,42 @@
1
1
  /**
2
- * Copy a chunk's bytes into a plain Uint8Array, regardless of origin.
3
- * The only copy incurred is for `kind: "native"` chunks.
2
+ * The two possible chunk memory origins.
4
3
  */
5
- export function toUint8Array(chunk) {
6
- if (chunk.kind === "js") {
7
- return chunk.data;
4
+ export var ChunkKind;
5
+ (function (ChunkKind) {
6
+ ChunkKind["Js"] = "js";
7
+ ChunkKind["Native"] = "native";
8
+ })(ChunkKind || (ChunkKind = {}));
9
+ /** Identity converter - only handles chunks already of the requested kind. */
10
+ export const identityChunkConverter = (chunk, toKind) => {
11
+ if (chunk.kind === toKind) {
12
+ return chunk;
8
13
  }
9
- throw new Error("Copying a native ChunkRef to Uint8Array requires an FFI-capable runtime helper - not implemented in pluggable-io-framework-api");
14
+ throw new Error(`Cannot convert a "${chunk.kind}" chunk to "${toKind}" without an FFI-capable ChunkConverter`);
15
+ };
16
+ /**
17
+ * Adapts a homogeneous stream of one kind to another, using `convert`. When
18
+ * `fromKind === toKind` the stream is passed straight through untouched (no
19
+ * per-chunk work at all). This is the ONE place a kind mismatch is decided -
20
+ * once per stream link, not once per chunk.
21
+ */
22
+ export function adaptReadableStream(stream, fromKind, toKind, convert = identityChunkConverter) {
23
+ if (fromKind === toKind) {
24
+ return stream;
25
+ }
26
+ const reader = stream.getReader();
27
+ return new ReadableStream({
28
+ async pull(controller) {
29
+ const { done, value } = await reader.read();
30
+ if (done) {
31
+ controller.close();
32
+ return;
33
+ }
34
+ controller.enqueue(convert(value, toKind));
35
+ },
36
+ cancel(reason) {
37
+ return reader.cancel(reason);
38
+ },
39
+ });
10
40
  }
11
41
  /**
12
42
  * Adapter to the standard Web Streams interop surface (fetch, pipeTo
@@ -22,7 +52,8 @@ export function toWebReadableStream(source) {
22
52
  controller.close();
23
53
  return;
24
54
  }
25
- controller.enqueue(toUint8Array(value));
55
+ // value is statically a JsChunk here - no per-chunk kind check needed.
56
+ controller.enqueue(value.data);
26
57
  },
27
58
  cancel(reason) {
28
59
  return reader.cancel(reason);
@@ -38,7 +69,7 @@ export function fromWebReadableStream(source) {
38
69
  controller.close();
39
70
  return;
40
71
  }
41
- controller.enqueue({ kind: "js", data: value });
72
+ controller.enqueue({ kind: ChunkKind.Js, data: value });
42
73
  },
43
74
  cancel(reason) {
44
75
  return reader.cancel(reason);
@@ -1,16 +1,22 @@
1
+ import type { ChunkKind } from "./ChunkRef.ts";
1
2
  import type { ItemProperties } from "./ItemProperties.ts";
2
3
  import type { Part } from "./Part.ts";
3
4
  import type { StreamHandle } from "./StreamHandle.ts";
4
5
  /**
5
6
  * A configured source/sink instance, as returned by
6
- * {@link IOProviderFactory.createProvider}.
7
+ * {@link IOProviderFactory.createProvider}. `K` is the single
8
+ * {@link ChunkKind} this provider natively produces/consumes - e.g. a
9
+ * pure-TS filesystem plugin is `IOProvider<"js">`, a Rust-FFI-backed plugin
10
+ * is `IOProvider<"native">`.
7
11
  *
8
- * Disposal is the explicit {@link IOProvider.dispose} method - not a reliance
9
- * on `Symbol.asyncDispose` - so plugins have an unambiguous, discoverable
10
- * contract for releasing connections/handles.
12
+ * Disposal is `Symbol.asyncDispose` (TC39 explicit resource management) -
13
+ * host code disposes deterministically via `await using provider = ...`,
14
+ * including on thrown errors, without needing a bespoke method name.
11
15
  */
12
- export interface IOProvider {
13
- dispose(): Promise<void>;
16
+ export interface IOProvider<K extends ChunkKind = ChunkKind> {
17
+ /** The single chunk kind this provider natively produces/consumes. */
18
+ readonly kind: K;
19
+ [Symbol.asyncDispose](): Promise<void>;
14
20
  list(path: string, options?: {
15
21
  recursive?: boolean;
16
22
  regex?: RegExp;
@@ -21,11 +27,11 @@ export interface IOProvider {
21
27
  getProperties(path: string): Promise<ItemProperties>;
22
28
  setProperties(path: string, properties: Partial<Record<string, unknown>>): Promise<void>;
23
29
  delete(path: string): Promise<void>;
24
- getReadableStream(path: string): Promise<StreamHandle>;
25
- getWritableStream(path: string): Promise<StreamHandle>;
26
- getMultipartReader(path: string): AsyncIterable<Part>;
30
+ getReadableStream(path: string): Promise<StreamHandle<K>>;
31
+ getWritableStream(path: string): Promise<StreamHandle<K>>;
32
+ getMultipartReader(path: string): AsyncIterable<Part<K>>;
27
33
  getMultipartWriter(path: string): {
28
- write(parts: AsyncIterable<Part>): Promise<void>;
34
+ write(parts: AsyncIterable<Part<K>>): Promise<void>;
29
35
  };
30
36
  /**
31
37
  * Self-reported direct-transfer eligibility - the provider owns what
@@ -1 +1 @@
1
- {"version":3,"file":"IOProvider.d.ts","sourceRoot":"","sources":["../../src/IOProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzB,IAAI,CACF,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAChD,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,cAAc,CAAA;KAAE,CAAC,CAAC;IAC/D,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACrD,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpC,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACvD,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACvD,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACtD,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,KAAK,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;IAEvF;;;;OAIG;IACH,iBAAiB,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC;IAC/C,UAAU,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,UAAU,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClE"}
1
+ {"version":3,"file":"IOProvider.d.ts","sourceRoot":"","sources":["../../src/IOProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS;IACzD,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAEjB,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvC,IAAI,CACF,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAChD,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,cAAc,CAAA;KAAE,CAAC,CAAC;IAC/D,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACrD,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpC,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1D,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1D,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACzD,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,KAAK,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;IAE1F;;;;OAIG;IACH,iBAAiB,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC;IAC/C,UAAU,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,UAAU,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClE"}
@@ -1,4 +1,5 @@
1
1
  import type { ZodType } from "zod";
2
+ import type { ChunkKind } from "./ChunkRef.ts";
2
3
  import type { IOProvider } from "./IOProvider.ts";
3
4
  /**
4
5
  * Extension point constant that a `dynamic-plugin-framework` `Plugin`'s
@@ -10,14 +11,10 @@ export declare const PLUGGABLE_IO_FRAMEWORK_PROVIDER_FACTORY_EXTENSION_POINT = "
10
11
  * Returned (as `unknown`, cast at the extension point boundary) from
11
12
  * `ExtensionFactory.create()` in a `dynamic-plugin-framework`
12
13
  * `ExtensionDescriptor`.
13
- *
14
- * Config/property schema: Zod is the source of truth; JSON Schema can be
15
- * derived via `zod-to-json-schema` for portability (hand-edited config
16
- * files, non-TS validation).
17
14
  */
18
- export interface IOProviderFactory<TConfig = unknown> {
15
+ export interface IOProviderFactory<TConfig = unknown, K extends ChunkKind = ChunkKind> {
19
16
  readonly configSchema: ZodType<TConfig>;
20
17
  readonly propertySchema: ZodType<Record<string, unknown>>;
21
- createProvider(config: TConfig): Promise<IOProvider>;
18
+ createProvider(config: TConfig): Promise<IOProvider<K>>;
22
19
  }
23
20
  //# sourceMappingURL=IOProviderFactory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"IOProviderFactory.d.ts","sourceRoot":"","sources":["../../src/IOProviderFactory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,uDAAuD,0DACX,CAAC;AAE1D;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB,CAAC,OAAO,GAAG,OAAO;IAClD,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1D,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACtD"}
1
+ {"version":3,"file":"IOProviderFactory.d.ts","sourceRoot":"","sources":["../../src/IOProviderFactory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACnC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,uDAAuD,0DACX,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,iBAAiB,CAAC,OAAO,GAAG,OAAO,EAAE,CAAC,SAAS,SAAS,GAAG,SAAS;IACnF,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1D,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;CACzD"}
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Properties of a file/folder item. `size`, `lastModified`, `isFolder` and
3
3
  * `contentType` are well-known, framework-guaranteed fields every provider
4
- * must populate so generic consumers (CLI, copy/move orchestration) can rely
5
- * on them without knowing the provider. Anything provider-specific (etag,
4
+ * must populate. Anything provider-specific (etag,
6
5
  * storage class, custom tags) goes in `properties`, validated against that
7
6
  * provider's `propertySchema`.
8
7
  */
@@ -1 +1 @@
1
- {"version":3,"file":"ItemProperties.d.ts","sourceRoot":"","sources":["../../src/ItemProperties.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,IAAI,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACxD"}
1
+ {"version":3,"file":"ItemProperties.d.ts","sourceRoot":"","sources":["../../src/ItemProperties.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,IAAI,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACxD"}
@@ -1,13 +1,15 @@
1
- import type { ChunkRef } from "./ChunkRef.ts";
1
+ import type { ChunkKind, ChunkOfKind } from "./ChunkRef.ts";
2
2
  /**
3
- * One independently readable/writable part of a multipart transfer.
4
- * Parts may be processed concurrently (e.g. `Promise.all` over N parts);
5
- * the provider assembles/commits them once all parts finish.
3
+ * One independently readable/writable part of a multipart transfer, carrying
4
+ * a homogeneous stream of a single declared {@link ChunkKind}. Parts may be
5
+ * processed concurrently (e.g. `Promise.all` over N parts); the provider
6
+ * assembles/commits them once all parts finish.
6
7
  */
7
- export interface Part {
8
+ export interface Part<K extends ChunkKind = ChunkKind> {
8
9
  readonly index: number;
9
10
  readonly offset: number;
10
- readonly stream: ReadableStream<ChunkRef> | WritableStream<ChunkRef>;
11
+ readonly kind: K;
12
+ readonly stream: ReadableStream<ChunkOfKind<K>> | WritableStream<ChunkOfKind<K>>;
11
13
  complete(): Promise<void>;
12
14
  }
13
15
  //# sourceMappingURL=Part.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Part.d.ts","sourceRoot":"","sources":["../../src/Part.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C;;;;GAIG;AACH,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;IACrE,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B"}
1
+ {"version":3,"file":"Part.d.ts","sourceRoot":"","sources":["../../src/Part.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,WAAW,IAAI,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS;IACnD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IACjF,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B"}
@@ -1,9 +1,22 @@
1
+ import type { ChunkKind, ChunkOfKind } from "./ChunkRef.ts";
1
2
  import type { StreamHandle } from "./StreamHandle.ts";
3
+ /** Capability added by the `seekable` decorator: jump to an absolute offset. */
4
+ export interface Seekable {
5
+ seek(offset: number): Promise<void>;
6
+ }
7
+ export declare function isSeekable<K extends ChunkKind>(handle: StreamHandle<K>): handle is StreamHandle<K> & Seekable;
8
+ /** Capability added by decorators that can serve an arbitrary byte range directly. */
9
+ export interface RangeReadable<K extends ChunkKind = ChunkKind> {
10
+ readRange(start: number, end: number): Promise<ReadableStream<ChunkOfKind<K>>>;
11
+ }
12
+ export declare function isRangeReadable<K extends ChunkKind>(handle: StreamHandle<K>): handle is StreamHandle<K> & RangeReadable<K>;
2
13
  /**
3
14
  * A decorator wraps a {@link StreamHandle} and returns an enhanced handle
4
- * with additional capability methods (e.g. `seekable` adds `seek`/
5
- * `readRange`; `locally-cached` transparently serves repeated reads from a
6
- * local cache).
15
+ * with additional capabilities `C` (e.g. {@link Seekable}). When `C` is
16
+ * known at the call site (the common case - a specific decorator is applied
17
+ * directly), consumers get static typing with no runtime capability check
18
+ * needed. Where a handle arrives already decorated by unknown/dynamic
19
+ * decorators, use the `is*` type guards above instead.
7
20
  */
8
- export type StreamDecorator = (handle: StreamHandle) => StreamHandle;
21
+ export type StreamDecorator<K extends ChunkKind = ChunkKind, C extends object = object> = (handle: StreamHandle<K>) => StreamHandle<K> & C;
9
22
  //# sourceMappingURL=StreamDecorator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"StreamDecorator.d.ts","sourceRoot":"","sources":["../../src/StreamDecorator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,YAAY,KAAK,YAAY,CAAC"}
1
+ {"version":3,"file":"StreamDecorator.d.ts","sourceRoot":"","sources":["../../src/StreamDecorator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,gFAAgF;AAChF,MAAM,WAAW,QAAQ;IACvB,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,wBAAgB,UAAU,CAAC,CAAC,SAAS,SAAS,EAC5C,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,MAAM,IAAI,YAAY,CAAC,CAAC,CAAC,GAAG,QAAQ,CAEtC;AAED,sFAAsF;AACtF,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS;IAC5D,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAChF;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,SAAS,EACjD,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,MAAM,IAAI,YAAY,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAE9C;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI,CACxF,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,KACpB,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC"}
@@ -1 +1,6 @@
1
- export {};
1
+ export function isSeekable(handle) {
2
+ return typeof handle.seek === "function";
3
+ }
4
+ export function isRangeReadable(handle) {
5
+ return typeof handle.readRange === "function";
6
+ }
@@ -1,15 +1,13 @@
1
- import type { ChunkRef } from "./ChunkRef.ts";
1
+ import type { ChunkKind, ChunkOfKind } from "./ChunkRef.ts";
2
2
  /**
3
- * A handle to a readable or writable stream of {@link ChunkRef}s, plus
4
- * optional capability methods for providers/decorators that support
5
- * random-access beyond plain sequential read (e.g. `seekable`).
6
- *
7
- * Capability methods are present only when supported - consumers
8
- * feature-detect via `typeof handle.seek === "function"` etc.
3
+ * A handle to a readable or writable stream of a single, declared
4
+ * {@link ChunkKind} - homogeneous, so consumers never need to test each
5
+ * chunk's kind. Plus optional capability methods for providers/decorators
6
+ * that support random-access beyond plain sequential read (e.g. `seekable`)
7
+ * - see {@link Seekable}/{@link isSeekable} in `StreamDecorator.ts`.
9
8
  */
10
- export interface StreamHandle {
11
- readonly stream: ReadableStream<ChunkRef> | WritableStream<ChunkRef>;
12
- seek?(offset: number): Promise<void>;
13
- readRange?(start: number, end: number): Promise<ReadableStream<ChunkRef>>;
9
+ export interface StreamHandle<K extends ChunkKind = ChunkKind> {
10
+ readonly kind: K;
11
+ readonly stream: ReadableStream<ChunkOfKind<K>> | WritableStream<ChunkOfKind<K>>;
14
12
  }
15
13
  //# sourceMappingURL=StreamHandle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"StreamHandle.d.ts","sourceRoot":"","sources":["../../src/StreamHandle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;IACrE,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,SAAS,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC;CAC3E"}
1
+ {"version":3,"file":"StreamHandle.d.ts","sourceRoot":"","sources":["../../src/StreamHandle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS;IAC3D,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;CAClF"}
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Global telemetry hooks supplied once at framework initialisation. Every
3
- * operation is given a correlation id and reports through the same hooks,
4
- * tagged with that id/operation-type - callers don't need to thread a
3
+ * operation is given a correlation ID and reports through the same hooks,
4
+ * tagged with that ID/operation-type. Callers don't need to thread a
5
5
  * callback through every call, but can still track individual operations
6
- * via the id in emitted events.
6
+ * via the ID in emitted events.
7
7
  */
8
8
  export interface TelemetryHooks {
9
9
  onProgress?(event: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flowscripter/pluggable-io-framework-api",
3
- "version": "1.0.0",
4
- "description": "API contracts for pluggable-io-framework source/sink provider plugins",
3
+ "version": "1.0.1",
4
+ "description": "API for the https://github.com/flowscripter/pluggable-io-framework",
5
5
  "keywords": [
6
6
  "api",
7
7
  "bun",
package/src/ChunkRef.ts CHANGED
@@ -1,44 +1,87 @@
1
+ /**
2
+ * The two possible chunk memory origins.
3
+ */
4
+ export enum ChunkKind {
5
+ Js = "js",
6
+ Native = "native",
7
+ }
8
+
9
+ /** A chunk whose payload lives in a normal JS-managed Uint8Array. */
10
+ export interface JsChunk {
11
+ readonly kind: ChunkKind.Js;
12
+ readonly data: Uint8Array;
13
+ readonly attributes?: Readonly<Record<string, unknown>>;
14
+ }
15
+
16
+ /**
17
+ * A chunk whose payload lives in memory owned outside the JS heap (e.g. a
18
+ * Rust-allocated buffer). `release()` must be called once nothing needs the
19
+ * buffer - ownership/lifetime is explicit rather than GC'd, since JS code
20
+ * cannot safely retain a raw pointer past the point its owner frees it.
21
+ */
22
+ export interface NativeChunk {
23
+ readonly kind: ChunkKind.Native;
24
+ readonly ptr: number;
25
+ readonly length: number;
26
+ release(): void;
27
+ readonly attributes?: Readonly<Record<string, unknown>>;
28
+ }
29
+
1
30
  /**
2
31
  * A chunk of stream payload, tagged with its memory origin/ownership.
3
- *
4
- * This is the actual unit flowing through {@link StreamHandle} and
5
- * {@link Part} streams - not a TS-vs-Rust-specific type. A pure-TS pipeline
6
- * is simply a stream of `kind: "js"` chunks end to end.
7
- *
8
- * Tagging origin lets a consumer choose a zero-copy pointer handoff when
9
- * compatible (js -> rust always; rust -> rust always) or fall back to an
10
- * explicit copy when required (rust -> js, since JS code cannot safely
11
- * retain a raw pointer past the call that produced it).
12
- *
13
- * `attributes` is deliberately shaped close to the future Flowscripter
14
- * runtime's Item (attributes + payload) so this type is a natural fit if/when
15
- * an `adapt` operator wraps these streams later. It is unused today.
16
32
  */
17
- export type ChunkRef =
18
- | {
19
- readonly kind: "js";
20
- readonly data: Uint8Array;
21
- readonly attributes?: Readonly<Record<string, unknown>>;
22
- }
23
- | {
24
- readonly kind: "native";
25
- readonly ptr: number;
26
- readonly length: number;
27
- release(): void;
28
- readonly attributes?: Readonly<Record<string, unknown>>;
29
- };
33
+ export type ChunkRef = JsChunk | NativeChunk;
34
+
35
+ /** The concrete chunk type produced by a stream tagged with a given kind. */
36
+ export type ChunkOfKind<K extends ChunkKind> = Extract<ChunkRef, { kind: K }>;
30
37
 
31
38
  /**
32
- * Copy a chunk's bytes into a plain Uint8Array, regardless of origin.
33
- * The only copy incurred is for `kind: "native"` chunks.
39
+ * Converts a single chunk to the target kind. Pure-TS code can only ever
40
+ * implement the identity case (same kind in, same kind out) - a real
41
+ * js<->native conversion needs FFI-capable pointer access and must be
42
+ * supplied by a runtime-specific package (e.g. via `bun:ffi`).
34
43
  */
35
- export function toUint8Array(chunk: ChunkRef): Uint8Array {
36
- if (chunk.kind === "js") {
37
- return chunk.data;
44
+ export type ChunkConverter = (chunk: ChunkRef, toKind: ChunkKind) => ChunkRef;
45
+
46
+ /** Identity converter - only handles chunks already of the requested kind. */
47
+ export const identityChunkConverter: ChunkConverter = (chunk, toKind) => {
48
+ if (chunk.kind === toKind) {
49
+ return chunk;
38
50
  }
39
51
  throw new Error(
40
- "Copying a native ChunkRef to Uint8Array requires an FFI-capable runtime helper - not implemented in pluggable-io-framework-api",
52
+ `Cannot convert a "${chunk.kind}" chunk to "${toKind}" without an FFI-capable ChunkConverter`,
41
53
  );
54
+ };
55
+
56
+ /**
57
+ * Adapts a homogeneous stream of one kind to another, using `convert`. When
58
+ * `fromKind === toKind` the stream is passed straight through untouched (no
59
+ * per-chunk work at all). This is the ONE place a kind mismatch is decided -
60
+ * once per stream link, not once per chunk.
61
+ */
62
+ export function adaptReadableStream<From extends ChunkKind, To extends ChunkKind>(
63
+ stream: ReadableStream<ChunkOfKind<From>>,
64
+ fromKind: From,
65
+ toKind: To,
66
+ convert: ChunkConverter = identityChunkConverter,
67
+ ): ReadableStream<ChunkOfKind<To>> {
68
+ if ((fromKind as ChunkKind) === (toKind as ChunkKind)) {
69
+ return stream as unknown as ReadableStream<ChunkOfKind<To>>;
70
+ }
71
+ const reader = stream.getReader();
72
+ return new ReadableStream<ChunkOfKind<To>>({
73
+ async pull(controller) {
74
+ const { done, value } = await reader.read();
75
+ if (done) {
76
+ controller.close();
77
+ return;
78
+ }
79
+ controller.enqueue(convert(value, toKind) as ChunkOfKind<To>);
80
+ },
81
+ cancel(reason) {
82
+ return reader.cancel(reason);
83
+ },
84
+ });
42
85
  }
43
86
 
44
87
  /**
@@ -46,7 +89,7 @@ export function toUint8Array(chunk: ChunkRef): Uint8Array {
46
89
  * external consumers). This is the one clearly-marked copy boundary -
47
90
  * internal source/sink/decorator code speaks {@link ChunkRef} directly.
48
91
  */
49
- export function toWebReadableStream(source: ReadableStream<ChunkRef>): ReadableStream<Uint8Array> {
92
+ export function toWebReadableStream(source: ReadableStream<JsChunk>): ReadableStream<Uint8Array> {
50
93
  const reader = source.getReader();
51
94
  return new ReadableStream<Uint8Array>({
52
95
  async pull(controller) {
@@ -55,7 +98,8 @@ export function toWebReadableStream(source: ReadableStream<ChunkRef>): ReadableS
55
98
  controller.close();
56
99
  return;
57
100
  }
58
- controller.enqueue(toUint8Array(value));
101
+ // value is statically a JsChunk here - no per-chunk kind check needed.
102
+ controller.enqueue(value.data);
59
103
  },
60
104
  cancel(reason) {
61
105
  return reader.cancel(reason);
@@ -63,18 +107,16 @@ export function toWebReadableStream(source: ReadableStream<ChunkRef>): ReadableS
63
107
  });
64
108
  }
65
109
 
66
- export function fromWebReadableStream(
67
- source: ReadableStream<Uint8Array>,
68
- ): ReadableStream<ChunkRef> {
110
+ export function fromWebReadableStream(source: ReadableStream<Uint8Array>): ReadableStream<JsChunk> {
69
111
  const reader = source.getReader();
70
- return new ReadableStream<ChunkRef>({
112
+ return new ReadableStream<JsChunk>({
71
113
  async pull(controller) {
72
114
  const { done, value } = await reader.read();
73
115
  if (done) {
74
116
  controller.close();
75
117
  return;
76
118
  }
77
- controller.enqueue({ kind: "js", data: value });
119
+ controller.enqueue({ kind: ChunkKind.Js, data: value });
78
120
  },
79
121
  cancel(reason) {
80
122
  return reader.cancel(reason);
package/src/IOProvider.ts CHANGED
@@ -1,17 +1,24 @@
1
+ import type { ChunkKind } from "./ChunkRef.ts";
1
2
  import type { ItemProperties } from "./ItemProperties.ts";
2
3
  import type { Part } from "./Part.ts";
3
4
  import type { StreamHandle } from "./StreamHandle.ts";
4
5
 
5
6
  /**
6
7
  * A configured source/sink instance, as returned by
7
- * {@link IOProviderFactory.createProvider}.
8
+ * {@link IOProviderFactory.createProvider}. `K` is the single
9
+ * {@link ChunkKind} this provider natively produces/consumes - e.g. a
10
+ * pure-TS filesystem plugin is `IOProvider<"js">`, a Rust-FFI-backed plugin
11
+ * is `IOProvider<"native">`.
8
12
  *
9
- * Disposal is the explicit {@link IOProvider.dispose} method - not a reliance
10
- * on `Symbol.asyncDispose` - so plugins have an unambiguous, discoverable
11
- * contract for releasing connections/handles.
13
+ * Disposal is `Symbol.asyncDispose` (TC39 explicit resource management) -
14
+ * host code disposes deterministically via `await using provider = ...`,
15
+ * including on thrown errors, without needing a bespoke method name.
12
16
  */
13
- export interface IOProvider {
14
- dispose(): Promise<void>;
17
+ export interface IOProvider<K extends ChunkKind = ChunkKind> {
18
+ /** The single chunk kind this provider natively produces/consumes. */
19
+ readonly kind: K;
20
+
21
+ [Symbol.asyncDispose](): Promise<void>;
15
22
 
16
23
  list(
17
24
  path: string,
@@ -21,10 +28,10 @@ export interface IOProvider {
21
28
  setProperties(path: string, properties: Partial<Record<string, unknown>>): Promise<void>;
22
29
  delete(path: string): Promise<void>;
23
30
 
24
- getReadableStream(path: string): Promise<StreamHandle>;
25
- getWritableStream(path: string): Promise<StreamHandle>;
26
- getMultipartReader(path: string): AsyncIterable<Part>;
27
- getMultipartWriter(path: string): { write(parts: AsyncIterable<Part>): Promise<void> };
31
+ getReadableStream(path: string): Promise<StreamHandle<K>>;
32
+ getWritableStream(path: string): Promise<StreamHandle<K>>;
33
+ getMultipartReader(path: string): AsyncIterable<Part<K>>;
34
+ getMultipartWriter(path: string): { write(parts: AsyncIterable<Part<K>>): Promise<void> };
28
35
 
29
36
  /**
30
37
  * Self-reported direct-transfer eligibility - the provider owns what
@@ -1,4 +1,5 @@
1
1
  import type { ZodType } from "zod";
2
+ import type { ChunkKind } from "./ChunkRef.ts";
2
3
  import type { IOProvider } from "./IOProvider.ts";
3
4
 
4
5
  /**
@@ -13,13 +14,9 @@ export const PLUGGABLE_IO_FRAMEWORK_PROVIDER_FACTORY_EXTENSION_POINT =
13
14
  * Returned (as `unknown`, cast at the extension point boundary) from
14
15
  * `ExtensionFactory.create()` in a `dynamic-plugin-framework`
15
16
  * `ExtensionDescriptor`.
16
- *
17
- * Config/property schema: Zod is the source of truth; JSON Schema can be
18
- * derived via `zod-to-json-schema` for portability (hand-edited config
19
- * files, non-TS validation).
20
17
  */
21
- export interface IOProviderFactory<TConfig = unknown> {
18
+ export interface IOProviderFactory<TConfig = unknown, K extends ChunkKind = ChunkKind> {
22
19
  readonly configSchema: ZodType<TConfig>;
23
20
  readonly propertySchema: ZodType<Record<string, unknown>>;
24
- createProvider(config: TConfig): Promise<IOProvider>;
21
+ createProvider(config: TConfig): Promise<IOProvider<K>>;
25
22
  }
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Properties of a file/folder item. `size`, `lastModified`, `isFolder` and
3
3
  * `contentType` are well-known, framework-guaranteed fields every provider
4
- * must populate so generic consumers (CLI, copy/move orchestration) can rely
5
- * on them without knowing the provider. Anything provider-specific (etag,
4
+ * must populate. Anything provider-specific (etag,
6
5
  * storage class, custom tags) goes in `properties`, validated against that
7
6
  * provider's `propertySchema`.
8
7
  */
package/src/Part.ts CHANGED
@@ -1,13 +1,15 @@
1
- import type { ChunkRef } from "./ChunkRef.ts";
1
+ import type { ChunkKind, ChunkOfKind } from "./ChunkRef.ts";
2
2
 
3
3
  /**
4
- * One independently readable/writable part of a multipart transfer.
5
- * Parts may be processed concurrently (e.g. `Promise.all` over N parts);
6
- * the provider assembles/commits them once all parts finish.
4
+ * One independently readable/writable part of a multipart transfer, carrying
5
+ * a homogeneous stream of a single declared {@link ChunkKind}. Parts may be
6
+ * processed concurrently (e.g. `Promise.all` over N parts); the provider
7
+ * assembles/commits them once all parts finish.
7
8
  */
8
- export interface Part {
9
+ export interface Part<K extends ChunkKind = ChunkKind> {
9
10
  readonly index: number;
10
11
  readonly offset: number;
11
- readonly stream: ReadableStream<ChunkRef> | WritableStream<ChunkRef>;
12
+ readonly kind: K;
13
+ readonly stream: ReadableStream<ChunkOfKind<K>> | WritableStream<ChunkOfKind<K>>;
12
14
  complete(): Promise<void>;
13
15
  }
@@ -1,9 +1,36 @@
1
+ import type { ChunkKind, ChunkOfKind } from "./ChunkRef.ts";
1
2
  import type { StreamHandle } from "./StreamHandle.ts";
2
3
 
4
+ /** Capability added by the `seekable` decorator: jump to an absolute offset. */
5
+ export interface Seekable {
6
+ seek(offset: number): Promise<void>;
7
+ }
8
+
9
+ export function isSeekable<K extends ChunkKind>(
10
+ handle: StreamHandle<K>,
11
+ ): handle is StreamHandle<K> & Seekable {
12
+ return typeof (handle as Partial<Seekable>).seek === "function";
13
+ }
14
+
15
+ /** Capability added by decorators that can serve an arbitrary byte range directly. */
16
+ export interface RangeReadable<K extends ChunkKind = ChunkKind> {
17
+ readRange(start: number, end: number): Promise<ReadableStream<ChunkOfKind<K>>>;
18
+ }
19
+
20
+ export function isRangeReadable<K extends ChunkKind>(
21
+ handle: StreamHandle<K>,
22
+ ): handle is StreamHandle<K> & RangeReadable<K> {
23
+ return typeof (handle as Partial<RangeReadable<K>>).readRange === "function";
24
+ }
25
+
3
26
  /**
4
27
  * A decorator wraps a {@link StreamHandle} and returns an enhanced handle
5
- * with additional capability methods (e.g. `seekable` adds `seek`/
6
- * `readRange`; `locally-cached` transparently serves repeated reads from a
7
- * local cache).
28
+ * with additional capabilities `C` (e.g. {@link Seekable}). When `C` is
29
+ * known at the call site (the common case - a specific decorator is applied
30
+ * directly), consumers get static typing with no runtime capability check
31
+ * needed. Where a handle arrives already decorated by unknown/dynamic
32
+ * decorators, use the `is*` type guards above instead.
8
33
  */
9
- export type StreamDecorator = (handle: StreamHandle) => StreamHandle;
34
+ export type StreamDecorator<K extends ChunkKind = ChunkKind, C extends object = object> = (
35
+ handle: StreamHandle<K>,
36
+ ) => StreamHandle<K> & C;
@@ -1,15 +1,13 @@
1
- import type { ChunkRef } from "./ChunkRef.ts";
1
+ import type { ChunkKind, ChunkOfKind } from "./ChunkRef.ts";
2
2
 
3
3
  /**
4
- * A handle to a readable or writable stream of {@link ChunkRef}s, plus
5
- * optional capability methods for providers/decorators that support
6
- * random-access beyond plain sequential read (e.g. `seekable`).
7
- *
8
- * Capability methods are present only when supported - consumers
9
- * feature-detect via `typeof handle.seek === "function"` etc.
4
+ * A handle to a readable or writable stream of a single, declared
5
+ * {@link ChunkKind} - homogeneous, so consumers never need to test each
6
+ * chunk's kind. Plus optional capability methods for providers/decorators
7
+ * that support random-access beyond plain sequential read (e.g. `seekable`)
8
+ * - see {@link Seekable}/{@link isSeekable} in `StreamDecorator.ts`.
10
9
  */
11
- export interface StreamHandle {
12
- readonly stream: ReadableStream<ChunkRef> | WritableStream<ChunkRef>;
13
- seek?(offset: number): Promise<void>;
14
- readRange?(start: number, end: number): Promise<ReadableStream<ChunkRef>>;
10
+ export interface StreamHandle<K extends ChunkKind = ChunkKind> {
11
+ readonly kind: K;
12
+ readonly stream: ReadableStream<ChunkOfKind<K>> | WritableStream<ChunkOfKind<K>>;
15
13
  }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Global telemetry hooks supplied once at framework initialisation. Every
3
- * operation is given a correlation id and reports through the same hooks,
4
- * tagged with that id/operation-type - callers don't need to thread a
3
+ * operation is given a correlation ID and reports through the same hooks,
4
+ * tagged with that ID/operation-type. Callers don't need to thread a
5
5
  * callback through every call, but can still track individual operations
6
- * via the id in emitted events.
6
+ * via the ID in emitted events.
7
7
  */
8
8
  export interface TelemetryHooks {
9
9
  onProgress?(event: {