@flowscripter/pluggable-io-framework 1.0.1 → 1.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/README.md CHANGED
@@ -24,6 +24,13 @@
24
24
  size crosses a configurable threshold) or plain streaming.
25
25
  - Reports progress via a global `TelemetryHooks` callback, tagged with a
26
26
  per-operation correlation id.
27
+ - Stream decorators:
28
+ - `seekable` wraps a handle that supports `RangeReadable` with a single
29
+ logical stream whose read position can be jumped via `seek(offset)`,
30
+ instead of requiring a fresh stream per range.
31
+ - `locallyCached` wraps a `StreamHandle` factory so the underlying source
32
+ is read at most once - later calls replay cached chunks in memory
33
+ without touching the source again.
27
34
  - See
28
35
  [io-plugin-filesystem](https://github.com/flowscripter/io-plugin-filesystem)
29
36
  for a reference local filesystem source/sink plugin.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from "./src/ProviderRegistry.ts";
2
2
  export * from "./src/copyMove.ts";
3
+ export * from "./src/decorators/locallyCached.ts";
4
+ export * from "./src/decorators/seekable.ts";
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,mCAAmC,CAAC;AAClD,cAAc,8BAA8B,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,4 @@
1
1
  export * from "./src/ProviderRegistry.js";
2
2
  export * from "./src/copyMove.js";
3
+ export * from "./src/decorators/locallyCached.js";
4
+ export * from "./src/decorators/seekable.js";
@@ -0,0 +1,15 @@
1
+ import type { ChunkKind, StreamHandle } from "@flowscripter/pluggable-io-framework-api";
2
+ /**
3
+ * Wraps a `StreamHandle` factory (e.g. `() => provider.getReadableStream(path)`)
4
+ * so the underlying source is read at most once - the first call drains the
5
+ * source while caching every chunk in memory; every subsequent call replays
6
+ * the cached chunks without touching the source again.
7
+ *
8
+ * A plain `StreamHandle` only exposes a single one-shot `ReadableStream`, so
9
+ * caching can't be a `StreamDecorator<K, C>` operating on an already-open
10
+ * handle (there would be nothing left to re-read on a second call) - it has
11
+ * to intercept the *open* operation itself, provider-agnostic regardless of
12
+ * what kind of source is behind it.
13
+ */
14
+ export declare function locallyCached<K extends ChunkKind>(open: () => Promise<StreamHandle<K>>): () => Promise<StreamHandle<K>>;
15
+ //# sourceMappingURL=locallyCached.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locallyCached.d.ts","sourceRoot":"","sources":["../../../src/decorators/locallyCached.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EAET,YAAY,EACb,MAAM,0CAA0C,CAAC;AAElD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,SAAS,EAC/C,IAAI,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GACnC,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAuChC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Wraps a `StreamHandle` factory (e.g. `() => provider.getReadableStream(path)`)
3
+ * so the underlying source is read at most once - the first call drains the
4
+ * source while caching every chunk in memory; every subsequent call replays
5
+ * the cached chunks without touching the source again.
6
+ *
7
+ * A plain `StreamHandle` only exposes a single one-shot `ReadableStream`, so
8
+ * caching can't be a `StreamDecorator<K, C>` operating on an already-open
9
+ * handle (there would be nothing left to re-read on a second call) - it has
10
+ * to intercept the *open* operation itself, provider-agnostic regardless of
11
+ * what kind of source is behind it.
12
+ */
13
+ export function locallyCached(open) {
14
+ let cache;
15
+ function replay(chunks) {
16
+ return new ReadableStream({
17
+ start(controller) {
18
+ for (const chunk of chunks)
19
+ controller.enqueue(chunk);
20
+ controller.close();
21
+ },
22
+ });
23
+ }
24
+ return async () => {
25
+ if (cache) {
26
+ return { kind: cache.kind, stream: replay(cache.chunks) };
27
+ }
28
+ const handle = await open();
29
+ const reader = handle.stream.getReader();
30
+ const chunks = [];
31
+ const stream = new ReadableStream({
32
+ async pull(controller) {
33
+ const { done, value } = await reader.read();
34
+ if (done) {
35
+ cache = { kind: handle.kind, chunks };
36
+ controller.close();
37
+ return;
38
+ }
39
+ chunks.push(value);
40
+ controller.enqueue(value);
41
+ },
42
+ cancel(reason) {
43
+ return reader.cancel(reason);
44
+ },
45
+ });
46
+ return { kind: handle.kind, stream };
47
+ };
48
+ }
@@ -0,0 +1,13 @@
1
+ import type { ChunkKind, RangeReadable, Seekable, StreamHandle } from "@flowscripter/pluggable-io-framework-api";
2
+ /**
3
+ * Wraps a handle that already supports {@link RangeReadable} (arbitrary
4
+ * byte-range reads) with a {@link Seekable} capability: a single logical
5
+ * stream whose read position can be jumped via `seek(offset)`, rather than
6
+ * requiring the caller to open a fresh stream per range.
7
+ *
8
+ * `seek` must not be called while a read from the current position is still
9
+ * in flight - like any seekable stream, seeking and reading are sequential,
10
+ * not concurrent, operations on the same handle.
11
+ */
12
+ export declare function seekable<K extends ChunkKind>(handle: StreamHandle<K> & RangeReadable<K>): StreamHandle<K> & Seekable;
13
+ //# sourceMappingURL=seekable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seekable.d.ts","sourceRoot":"","sources":["../../../src/decorators/seekable.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EAET,aAAa,EACb,QAAQ,EACR,YAAY,EACb,MAAM,0CAA0C,CAAC;AAElD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,SAAS,EAC1C,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,GACzC,YAAY,CAAC,CAAC,CAAC,GAAG,QAAQ,CAiC5B"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Wraps a handle that already supports {@link RangeReadable} (arbitrary
3
+ * byte-range reads) with a {@link Seekable} capability: a single logical
4
+ * stream whose read position can be jumped via `seek(offset)`, rather than
5
+ * requiring the caller to open a fresh stream per range.
6
+ *
7
+ * `seek` must not be called while a read from the current position is still
8
+ * in flight - like any seekable stream, seeking and reading are sequential,
9
+ * not concurrent, operations on the same handle.
10
+ */
11
+ export function seekable(handle) {
12
+ let reader = handle.stream.getReader();
13
+ const stream = new ReadableStream({
14
+ async pull(controller) {
15
+ const { done, value } = await reader.read();
16
+ if (done) {
17
+ controller.close();
18
+ return;
19
+ }
20
+ controller.enqueue(value);
21
+ },
22
+ cancel(reason) {
23
+ return reader.cancel(reason);
24
+ },
25
+ },
26
+ // Default queuing strategy pre-fetches one chunk ahead of what the
27
+ // consumer has actually asked for, which would call pull() again -
28
+ // hitting "done" on the pre-seek reader and closing the stream - before
29
+ // a seek() issued between two explicit reads ever runs. highWaterMark 0
30
+ // makes pull() fire only for an explicit read, never speculatively.
31
+ { highWaterMark: 0 });
32
+ return {
33
+ kind: handle.kind,
34
+ stream,
35
+ async seek(offset) {
36
+ const ranged = await handle.readRange(offset, Number.MAX_SAFE_INTEGER);
37
+ reader = ranged.getReader();
38
+ },
39
+ };
40
+ }
package/index.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export * from "./src/ProviderRegistry.ts";
2
2
  export * from "./src/copyMove.ts";
3
+ export * from "./src/decorators/locallyCached.ts";
4
+ export * from "./src/decorators/seekable.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/pluggable-io-framework",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "A pluggable source/sink IO framework using https://github.com/flowscripter/dynamic-plugin-framework",
5
5
  "keywords": [
6
6
  "bun",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@flowscripter/dynamic-plugin-framework": "^2.2.0",
44
- "@flowscripter/pluggable-io-framework-api": "^1.0.1"
44
+ "@flowscripter/pluggable-io-framework-api": "^1.0.4"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/bun": "^1.3.14",
@@ -0,0 +1,60 @@
1
+ import type {
2
+ ChunkKind,
3
+ ChunkOfKind,
4
+ StreamHandle,
5
+ } from "@flowscripter/pluggable-io-framework-api";
6
+
7
+ /**
8
+ * Wraps a `StreamHandle` factory (e.g. `() => provider.getReadableStream(path)`)
9
+ * so the underlying source is read at most once - the first call drains the
10
+ * source while caching every chunk in memory; every subsequent call replays
11
+ * the cached chunks without touching the source again.
12
+ *
13
+ * A plain `StreamHandle` only exposes a single one-shot `ReadableStream`, so
14
+ * caching can't be a `StreamDecorator<K, C>` operating on an already-open
15
+ * handle (there would be nothing left to re-read on a second call) - it has
16
+ * to intercept the *open* operation itself, provider-agnostic regardless of
17
+ * what kind of source is behind it.
18
+ */
19
+ export function locallyCached<K extends ChunkKind>(
20
+ open: () => Promise<StreamHandle<K>>,
21
+ ): () => Promise<StreamHandle<K>> {
22
+ let cache: { kind: K; chunks: ChunkOfKind<K>[] } | undefined;
23
+
24
+ function replay(chunks: ChunkOfKind<K>[]): ReadableStream<ChunkOfKind<K>> {
25
+ return new ReadableStream<ChunkOfKind<K>>({
26
+ start(controller) {
27
+ for (const chunk of chunks) controller.enqueue(chunk);
28
+ controller.close();
29
+ },
30
+ });
31
+ }
32
+
33
+ return async () => {
34
+ if (cache) {
35
+ return { kind: cache.kind, stream: replay(cache.chunks) };
36
+ }
37
+
38
+ const handle = await open();
39
+ const reader = (handle.stream as ReadableStream<ChunkOfKind<K>>).getReader();
40
+ const chunks: ChunkOfKind<K>[] = [];
41
+
42
+ const stream = new ReadableStream<ChunkOfKind<K>>({
43
+ async pull(controller) {
44
+ const { done, value } = await reader.read();
45
+ if (done) {
46
+ cache = { kind: handle.kind, chunks };
47
+ controller.close();
48
+ return;
49
+ }
50
+ chunks.push(value);
51
+ controller.enqueue(value);
52
+ },
53
+ cancel(reason) {
54
+ return reader.cancel(reason);
55
+ },
56
+ });
57
+
58
+ return { kind: handle.kind, stream };
59
+ };
60
+ }
@@ -0,0 +1,54 @@
1
+ import type {
2
+ ChunkKind,
3
+ ChunkOfKind,
4
+ RangeReadable,
5
+ Seekable,
6
+ StreamHandle,
7
+ } from "@flowscripter/pluggable-io-framework-api";
8
+
9
+ /**
10
+ * Wraps a handle that already supports {@link RangeReadable} (arbitrary
11
+ * byte-range reads) with a {@link Seekable} capability: a single logical
12
+ * stream whose read position can be jumped via `seek(offset)`, rather than
13
+ * requiring the caller to open a fresh stream per range.
14
+ *
15
+ * `seek` must not be called while a read from the current position is still
16
+ * in flight - like any seekable stream, seeking and reading are sequential,
17
+ * not concurrent, operations on the same handle.
18
+ */
19
+ export function seekable<K extends ChunkKind>(
20
+ handle: StreamHandle<K> & RangeReadable<K>,
21
+ ): StreamHandle<K> & Seekable {
22
+ let reader = (handle.stream as ReadableStream<ChunkOfKind<K>>).getReader();
23
+
24
+ const stream = new ReadableStream<ChunkOfKind<K>>(
25
+ {
26
+ async pull(controller) {
27
+ const { done, value } = await reader.read();
28
+ if (done) {
29
+ controller.close();
30
+ return;
31
+ }
32
+ controller.enqueue(value);
33
+ },
34
+ cancel(reason) {
35
+ return reader.cancel(reason);
36
+ },
37
+ },
38
+ // Default queuing strategy pre-fetches one chunk ahead of what the
39
+ // consumer has actually asked for, which would call pull() again -
40
+ // hitting "done" on the pre-seek reader and closing the stream - before
41
+ // a seek() issued between two explicit reads ever runs. highWaterMark 0
42
+ // makes pull() fire only for an explicit read, never speculatively.
43
+ { highWaterMark: 0 },
44
+ );
45
+
46
+ return {
47
+ kind: handle.kind,
48
+ stream,
49
+ async seek(offset: number) {
50
+ const ranged = await handle.readRange(offset, Number.MAX_SAFE_INTEGER);
51
+ reader = ranged.getReader();
52
+ },
53
+ };
54
+ }