@flowscripter/pluggable-io-framework 1.0.1 → 1.1.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 +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/src/ProviderRegistry.d.ts.map +1 -1
- package/dist/src/decorators/locallyCached.d.ts +15 -0
- package/dist/src/decorators/locallyCached.d.ts.map +1 -0
- package/dist/src/decorators/locallyCached.js +48 -0
- package/dist/src/decorators/seekable.d.ts +13 -0
- package/dist/src/decorators/seekable.d.ts.map +1 -0
- package/dist/src/decorators/seekable.js +40 -0
- package/index.ts +2 -0
- package/package.json +7 -7
- package/src/decorators/locallyCached.ts +60 -0
- package/src/decorators/seekable.ts +54 -0
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
package/dist/index.d.ts.map
CHANGED
|
@@ -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 +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;
|
|
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;IAAjD,YAAoC,aAAa,EAAE,aAAa,EAAI;IAEvD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAIrC;IAEY,sBAAsB,uFAIlC;IAEY,cAAc,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAIzF;CACF"}
|
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowscripter/pluggable-io-framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "A pluggable source/sink IO framework using https://github.com/flowscripter/dynamic-plugin-framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -40,16 +40,16 @@
|
|
|
40
40
|
"test": "bun test"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@flowscripter/dynamic-plugin-framework": "^2.2.
|
|
44
|
-
"@flowscripter/pluggable-io-framework-api": "^1.0.
|
|
43
|
+
"@flowscripter/dynamic-plugin-framework": "^2.2.4",
|
|
44
|
+
"@flowscripter/pluggable-io-framework-api": "^1.0.4"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@types/bun": "^1.
|
|
48
|
-
"oxfmt": "0.
|
|
49
|
-
"oxlint": "1.
|
|
47
|
+
"@types/bun": "^1.4.0",
|
|
48
|
+
"oxfmt": "0.65.0",
|
|
49
|
+
"oxlint": "1.80.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"typescript": "^
|
|
52
|
+
"typescript": "^7.0.2"
|
|
53
53
|
},
|
|
54
54
|
"typedocOptions": {
|
|
55
55
|
"readme": "none",
|
|
@@ -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
|
+
}
|