@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.
Files changed (142) hide show
  1. package/README.md +47 -178
  2. package/dist/index.d.ts +11 -4
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +11 -4
  5. package/dist/src/concurrency/AsyncChannel.d.ts +9 -0
  6. package/dist/src/concurrency/AsyncChannel.d.ts.map +1 -0
  7. package/dist/src/concurrency/AsyncChannel.js +48 -0
  8. package/dist/src/{ConcurrencyLimiter.d.ts → concurrency/ConcurrencyLimiter.d.ts} +1 -12
  9. package/dist/src/concurrency/ConcurrencyLimiter.d.ts.map +1 -0
  10. package/dist/src/concurrency/ConcurrencyLimiter.js +64 -0
  11. package/dist/src/concurrency/mapAsyncIterableConcurrently.d.ts +13 -0
  12. package/dist/src/concurrency/mapAsyncIterableConcurrently.d.ts.map +1 -0
  13. package/dist/src/concurrency/mapAsyncIterableConcurrently.js +61 -0
  14. package/dist/src/decorators/locallyCached.d.ts +9 -10
  15. package/dist/src/decorators/locallyCached.d.ts.map +1 -1
  16. package/dist/src/decorators/locallyCached.js +23 -16
  17. package/dist/src/decorators/seekable.d.ts +4 -4
  18. package/dist/src/decorators/seekable.d.ts.map +1 -1
  19. package/dist/src/decorators/seekable.js +2 -2
  20. package/dist/src/registry/ProviderRegistry.d.ts +52 -0
  21. package/dist/src/registry/ProviderRegistry.d.ts.map +1 -0
  22. package/dist/src/registry/ProviderRegistry.js +148 -0
  23. package/dist/src/registry/detectProtocol.d.ts +8 -0
  24. package/dist/src/registry/detectProtocol.d.ts.map +1 -0
  25. package/dist/src/registry/detectProtocol.js +15 -0
  26. package/dist/src/registry/negotiateTransfer.d.ts +29 -0
  27. package/dist/src/registry/negotiateTransfer.d.ts.map +1 -0
  28. package/dist/src/registry/negotiateTransfer.js +69 -0
  29. package/dist/src/retry/RetryOptions.d.ts +19 -0
  30. package/dist/src/retry/RetryOptions.d.ts.map +1 -0
  31. package/dist/src/retry/RetryOptions.js +9 -0
  32. package/dist/src/{withRetry.d.ts → retry/withRetry.d.ts} +1 -5
  33. package/dist/src/retry/withRetry.d.ts.map +1 -0
  34. package/dist/src/{withRetry.js → retry/withRetry.js} +2 -5
  35. package/dist/src/transfer/EntryContext.d.ts +16 -0
  36. package/dist/src/transfer/EntryContext.d.ts.map +1 -0
  37. package/dist/src/transfer/EntryContext.js +1 -0
  38. package/dist/src/transfer/TransferOptions.d.ts +45 -0
  39. package/dist/src/transfer/TransferOptions.d.ts.map +1 -0
  40. package/dist/src/transfer/TransferOptions.js +1 -0
  41. package/dist/src/transfer/TransferResult.d.ts +9 -0
  42. package/dist/src/transfer/TransferResult.d.ts.map +1 -0
  43. package/dist/src/transfer/TransferResult.js +1 -0
  44. package/dist/src/transfer/aggregateResults.d.ts +4 -0
  45. package/dist/src/transfer/aggregateResults.d.ts.map +1 -0
  46. package/dist/src/transfer/aggregateResults.js +9 -0
  47. package/dist/src/transfer/copyMove.d.ts +23 -0
  48. package/dist/src/transfer/copyMove.d.ts.map +1 -0
  49. package/dist/src/transfer/copyMove.js +68 -0
  50. package/dist/src/transfer/leaseTransfer.d.ts +14 -0
  51. package/dist/src/transfer/leaseTransfer.d.ts.map +1 -0
  52. package/dist/src/transfer/leaseTransfer.js +74 -0
  53. package/dist/src/transfer/multipartTransfer.d.ts +23 -0
  54. package/dist/src/transfer/multipartTransfer.d.ts.map +1 -0
  55. package/dist/src/transfer/multipartTransfer.js +94 -0
  56. package/dist/src/transfer/negotiatePartSize.d.ts +10 -0
  57. package/dist/src/transfer/negotiatePartSize.d.ts.map +1 -0
  58. package/dist/src/transfer/negotiatePartSize.js +27 -0
  59. package/dist/src/transfer/patternTransfer.d.ts +12 -0
  60. package/dist/src/transfer/patternTransfer.d.ts.map +1 -0
  61. package/dist/src/transfer/patternTransfer.js +61 -0
  62. package/dist/src/transfer/rangeReadableMultipartReader.d.ts +8 -0
  63. package/dist/src/transfer/rangeReadableMultipartReader.d.ts.map +1 -0
  64. package/dist/src/transfer/rangeReadableMultipartReader.js +19 -0
  65. package/dist/src/transfer/recursiveTransfer.d.ts +13 -0
  66. package/dist/src/transfer/recursiveTransfer.d.ts.map +1 -0
  67. package/dist/src/transfer/recursiveTransfer.js +112 -0
  68. package/dist/src/transfer/streamTransfer.d.ts +32 -0
  69. package/dist/src/transfer/streamTransfer.d.ts.map +1 -0
  70. package/dist/src/transfer/streamTransfer.js +186 -0
  71. package/dist/src/transfer/transferEntry.d.ts +25 -0
  72. package/dist/src/transfer/transferEntry.d.ts.map +1 -0
  73. package/dist/src/transfer/transferEntry.js +136 -0
  74. package/dist/src/util/abort.d.ts +12 -0
  75. package/dist/src/util/abort.d.ts.map +1 -0
  76. package/dist/src/util/abort.js +39 -0
  77. package/dist/src/util/applyPayloadConverter.d.ts +9 -0
  78. package/dist/src/util/applyPayloadConverter.d.ts.map +1 -0
  79. package/dist/src/util/applyPayloadConverter.js +26 -0
  80. package/dist/src/util/describePath.d.ts +6 -0
  81. package/dist/src/util/describePath.d.ts.map +1 -0
  82. package/dist/src/util/describePath.js +9 -0
  83. package/dist/src/util/globToRegex.d.ts +8 -0
  84. package/dist/src/util/globToRegex.d.ts.map +1 -0
  85. package/dist/src/util/globToRegex.js +35 -0
  86. package/dist/src/util/itemLength.d.ts +4 -0
  87. package/dist/src/util/itemLength.d.ts.map +1 -0
  88. package/dist/src/util/itemLength.js +5 -0
  89. package/dist/src/util/keys.d.ts +8 -0
  90. package/dist/src/util/keys.d.ts.map +1 -0
  91. package/dist/src/util/keys.js +19 -0
  92. package/dist/src/util/withParentOperationId.d.ts +4 -0
  93. package/dist/src/util/withParentOperationId.d.ts.map +1 -0
  94. package/dist/src/util/withParentOperationId.js +12 -0
  95. package/index.ts +11 -4
  96. package/package.json +4 -3
  97. package/src/concurrency/AsyncChannel.ts +52 -0
  98. package/src/concurrency/ConcurrencyLimiter.ts +72 -0
  99. package/src/concurrency/mapAsyncIterableConcurrently.ts +66 -0
  100. package/src/decorators/locallyCached.ts +30 -22
  101. package/src/decorators/seekable.ts +7 -7
  102. package/src/registry/ProviderRegistry.ts +241 -0
  103. package/src/registry/detectProtocol.ts +16 -0
  104. package/src/registry/negotiateTransfer.ts +121 -0
  105. package/src/retry/RetryOptions.ts +26 -0
  106. package/src/{withRetry.ts → retry/withRetry.ts} +2 -12
  107. package/src/transfer/EntryContext.ts +17 -0
  108. package/src/transfer/TransferOptions.ts +45 -0
  109. package/src/transfer/TransferResult.ts +8 -0
  110. package/src/transfer/aggregateResults.ts +15 -0
  111. package/src/transfer/copyMove.ts +113 -0
  112. package/src/transfer/leaseTransfer.ts +93 -0
  113. package/src/transfer/multipartTransfer.ts +119 -0
  114. package/src/transfer/negotiatePartSize.ts +40 -0
  115. package/src/transfer/patternTransfer.ts +84 -0
  116. package/src/transfer/rangeReadableMultipartReader.ts +30 -0
  117. package/src/transfer/recursiveTransfer.ts +147 -0
  118. package/src/transfer/streamTransfer.ts +214 -0
  119. package/src/transfer/transferEntry.ts +185 -0
  120. package/src/util/abort.ts +42 -0
  121. package/src/util/applyPayloadConverter.ts +37 -0
  122. package/src/util/describePath.ts +12 -0
  123. package/src/util/globToRegex.ts +32 -0
  124. package/src/util/itemLength.ts +6 -0
  125. package/src/util/keys.ts +21 -0
  126. package/src/util/withParentOperationId.ts +17 -0
  127. package/dist/src/ConcurrencyLimiter.d.ts.map +0 -1
  128. package/dist/src/ConcurrencyLimiter.js +0 -161
  129. package/dist/src/ProviderRegistry.d.ts +0 -14
  130. package/dist/src/ProviderRegistry.d.ts.map +0 -1
  131. package/dist/src/ProviderRegistry.js +0 -22
  132. package/dist/src/chunkLength.d.ts +0 -3
  133. package/dist/src/chunkLength.d.ts.map +0 -1
  134. package/dist/src/chunkLength.js +0 -4
  135. package/dist/src/copyMove.d.ts +0 -46
  136. package/dist/src/copyMove.d.ts.map +0 -1
  137. package/dist/src/copyMove.js +0 -280
  138. package/dist/src/withRetry.d.ts.map +0 -1
  139. package/src/ConcurrencyLimiter.ts +0 -178
  140. package/src/ProviderRegistry.ts +0 -32
  141. package/src/chunkLength.ts +0 -5
  142. 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
- ChunkKind,
3
- ChunkOfKind,
2
+ ItemOfKind,
3
+ PayloadKind,
4
4
  StreamHandle,
5
5
  } from "@flowscripter/pluggable-io-framework-api";
6
6
 
7
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.
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<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.
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 ChunkKind>(
18
+ export function locallyCached<K extends PayloadKind>(
20
19
  open: () => Promise<StreamHandle<K>>,
21
20
  ): () => Promise<StreamHandle<K>> {
22
- let cache: { kind: K; chunks: ChunkOfKind<K>[] } | undefined;
21
+ let cache: { handle: StreamHandle<K>; items: ItemOfKind<K>[] } | undefined;
23
22
 
24
- function replay(chunks: ChunkOfKind<K>[]): ReadableStream<ChunkOfKind<K>> {
25
- return new ReadableStream<ChunkOfKind<K>>({
23
+ function replay(items: ItemOfKind<K>[]): ReadableStream<ItemOfKind<K>> {
24
+ return new ReadableStream<ItemOfKind<K>>({
26
25
  start(controller) {
27
- for (const chunk of chunks) controller.enqueue(chunk);
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 { kind: cache.kind, stream: replay(cache.chunks) };
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<ChunkOfKind<K>>).getReader();
40
- const chunks: ChunkOfKind<K>[] = [];
47
+ const reader = (handle.stream as ReadableStream<ItemOfKind<K>>).getReader();
48
+ const items: ItemOfKind<K>[] = [];
41
49
 
42
- const stream = new ReadableStream<ChunkOfKind<K>>({
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 = { kind: handle.kind, chunks };
54
+ cache = { handle, items };
47
55
  controller.close();
48
56
  return;
49
57
  }
50
- chunks.push(value);
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 { kind: handle.kind, stream };
66
+ return describe(handle, stream);
59
67
  };
60
68
  }
@@ -1,14 +1,14 @@
1
1
  import type {
2
- ChunkKind,
3
- ChunkOfKind,
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 {@link RangeReadable} (arbitrary
11
- * byte-range reads) with a {@link Seekable} capability: a single logical
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 ChunkKind>(
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<ChunkOfKind<K>>).getReader();
22
+ let reader = (handle.stream as ReadableStream<ItemOfKind<K>>).getReader();
23
23
 
24
- const stream = new ReadableStream<ChunkOfKind<K>>(
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
+ }