@flowscripter/pluggable-io-framework 1.2.7 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -178
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -4
- package/dist/src/concurrency/AsyncChannel.d.ts +9 -0
- package/dist/src/concurrency/AsyncChannel.d.ts.map +1 -0
- package/dist/src/concurrency/AsyncChannel.js +48 -0
- package/dist/src/{ConcurrencyLimiter.d.ts → concurrency/ConcurrencyLimiter.d.ts} +1 -12
- package/dist/src/concurrency/ConcurrencyLimiter.d.ts.map +1 -0
- package/dist/src/concurrency/ConcurrencyLimiter.js +64 -0
- package/dist/src/concurrency/mapAsyncIterableConcurrently.d.ts +13 -0
- package/dist/src/concurrency/mapAsyncIterableConcurrently.d.ts.map +1 -0
- package/dist/src/concurrency/mapAsyncIterableConcurrently.js +61 -0
- package/dist/src/decorators/locallyCached.d.ts +9 -10
- package/dist/src/decorators/locallyCached.d.ts.map +1 -1
- package/dist/src/decorators/locallyCached.js +23 -16
- package/dist/src/decorators/seekable.d.ts +4 -4
- package/dist/src/decorators/seekable.d.ts.map +1 -1
- package/dist/src/decorators/seekable.js +2 -2
- package/dist/src/registry/ProviderRegistry.d.ts +52 -0
- package/dist/src/registry/ProviderRegistry.d.ts.map +1 -0
- package/dist/src/registry/ProviderRegistry.js +148 -0
- package/dist/src/registry/detectProtocol.d.ts +8 -0
- package/dist/src/registry/detectProtocol.d.ts.map +1 -0
- package/dist/src/registry/detectProtocol.js +15 -0
- package/dist/src/registry/negotiateTransfer.d.ts +29 -0
- package/dist/src/registry/negotiateTransfer.d.ts.map +1 -0
- package/dist/src/registry/negotiateTransfer.js +69 -0
- package/dist/src/retry/RetryOptions.d.ts +19 -0
- package/dist/src/retry/RetryOptions.d.ts.map +1 -0
- package/dist/src/retry/RetryOptions.js +9 -0
- package/dist/src/{withRetry.d.ts → retry/withRetry.d.ts} +1 -5
- package/dist/src/retry/withRetry.d.ts.map +1 -0
- package/dist/src/{withRetry.js → retry/withRetry.js} +2 -5
- package/dist/src/transfer/EntryContext.d.ts +16 -0
- package/dist/src/transfer/EntryContext.d.ts.map +1 -0
- package/dist/src/transfer/EntryContext.js +1 -0
- package/dist/src/transfer/TransferOptions.d.ts +45 -0
- package/dist/src/transfer/TransferOptions.d.ts.map +1 -0
- package/dist/src/transfer/TransferOptions.js +1 -0
- package/dist/src/transfer/TransferResult.d.ts +9 -0
- package/dist/src/transfer/TransferResult.d.ts.map +1 -0
- package/dist/src/transfer/TransferResult.js +1 -0
- package/dist/src/transfer/aggregateResults.d.ts +4 -0
- package/dist/src/transfer/aggregateResults.d.ts.map +1 -0
- package/dist/src/transfer/aggregateResults.js +9 -0
- package/dist/src/transfer/copyMove.d.ts +23 -0
- package/dist/src/transfer/copyMove.d.ts.map +1 -0
- package/dist/src/transfer/copyMove.js +68 -0
- package/dist/src/transfer/leaseTransfer.d.ts +14 -0
- package/dist/src/transfer/leaseTransfer.d.ts.map +1 -0
- package/dist/src/transfer/leaseTransfer.js +74 -0
- package/dist/src/transfer/multipartTransfer.d.ts +23 -0
- package/dist/src/transfer/multipartTransfer.d.ts.map +1 -0
- package/dist/src/transfer/multipartTransfer.js +94 -0
- package/dist/src/transfer/negotiatePartSize.d.ts +10 -0
- package/dist/src/transfer/negotiatePartSize.d.ts.map +1 -0
- package/dist/src/transfer/negotiatePartSize.js +27 -0
- package/dist/src/transfer/patternTransfer.d.ts +12 -0
- package/dist/src/transfer/patternTransfer.d.ts.map +1 -0
- package/dist/src/transfer/patternTransfer.js +61 -0
- package/dist/src/transfer/rangeReadableMultipartReader.d.ts +8 -0
- package/dist/src/transfer/rangeReadableMultipartReader.d.ts.map +1 -0
- package/dist/src/transfer/rangeReadableMultipartReader.js +19 -0
- package/dist/src/transfer/recursiveTransfer.d.ts +13 -0
- package/dist/src/transfer/recursiveTransfer.d.ts.map +1 -0
- package/dist/src/transfer/recursiveTransfer.js +112 -0
- package/dist/src/transfer/streamTransfer.d.ts +32 -0
- package/dist/src/transfer/streamTransfer.d.ts.map +1 -0
- package/dist/src/transfer/streamTransfer.js +186 -0
- package/dist/src/transfer/transferEntry.d.ts +25 -0
- package/dist/src/transfer/transferEntry.d.ts.map +1 -0
- package/dist/src/transfer/transferEntry.js +136 -0
- package/dist/src/util/abort.d.ts +12 -0
- package/dist/src/util/abort.d.ts.map +1 -0
- package/dist/src/util/abort.js +39 -0
- package/dist/src/util/applyPayloadConverter.d.ts +9 -0
- package/dist/src/util/applyPayloadConverter.d.ts.map +1 -0
- package/dist/src/util/applyPayloadConverter.js +26 -0
- package/dist/src/util/describePath.d.ts +6 -0
- package/dist/src/util/describePath.d.ts.map +1 -0
- package/dist/src/util/describePath.js +9 -0
- package/dist/src/util/globToRegex.d.ts +8 -0
- package/dist/src/util/globToRegex.d.ts.map +1 -0
- package/dist/src/util/globToRegex.js +35 -0
- package/dist/src/util/itemLength.d.ts +4 -0
- package/dist/src/util/itemLength.d.ts.map +1 -0
- package/dist/src/util/itemLength.js +5 -0
- package/dist/src/util/keys.d.ts +8 -0
- package/dist/src/util/keys.d.ts.map +1 -0
- package/dist/src/util/keys.js +19 -0
- package/dist/src/util/withParentOperationId.d.ts +4 -0
- package/dist/src/util/withParentOperationId.d.ts.map +1 -0
- package/dist/src/util/withParentOperationId.js +12 -0
- package/index.ts +11 -4
- package/package.json +4 -3
- package/src/concurrency/AsyncChannel.ts +52 -0
- package/src/concurrency/ConcurrencyLimiter.ts +72 -0
- package/src/concurrency/mapAsyncIterableConcurrently.ts +66 -0
- package/src/decorators/locallyCached.ts +30 -22
- package/src/decorators/seekable.ts +7 -7
- package/src/registry/ProviderRegistry.ts +241 -0
- package/src/registry/detectProtocol.ts +16 -0
- package/src/registry/negotiateTransfer.ts +121 -0
- package/src/retry/RetryOptions.ts +26 -0
- package/src/{withRetry.ts → retry/withRetry.ts} +2 -12
- package/src/transfer/EntryContext.ts +17 -0
- package/src/transfer/TransferOptions.ts +45 -0
- package/src/transfer/TransferResult.ts +8 -0
- package/src/transfer/aggregateResults.ts +15 -0
- package/src/transfer/copyMove.ts +113 -0
- package/src/transfer/leaseTransfer.ts +93 -0
- package/src/transfer/multipartTransfer.ts +119 -0
- package/src/transfer/negotiatePartSize.ts +40 -0
- package/src/transfer/patternTransfer.ts +84 -0
- package/src/transfer/rangeReadableMultipartReader.ts +30 -0
- package/src/transfer/recursiveTransfer.ts +147 -0
- package/src/transfer/streamTransfer.ts +214 -0
- package/src/transfer/transferEntry.ts +185 -0
- package/src/util/abort.ts +42 -0
- package/src/util/applyPayloadConverter.ts +37 -0
- package/src/util/describePath.ts +12 -0
- package/src/util/globToRegex.ts +32 -0
- package/src/util/itemLength.ts +6 -0
- package/src/util/keys.ts +21 -0
- package/src/util/withParentOperationId.ts +17 -0
- package/dist/src/ConcurrencyLimiter.d.ts.map +0 -1
- package/dist/src/ConcurrencyLimiter.js +0 -161
- package/dist/src/ProviderRegistry.d.ts +0 -14
- package/dist/src/ProviderRegistry.d.ts.map +0 -1
- package/dist/src/ProviderRegistry.js +0 -22
- package/dist/src/chunkLength.d.ts +0 -3
- package/dist/src/chunkLength.d.ts.map +0 -1
- package/dist/src/chunkLength.js +0 -4
- package/dist/src/copyMove.d.ts +0 -46
- package/dist/src/copyMove.d.ts.map +0 -1
- package/dist/src/copyMove.js +0 -280
- package/dist/src/withRetry.d.ts.map +0 -1
- package/src/ConcurrencyLimiter.ts +0 -178
- package/src/ProviderRegistry.ts +0 -32
- package/src/chunkLength.ts +0 -5
- package/src/copyMove.ts +0 -458
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Minimal single-consumer async FIFO queue. */
|
|
2
|
+
export class AsyncChannel<T> implements AsyncIterable<T> {
|
|
3
|
+
#items: T[] = [];
|
|
4
|
+
#waiters: { resolve: (result: IteratorResult<T>) => void; reject: (error: unknown) => void }[] =
|
|
5
|
+
[];
|
|
6
|
+
#closed = false;
|
|
7
|
+
#hasError = false;
|
|
8
|
+
#error: unknown;
|
|
9
|
+
|
|
10
|
+
push(item: T): void {
|
|
11
|
+
const waiter = this.#waiters.shift();
|
|
12
|
+
if (waiter) {
|
|
13
|
+
waiter.resolve({ value: item, done: false });
|
|
14
|
+
} else {
|
|
15
|
+
this.#items.push(item);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
close(): void {
|
|
20
|
+
this.#closed = true;
|
|
21
|
+
for (const waiter of this.#waiters.splice(0)) {
|
|
22
|
+
if (this.#hasError) {
|
|
23
|
+
waiter.reject(this.#error);
|
|
24
|
+
} else {
|
|
25
|
+
waiter.resolve({ value: undefined as never, done: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fail(error: unknown): void {
|
|
31
|
+
this.#hasError = true;
|
|
32
|
+
this.#error = error;
|
|
33
|
+
this.close();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async #next(): Promise<IteratorResult<T>> {
|
|
37
|
+
if (this.#items.length > 0) {
|
|
38
|
+
return { value: this.#items.shift() as T, done: false };
|
|
39
|
+
}
|
|
40
|
+
if (this.#closed) {
|
|
41
|
+
if (this.#hasError) {
|
|
42
|
+
throw this.#error;
|
|
43
|
+
}
|
|
44
|
+
return { value: undefined as never, done: true };
|
|
45
|
+
}
|
|
46
|
+
return new Promise((resolve, reject) => this.#waiters.push({ resolve, reject }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
50
|
+
return { next: () => this.#next() };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const DEFAULT_MAX_CONCURRENCY = 4;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A semaphore bounding how many `run()` callbacks execute at once. A single
|
|
5
|
+
* shared instance (see {@link defaultConcurrencyLimiter}) enforces its cap
|
|
6
|
+
* *globally* across concurrently invoked `copy()`/`move()` calls, not just
|
|
7
|
+
* within one call - two folder copies started at the same time and sharing
|
|
8
|
+
* a limiter will never together exceed its `maxConcurrency`.
|
|
9
|
+
*/
|
|
10
|
+
export class ConcurrencyLimiter {
|
|
11
|
+
#maxConcurrency: number;
|
|
12
|
+
#active = 0;
|
|
13
|
+
#queue: (() => void)[] = [];
|
|
14
|
+
|
|
15
|
+
public constructor(maxConcurrency: number) {
|
|
16
|
+
ConcurrencyLimiter.#validate(maxConcurrency);
|
|
17
|
+
this.#maxConcurrency = maxConcurrency;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public get maxConcurrency(): number {
|
|
21
|
+
return this.#maxConcurrency;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public setMaxConcurrency(maxConcurrency: number): void {
|
|
25
|
+
ConcurrencyLimiter.#validate(maxConcurrency);
|
|
26
|
+
this.#maxConcurrency = maxConcurrency;
|
|
27
|
+
this.#drain();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async run<T>(fn: () => Promise<T>): Promise<T> {
|
|
31
|
+
await this.#acquire();
|
|
32
|
+
try {
|
|
33
|
+
return await fn();
|
|
34
|
+
} finally {
|
|
35
|
+
this.#active -= 1;
|
|
36
|
+
this.#drain();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#acquire(): Promise<void> {
|
|
41
|
+
if (this.#active < this.#maxConcurrency) {
|
|
42
|
+
this.#active += 1;
|
|
43
|
+
return Promise.resolve();
|
|
44
|
+
}
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
this.#queue.push(() => {
|
|
47
|
+
this.#active += 1;
|
|
48
|
+
resolve();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#drain(): void {
|
|
54
|
+
while (this.#active < this.#maxConcurrency && this.#queue.length > 0) {
|
|
55
|
+
this.#queue.shift()?.();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static #validate(maxConcurrency: number): void {
|
|
60
|
+
if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
|
|
61
|
+
throw new Error(`maxConcurrency must be a positive integer, got ${maxConcurrency}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Process-wide default limiter, used by `copy()`/`move()` whenever
|
|
68
|
+
* `TransferOptions.concurrencyLimiter` is omitted - so any two calls in the
|
|
69
|
+
* same process share one cap unless a caller explicitly opts out with its
|
|
70
|
+
* own instance.
|
|
71
|
+
*/
|
|
72
|
+
export const defaultConcurrencyLimiter = new ConcurrencyLimiter(DEFAULT_MAX_CONCURRENCY);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AsyncChannel } from "./AsyncChannel.ts";
|
|
2
|
+
import type { ConcurrencyLimiter } from "./ConcurrencyLimiter.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pulls items from `source` and runs `fn` on each, bounded by `limiter`:
|
|
6
|
+
* exactly `limiter.maxConcurrency` worker loops each pull-then-process one
|
|
7
|
+
* item at a time through `limiter.run`, so at most that many items are ever
|
|
8
|
+
* pulled/in-flight at once (important for sources like a multipart reader
|
|
9
|
+
* where pulling an item eagerly opens a file/network handle). Results are
|
|
10
|
+
* yielded in completion order, not source order. If `fn` throws, no new
|
|
11
|
+
* items are pulled but already-in-flight ones are allowed to finish before
|
|
12
|
+
* the error is rethrown from the returned iterable.
|
|
13
|
+
*/
|
|
14
|
+
export function mapAsyncIterableConcurrently<T, R>(
|
|
15
|
+
source: AsyncIterable<T>,
|
|
16
|
+
fn: (item: T) => Promise<R>,
|
|
17
|
+
limiter: ConcurrencyLimiter,
|
|
18
|
+
): AsyncIterable<R> {
|
|
19
|
+
const channel = new AsyncChannel<R>();
|
|
20
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
21
|
+
let stopped = false;
|
|
22
|
+
let firstError: unknown;
|
|
23
|
+
let errorRecorded = false;
|
|
24
|
+
|
|
25
|
+
function recordError(error: unknown): void {
|
|
26
|
+
stopped = true;
|
|
27
|
+
if (!errorRecorded) {
|
|
28
|
+
errorRecorded = true;
|
|
29
|
+
firstError = error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function worker(): Promise<void> {
|
|
34
|
+
for (;;) {
|
|
35
|
+
if (stopped) return;
|
|
36
|
+
let next: IteratorResult<T>;
|
|
37
|
+
try {
|
|
38
|
+
next = await iterator.next();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
recordError(error);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (next.done) return;
|
|
44
|
+
const value = next.value;
|
|
45
|
+
try {
|
|
46
|
+
const result = await limiter.run(() => fn(value));
|
|
47
|
+
channel.push(result);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
recordError(error);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
void (async () => {
|
|
56
|
+
const workerCount = Math.max(1, limiter.maxConcurrency);
|
|
57
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
58
|
+
if (errorRecorded) {
|
|
59
|
+
channel.fail(firstError);
|
|
60
|
+
} else {
|
|
61
|
+
channel.close();
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
|
|
65
|
+
return channel;
|
|
66
|
+
}
|
|
@@ -1,53 +1,61 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
ItemOfKind,
|
|
3
|
+
PayloadKind,
|
|
4
4
|
StreamHandle,
|
|
5
5
|
} from "@flowscripter/pluggable-io-framework-api";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* source while caching every
|
|
11
|
-
* the cached
|
|
8
|
+
* A `StreamOpenerDecorator`: wraps a `StreamHandle` opener so the
|
|
9
|
+
* underlying source is read at most once - the first call drains the
|
|
10
|
+
* source while caching every item in memory; every subsequent call replays
|
|
11
|
+
* the cached items without touching the source again.
|
|
12
12
|
*
|
|
13
13
|
* A plain `StreamHandle` only exposes a single one-shot `ReadableStream`, so
|
|
14
|
-
* caching can't be a `StreamDecorator
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* what kind of source is behind it.
|
|
14
|
+
* caching can't be a `StreamDecorator` operating on an already-open handle
|
|
15
|
+
* (there would be nothing left to re-read on a second call) - it has to
|
|
16
|
+
* intercept the *open* operation itself.
|
|
18
17
|
*/
|
|
19
|
-
export function locallyCached<K extends
|
|
18
|
+
export function locallyCached<K extends PayloadKind>(
|
|
20
19
|
open: () => Promise<StreamHandle<K>>,
|
|
21
20
|
): () => Promise<StreamHandle<K>> {
|
|
22
|
-
let cache: {
|
|
21
|
+
let cache: { handle: StreamHandle<K>; items: ItemOfKind<K>[] } | undefined;
|
|
23
22
|
|
|
24
|
-
function replay(
|
|
25
|
-
return new ReadableStream<
|
|
23
|
+
function replay(items: ItemOfKind<K>[]): ReadableStream<ItemOfKind<K>> {
|
|
24
|
+
return new ReadableStream<ItemOfKind<K>>({
|
|
26
25
|
start(controller) {
|
|
27
|
-
for (const
|
|
26
|
+
for (const item of items) controller.enqueue(item);
|
|
28
27
|
controller.close();
|
|
29
28
|
},
|
|
30
29
|
});
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
function describe(handle: StreamHandle<K>, stream: ReadableStream<ItemOfKind<K>>) {
|
|
33
|
+
return {
|
|
34
|
+
kind: handle.kind,
|
|
35
|
+
stream,
|
|
36
|
+
bounded: handle.bounded,
|
|
37
|
+
payloadType: handle.payloadType,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
return async () => {
|
|
34
42
|
if (cache) {
|
|
35
|
-
return
|
|
43
|
+
return describe(cache.handle, replay(cache.items));
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
const handle = await open();
|
|
39
|
-
const reader = (handle.stream as ReadableStream<
|
|
40
|
-
const
|
|
47
|
+
const reader = (handle.stream as ReadableStream<ItemOfKind<K>>).getReader();
|
|
48
|
+
const items: ItemOfKind<K>[] = [];
|
|
41
49
|
|
|
42
|
-
const stream = new ReadableStream<
|
|
50
|
+
const stream = new ReadableStream<ItemOfKind<K>>({
|
|
43
51
|
async pull(controller) {
|
|
44
52
|
const { done, value } = await reader.read();
|
|
45
53
|
if (done) {
|
|
46
|
-
cache = {
|
|
54
|
+
cache = { handle, items };
|
|
47
55
|
controller.close();
|
|
48
56
|
return;
|
|
49
57
|
}
|
|
50
|
-
|
|
58
|
+
items.push(value);
|
|
51
59
|
controller.enqueue(value);
|
|
52
60
|
},
|
|
53
61
|
cancel(reason) {
|
|
@@ -55,6 +63,6 @@ export function locallyCached<K extends ChunkKind>(
|
|
|
55
63
|
},
|
|
56
64
|
});
|
|
57
65
|
|
|
58
|
-
return
|
|
66
|
+
return describe(handle, stream);
|
|
59
67
|
};
|
|
60
68
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
PayloadKind,
|
|
3
|
+
ItemOfKind,
|
|
4
4
|
RangeReadable,
|
|
5
5
|
Seekable,
|
|
6
6
|
StreamHandle,
|
|
7
7
|
} from "@flowscripter/pluggable-io-framework-api";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Wraps a handle that already supports
|
|
11
|
-
* byte-range reads) with a
|
|
10
|
+
* Wraps a handle that already supports `RangeReadable` (arbitrary
|
|
11
|
+
* byte-range reads) with a `Seekable` capability: a single logical
|
|
12
12
|
* stream whose read position can be jumped via `seek(offset)`, rather than
|
|
13
13
|
* requiring the caller to open a fresh stream per range.
|
|
14
14
|
*
|
|
@@ -16,12 +16,12 @@ import type {
|
|
|
16
16
|
* in flight - like any seekable stream, seeking and reading are sequential,
|
|
17
17
|
* not concurrent, operations on the same handle.
|
|
18
18
|
*/
|
|
19
|
-
export function seekable<K extends
|
|
19
|
+
export function seekable<K extends PayloadKind>(
|
|
20
20
|
handle: StreamHandle<K> & RangeReadable<K>,
|
|
21
21
|
): StreamHandle<K> & Seekable {
|
|
22
|
-
let reader = (handle.stream as ReadableStream<
|
|
22
|
+
let reader = (handle.stream as ReadableStream<ItemOfKind<K>>).getReader();
|
|
23
23
|
|
|
24
|
-
const stream = new ReadableStream<
|
|
24
|
+
const stream = new ReadableStream<ItemOfKind<K>>(
|
|
25
25
|
{
|
|
26
26
|
async pull(controller) {
|
|
27
27
|
const { done, value } = await reader.read();
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { PluginManager } from "@flowscripter/dynamic-plugin-framework";
|
|
2
|
+
import {
|
|
3
|
+
BYTES_PAYLOAD_TYPE,
|
|
4
|
+
type IOProvider,
|
|
5
|
+
type IOProviderFactory,
|
|
6
|
+
type LocationTarget,
|
|
7
|
+
PayloadKind,
|
|
8
|
+
type PayloadConverter,
|
|
9
|
+
PermanentIOError,
|
|
10
|
+
PLUGGABLE_IO_FRAMEWORK_PAYLOAD_CONVERTER_EXTENSION_POINT,
|
|
11
|
+
PLUGGABLE_IO_FRAMEWORK_PROVIDER_FACTORY_EXTENSION_POINT,
|
|
12
|
+
type ProviderResolver,
|
|
13
|
+
} from "@flowscripter/pluggable-io-framework-api";
|
|
14
|
+
import type { TransferOptions } from "../transfer/TransferOptions.ts";
|
|
15
|
+
import { detectProtocol } from "./detectProtocol.ts";
|
|
16
|
+
import { DEFAULT_NATIVE_DOMAINS, negotiateTransfer } from "./negotiateTransfer.ts";
|
|
17
|
+
|
|
18
|
+
/** Nested provider resolutions allowed before resolution fails. */
|
|
19
|
+
export const MAX_RESOLUTION_DEPTH = 4;
|
|
20
|
+
|
|
21
|
+
export interface ResolvedProvider {
|
|
22
|
+
readonly provider: IOProvider;
|
|
23
|
+
readonly target: LocationTarget;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TransferProviders {
|
|
27
|
+
readonly source: ResolvedProvider;
|
|
28
|
+
readonly dest: ResolvedProvider;
|
|
29
|
+
/**
|
|
30
|
+
* The negotiated `converter`, `path` and `writePayloadTypes`, to pass to
|
|
31
|
+
* `copy`/`move` as is or spread together with caller options.
|
|
32
|
+
*/
|
|
33
|
+
readonly options: TransferOptions;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function locationFieldNames(factory: IOProviderFactory): string[] {
|
|
37
|
+
const shape = (factory.locationSchema as { shape?: Record<string, unknown> }).shape;
|
|
38
|
+
return Object.keys(shape ?? {}).sort();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Discovers provider factories and payload converters through a
|
|
43
|
+
* `dynamic-plugin-framework` `PluginManager`, keyed by protocol and
|
|
44
|
+
* payload kind, and creates providers for location strings. It is also the
|
|
45
|
+
* `ProviderResolver` handed to every provider it creates.
|
|
46
|
+
*/
|
|
47
|
+
export class ProviderRegistry implements ProviderResolver {
|
|
48
|
+
#factories = new Map<string, Map<PayloadKind, IOProviderFactory>>();
|
|
49
|
+
#converters: PayloadConverter[] = [];
|
|
50
|
+
|
|
51
|
+
public constructor(private readonly pluginManager: PluginManager) {}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Instantiates every registered factory and converter once. Fails, naming
|
|
55
|
+
* both factories, if two share a (protocol, kind) pair or if factories for
|
|
56
|
+
* the same protocol have different top-level location fields.
|
|
57
|
+
*/
|
|
58
|
+
public async discover(): Promise<void> {
|
|
59
|
+
const factories = new Map<string, Map<PayloadKind, IOProviderFactory>>();
|
|
60
|
+
const handles = new Map<IOProviderFactory, string>();
|
|
61
|
+
for (const extension of await this.#extensions(
|
|
62
|
+
PLUGGABLE_IO_FRAMEWORK_PROVIDER_FACTORY_EXTENSION_POINT,
|
|
63
|
+
)) {
|
|
64
|
+
const factory = (await this.pluginManager.instantiate(
|
|
65
|
+
extension.extensionHandle,
|
|
66
|
+
)) as IOProviderFactory;
|
|
67
|
+
handles.set(factory, extension.extensionHandle);
|
|
68
|
+
const byKind = factories.get(factory.protocol) ?? new Map<PayloadKind, IOProviderFactory>();
|
|
69
|
+
const clash = byKind.get(factory.kind);
|
|
70
|
+
if (clash) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Factories "${handles.get(clash)}" and "${extension.extensionHandle}" both provide protocol "${factory.protocol}" with payload kind "${factory.kind}"`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
for (const other of byKind.values()) {
|
|
76
|
+
if (locationFieldNames(other).join() !== locationFieldNames(factory).join()) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Factories "${handles.get(other)}" and "${extension.extensionHandle}" for protocol "${factory.protocol}" have different location fields`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
byKind.set(factory.kind, factory);
|
|
83
|
+
factories.set(factory.protocol, byKind);
|
|
84
|
+
}
|
|
85
|
+
const converters: PayloadConverter[] = [];
|
|
86
|
+
for (const extension of await this.#extensions(
|
|
87
|
+
PLUGGABLE_IO_FRAMEWORK_PAYLOAD_CONVERTER_EXTENSION_POINT,
|
|
88
|
+
)) {
|
|
89
|
+
converters.push(
|
|
90
|
+
(await this.pluginManager.instantiate(extension.extensionHandle)) as PayloadConverter,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
this.#factories = factories;
|
|
94
|
+
this.#converters = converters;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public getProtocols(): string[] {
|
|
98
|
+
return [...this.#factories.keys()].sort();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public getKinds(protocol: string): PayloadKind[] {
|
|
102
|
+
return [...(this.#factories.get(protocol)?.keys() ?? [])].sort();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The factory for `protocol` and `kind`; without `kind`, the `js` one, else the only one. */
|
|
106
|
+
public getFactory(protocol: string, kind?: PayloadKind): IOProviderFactory | undefined {
|
|
107
|
+
const byKind = this.#factories.get(protocol);
|
|
108
|
+
if (!byKind) return undefined;
|
|
109
|
+
if (kind !== undefined) return byKind.get(kind);
|
|
110
|
+
return byKind.get(PayloadKind.Js) ?? byKind.values().next().value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public getConverters(): readonly PayloadConverter[] {
|
|
114
|
+
return this.#converters;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public createProviderForLocation(
|
|
118
|
+
location: string,
|
|
119
|
+
options?: { kind?: PayloadKind; domain?: string },
|
|
120
|
+
): Promise<ResolvedProvider> {
|
|
121
|
+
return this.#resolve(location, options, 0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Creates providers for both sides of a transfer, negotiating a common
|
|
126
|
+
* (kind, domain) and payload type, or a converter.
|
|
127
|
+
*/
|
|
128
|
+
public async createProvidersForTransfer(
|
|
129
|
+
source: string,
|
|
130
|
+
dest: string,
|
|
131
|
+
options?: { kind?: PayloadKind },
|
|
132
|
+
): Promise<TransferProviders> {
|
|
133
|
+
const sourceProtocol = detectProtocol(source);
|
|
134
|
+
const destProtocol = detectProtocol(dest);
|
|
135
|
+
const sourceFactories = this.#factoriesFor(sourceProtocol, options?.kind);
|
|
136
|
+
const destFactories = this.#factoriesFor(destProtocol, options?.kind);
|
|
137
|
+
const negotiation = negotiateTransfer(
|
|
138
|
+
sourceProtocol,
|
|
139
|
+
sourceFactories,
|
|
140
|
+
destProtocol,
|
|
141
|
+
destFactories,
|
|
142
|
+
this.#converters,
|
|
143
|
+
options?.kind,
|
|
144
|
+
);
|
|
145
|
+
const sourceResolved = await this.#instantiate(
|
|
146
|
+
negotiation.source.factory,
|
|
147
|
+
source,
|
|
148
|
+
negotiation.source.domain,
|
|
149
|
+
0,
|
|
150
|
+
);
|
|
151
|
+
let destResolved: ResolvedProvider;
|
|
152
|
+
try {
|
|
153
|
+
destResolved = await this.#instantiate(
|
|
154
|
+
negotiation.dest.factory,
|
|
155
|
+
dest,
|
|
156
|
+
negotiation.dest.domain,
|
|
157
|
+
0,
|
|
158
|
+
);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
await sourceResolved.provider[Symbol.asyncDispose]();
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
source: sourceResolved,
|
|
165
|
+
dest: destResolved,
|
|
166
|
+
options: {
|
|
167
|
+
converter: negotiation.converter,
|
|
168
|
+
path: negotiation.path,
|
|
169
|
+
writePayloadTypes: negotiation.dest.factory.writePayloadTypes ?? [BYTES_PAYLOAD_TYPE],
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#extensions(extensionPoint: string) {
|
|
175
|
+
return this.pluginManager
|
|
176
|
+
.registerExtensions(extensionPoint)
|
|
177
|
+
.then(() => this.pluginManager.getRegisteredExtensions(extensionPoint));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#factoriesFor(protocol: string, kind?: PayloadKind): IOProviderFactory[] {
|
|
181
|
+
const byKind = this.#factories.get(protocol);
|
|
182
|
+
if (!byKind) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`No provider for protocol "${protocol}" (available: ${this.getProtocols().join(", ")})`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (kind !== undefined && !byKind.has(kind)) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`No provider for protocol "${protocol}" with payload kind "${kind}" (available: ${this.getKinds(protocol).join(", ")})`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return [...byKind.values()];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async #resolve(
|
|
196
|
+
location: string,
|
|
197
|
+
options: { kind?: PayloadKind; domain?: string } | undefined,
|
|
198
|
+
depth: number,
|
|
199
|
+
): Promise<ResolvedProvider> {
|
|
200
|
+
if (depth > MAX_RESOLUTION_DEPTH) {
|
|
201
|
+
throw new PermanentIOError("provider resolution too deep");
|
|
202
|
+
}
|
|
203
|
+
const protocol = detectProtocol(location);
|
|
204
|
+
this.#factoriesFor(protocol, options?.kind);
|
|
205
|
+
const factory = this.getFactory(protocol, options?.kind) as IOProviderFactory;
|
|
206
|
+
let domain: string | undefined;
|
|
207
|
+
if (factory.kind === PayloadKind.Native) {
|
|
208
|
+
const domains = factory.domains ?? DEFAULT_NATIVE_DOMAINS;
|
|
209
|
+
domain = options?.domain ?? domains[0];
|
|
210
|
+
if (domain === undefined || !domains.includes(domain)) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`No provider for protocol "${protocol}" supports domain "${domain}" (available: ${domains.join(", ")})`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return this.#instantiate(factory, location, domain, depth);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async #instantiate(
|
|
220
|
+
factory: IOProviderFactory,
|
|
221
|
+
location: string,
|
|
222
|
+
domain: string | undefined,
|
|
223
|
+
depth: number,
|
|
224
|
+
): Promise<ResolvedProvider> {
|
|
225
|
+
const parsed = factory.locationSchema.parse(factory.parseLocationString(location));
|
|
226
|
+
const { config, target } = factory.toProviderInputs(parsed);
|
|
227
|
+
const resolver: ProviderResolver = {
|
|
228
|
+
createProviderForLocation: (inner, opts) =>
|
|
229
|
+
this.#resolve(
|
|
230
|
+
inner,
|
|
231
|
+
{ kind: opts?.kind ?? factory.kind, domain: opts?.domain ?? domain },
|
|
232
|
+
depth + 1,
|
|
233
|
+
),
|
|
234
|
+
};
|
|
235
|
+
const provider = await factory.createProvider(factory.configSchema.parse(config), {
|
|
236
|
+
domain,
|
|
237
|
+
resolver,
|
|
238
|
+
});
|
|
239
|
+
return { provider, target };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const SCHEME = /^([a-z][a-z0-9+.-]+):/i;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the protocol of a location string: its URI scheme, lower-cased,
|
|
5
|
+
* or `"file"` when there is none. A scheme must be at least two characters,
|
|
6
|
+
* so a Windows drive letter (`C:\foo`) is not read as one. For a composite
|
|
7
|
+
* scheme the protocol is the part before the first `+`.
|
|
8
|
+
*/
|
|
9
|
+
export function detectProtocol(input: string): string {
|
|
10
|
+
const match = SCHEME.exec(input);
|
|
11
|
+
if (!match) {
|
|
12
|
+
return "file";
|
|
13
|
+
}
|
|
14
|
+
const scheme = (match[1] as string).toLowerCase();
|
|
15
|
+
return scheme.split("+")[0] as string;
|
|
16
|
+
}
|