@flowscripter/pluggable-io-framework 2.0.0 → 2.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
@@ -10,9 +10,12 @@
10
10
 
11
11
  ## Key Features
12
12
 
13
- - Protocol-aware locations: `file:///data/a.txt`, `s3://bucket/key`,
14
- `https://host/a` and composite schemes such as `tams+https:` select the
15
- installed provider plugin for that protocol. Bare paths default to `file`.
13
+ - Protocol-aware locations, given as a string (`file:///data/a.txt`,
14
+ `s3://bucket/key`, `https://host/a`, composite schemes such as
15
+ `tams+https:`; bare paths default to `file`) or as a structured
16
+ `{ protocol, location }` object. Either form selects the installed provider
17
+ plugin for that protocol. See
18
+ [String and Structured Locations](README/key-concepts.md#string-and-structured-locations).
16
19
  - A `ProviderRegistry` keyed by protocol and payload kind, with more than one
17
20
  implementation of a protocol installed side by side (e.g. JS and native
18
21
  `file` providers).
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export * from "./src/decorators/locallyCached.ts";
4
4
  export * from "./src/decorators/seekable.ts";
5
5
  export * from "./src/registry/detectProtocol.ts";
6
6
  export * from "./src/registry/ProviderRegistry.ts";
7
+ export * from "./src/registry/StructuredLocation.ts";
7
8
  export * from "./src/retry/RetryOptions.ts";
8
9
  export * from "./src/retry/withRetry.ts";
9
10
  export * from "./src/transfer/copyMove.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,yCAAyC,CAAC;AACxD,cAAc,mDAAmD,CAAC;AAClE,cAAc,mCAAmC,CAAC;AAClD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,oCAAoC,CAAC;AACnD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gDAAgD,CAAC;AAC/D,cAAc,mCAAmC,CAAC;AAClD,cAAc,kCAAkC,CAAC;AACjD,cAAc,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,yCAAyC,CAAC;AACxD,cAAc,mDAAmD,CAAC;AAClE,cAAc,mCAAmC,CAAC;AAClD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,oCAAoC,CAAC;AACnD,cAAc,sCAAsC,CAAC;AACrD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gDAAgD,CAAC;AAC/D,cAAc,mCAAmC,CAAC;AAClD,cAAc,kCAAkC,CAAC;AACjD,cAAc,2BAA2B,CAAC"}
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export * from "./src/decorators/locallyCached.js";
4
4
  export * from "./src/decorators/seekable.js";
5
5
  export * from "./src/registry/detectProtocol.js";
6
6
  export * from "./src/registry/ProviderRegistry.js";
7
+ export * from "./src/registry/StructuredLocation.js";
7
8
  export * from "./src/retry/RetryOptions.js";
8
9
  export * from "./src/retry/withRetry.js";
9
10
  export * from "./src/transfer/copyMove.js";
@@ -1,6 +1,7 @@
1
1
  import type { PluginManager } from "@flowscripter/dynamic-plugin-framework";
2
2
  import { type IOProvider, type IOProviderFactory, type LocationTarget, PayloadKind, type PayloadConverter, type ProviderResolver } from "@flowscripter/pluggable-io-framework-api";
3
3
  import type { TransferOptions } from "../transfer/TransferOptions.ts";
4
+ import type { StructuredLocation } from "./StructuredLocation.ts";
4
5
  /** Nested provider resolutions allowed before resolution fails. */
5
6
  export declare const MAX_RESOLUTION_DEPTH = 4;
6
7
  export interface ResolvedProvider {
@@ -19,7 +20,8 @@ export interface TransferProviders {
19
20
  /**
20
21
  * Discovers provider factories and payload converters through a
21
22
  * `dynamic-plugin-framework` `PluginManager`, keyed by protocol and
22
- * payload kind, and creates providers for location strings. It is also the
23
+ * payload kind, and creates providers for location strings or
24
+ * {@link StructuredLocation}s. It is also the
23
25
  * `ProviderResolver` handed to every provider it creates.
24
26
  */
25
27
  export declare class ProviderRegistry implements ProviderResolver {
@@ -37,7 +39,7 @@ export declare class ProviderRegistry implements ProviderResolver {
37
39
  /** The factory for `protocol` and `kind`; without `kind`, the `js` one, else the only one. */
38
40
  getFactory(protocol: string, kind?: PayloadKind): IOProviderFactory | undefined;
39
41
  getConverters(): readonly PayloadConverter[];
40
- createProviderForLocation(location: string, options?: {
42
+ createProviderForLocation(location: string | StructuredLocation, options?: {
41
43
  kind?: PayloadKind;
42
44
  domain?: string;
43
45
  }): Promise<ResolvedProvider>;
@@ -45,7 +47,7 @@ export declare class ProviderRegistry implements ProviderResolver {
45
47
  * Creates providers for both sides of a transfer, negotiating a common
46
48
  * (kind, domain) and payload type, or a converter.
47
49
  */
48
- createProvidersForTransfer(source: string, dest: string, options?: {
50
+ createProvidersForTransfer(source: string | StructuredLocation, dest: string | StructuredLocation, options?: {
49
51
  kind?: PayloadKind;
50
52
  }): Promise<TransferProviders>;
51
53
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ProviderRegistry.d.ts","sourceRoot":"","sources":["../../../src/registry/ProviderRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAC;AAC5E,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,WAAW,EACX,KAAK,gBAAgB,EAIrB,KAAK,gBAAgB,EACtB,MAAM,0CAA0C,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAItE,mEAAmE;AACnE,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;CACnC;AAOD;;;;;GAKG;AACH,qBAAa,gBAAiB,YAAW,gBAAgB;;IAIpC,OAAO,CAAC,QAAQ,CAAC,aAAa;IAAjD,YAAoC,aAAa,EAAE,aAAa,EAAI;IAEpE;;;;OAIG;IACU,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAqCrC;IAEM,YAAY,IAAI,MAAM,EAAE,CAE9B;IAEM,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,EAAE,CAE/C;IAED,8FAA8F;IACvF,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,iBAAiB,GAAG,SAAS,CAKrF;IAEM,aAAa,IAAI,SAAS,gBAAgB,EAAE,CAElD;IAEM,yBAAyB,CAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,WAAW,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAChD,OAAO,CAAC,gBAAgB,CAAC,CAE3B;IAED;;;OAGG;IACU,0BAA0B,CACrC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,WAAW,CAAA;KAAE,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAwC5B;CAqEF"}
1
+ {"version":3,"file":"ProviderRegistry.d.ts","sourceRoot":"","sources":["../../../src/registry/ProviderRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAC;AAC5E,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,WAAW,EACX,KAAK,gBAAgB,EAIrB,KAAK,gBAAgB,EACtB,MAAM,0CAA0C,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEtE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAGlE,mEAAmE;AACnE,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;CACnC;AAWD;;;;;;GAMG;AACH,qBAAa,gBAAiB,YAAW,gBAAgB;;IAIpC,OAAO,CAAC,QAAQ,CAAC,aAAa;IAAjD,YAAoC,aAAa,EAAE,aAAa,EAAI;IAEpE;;;;OAIG;IACU,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAqCrC;IAEM,YAAY,IAAI,MAAM,EAAE,CAE9B;IAEM,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,EAAE,CAE/C;IAED,8FAA8F;IACvF,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,iBAAiB,GAAG,SAAS,CAKrF;IAEM,aAAa,IAAI,SAAS,gBAAgB,EAAE,CAElD;IAEM,yBAAyB,CAC9B,QAAQ,EAAE,MAAM,GAAG,kBAAkB,EACrC,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,WAAW,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAChD,OAAO,CAAC,gBAAgB,CAAC,CAE3B;IAED;;;OAGG;IACU,0BAA0B,CACrC,MAAM,EAAE,MAAM,GAAG,kBAAkB,EACnC,IAAI,EAAE,MAAM,GAAG,kBAAkB,EACjC,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,WAAW,CAAA;KAAE,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAwC5B;CAuEF"}
@@ -3,6 +3,9 @@ import { detectProtocol } from "./detectProtocol.js";
3
3
  import { DEFAULT_NATIVE_DOMAINS, negotiateTransfer } from "./negotiateTransfer.js";
4
4
  /** Nested provider resolutions allowed before resolution fails. */
5
5
  export const MAX_RESOLUTION_DEPTH = 4;
6
+ function protocolOf(location) {
7
+ return typeof location === "string" ? detectProtocol(location) : location.protocol;
8
+ }
6
9
  function locationFieldNames(factory) {
7
10
  const shape = factory.locationSchema.shape;
8
11
  return Object.keys(shape ?? {}).sort();
@@ -10,7 +13,8 @@ function locationFieldNames(factory) {
10
13
  /**
11
14
  * Discovers provider factories and payload converters through a
12
15
  * `dynamic-plugin-framework` `PluginManager`, keyed by protocol and
13
- * payload kind, and creates providers for location strings. It is also the
16
+ * payload kind, and creates providers for location strings or
17
+ * {@link StructuredLocation}s. It is also the
14
18
  * `ProviderResolver` handed to every provider it creates.
15
19
  */
16
20
  export class ProviderRegistry {
@@ -77,8 +81,8 @@ export class ProviderRegistry {
77
81
  * (kind, domain) and payload type, or a converter.
78
82
  */
79
83
  async createProvidersForTransfer(source, dest, options) {
80
- const sourceProtocol = detectProtocol(source);
81
- const destProtocol = detectProtocol(dest);
84
+ const sourceProtocol = protocolOf(source);
85
+ const destProtocol = protocolOf(dest);
82
86
  const sourceFactories = this.#factoriesFor(sourceProtocol, options?.kind);
83
87
  const destFactories = this.#factoriesFor(destProtocol, options?.kind);
84
88
  const negotiation = negotiateTransfer(sourceProtocol, sourceFactories, destProtocol, destFactories, this.#converters, options?.kind);
@@ -120,7 +124,7 @@ export class ProviderRegistry {
120
124
  if (depth > MAX_RESOLUTION_DEPTH) {
121
125
  throw new PermanentIOError("provider resolution too deep");
122
126
  }
123
- const protocol = detectProtocol(location);
127
+ const protocol = protocolOf(location);
124
128
  this.#factoriesFor(protocol, options?.kind);
125
129
  const factory = this.getFactory(protocol, options?.kind);
126
130
  let domain;
@@ -134,7 +138,8 @@ export class ProviderRegistry {
134
138
  return this.#instantiate(factory, location, domain, depth);
135
139
  }
136
140
  async #instantiate(factory, location, domain, depth) {
137
- const parsed = factory.locationSchema.parse(factory.parseLocationString(location));
141
+ const raw = typeof location === "string" ? factory.parseLocationString(location) : location.location;
142
+ const parsed = factory.locationSchema.parse(raw);
138
143
  const { config, target } = factory.toProviderInputs(parsed);
139
144
  const resolver = {
140
145
  createProviderForLocation: (inner, opts) => this.#resolve(inner, { kind: opts?.kind ?? factory.kind, domain: opts?.domain ?? domain }, depth + 1),
@@ -0,0 +1,11 @@
1
+ /**
2
+ * A location given as its protocol plus the raw location object that the
3
+ * protocol's factory `locationSchema` validates, instead of a location
4
+ * string. It can carry fields a string cannot, such as `filename`,
5
+ * `pattern` or credentials.
6
+ */
7
+ export interface StructuredLocation {
8
+ readonly protocol: string;
9
+ readonly location: unknown;
10
+ }
11
+ //# sourceMappingURL=StructuredLocation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StructuredLocation.d.ts","sourceRoot":"","sources":["../../../src/registry/StructuredLocation.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B"}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,14 +1,14 @@
1
- import { type IOProvider, type Part, type RangeReadable, type StreamHandle } from "@flowscripter/pluggable-io-framework-api";
1
+ import { type IOProvider, type MultipartWriter, type RangeReadable, type StreamHandle } from "@flowscripter/pluggable-io-framework-api";
2
2
  import type { ConcurrencyLimiter } from "../concurrency/ConcurrencyLimiter.ts";
3
3
  import type { EntryContext, EntryOutcome } from "./EntryContext.ts";
4
4
  export interface MultipartTransferInput {
5
5
  readonly readable: StreamHandle & RangeReadable;
6
6
  readonly sink: IOProvider;
7
- readonly writer: {
8
- write(parts: AsyncIterable<Part>): Promise<void>;
9
- };
7
+ readonly writer: MultipartWriter;
10
8
  readonly totalBytes: number;
11
9
  readonly partSize: number;
10
+ /** Where a resumed upload continues: parts start at the part containing this offset. */
11
+ readonly startOffset?: number;
12
12
  readonly limiter: ConcurrencyLimiter;
13
13
  readonly context: EntryContext;
14
14
  }
@@ -17,7 +17,8 @@ export interface MultipartTransferInput {
17
17
  * writer, bounded by `limiter`. A part whose read fails with a
18
18
  * `TransientIOError` is re-read on its own, up to `retry.maxRetries` times,
19
19
  * within the same upload. `stop` ends the transfer after the parts already
20
- * started; `signal` aborts it.
20
+ * started; `signal` aborts it. Reported bytes include the parts before
21
+ * `startOffset`.
21
22
  */
22
23
  export declare function multipartTransfer(input: MultipartTransferInput): Promise<EntryOutcome>;
23
24
  //# sourceMappingURL=multipartTransfer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"multipartTransfer.d.ts","sourceRoot":"","sources":["../../../src/transfer/multipartTransfer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAEf,KAAK,IAAI,EACT,KAAK,aAAa,EAClB,KAAK,YAAY,EAElB,MAAM,0CAA0C,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAM/E,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGpE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,YAAY,GAAG,aAAa,CAAC;IAChD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;IACtE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;CAChC;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,YAAY,CAAC,CAoF5F"}
1
+ {"version":3,"file":"multipartTransfer.d.ts","sourceRoot":"","sources":["../../../src/transfer/multipartTransfer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAEf,KAAK,eAAe,EAEpB,KAAK,aAAa,EAClB,KAAK,YAAY,EAElB,MAAM,0CAA0C,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAM/E,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGpE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,YAAY,GAAG,aAAa,CAAC;IAChD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,wFAAwF;IACxF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,YAAY,CAAC,CA0F5F"}
@@ -10,13 +10,15 @@ import { rangeReadableMultipartReader } from "./rangeReadableMultipartReader.js"
10
10
  * writer, bounded by `limiter`. A part whose read fails with a
11
11
  * `TransientIOError` is re-read on its own, up to `retry.maxRetries` times,
12
12
  * within the same upload. `stop` ends the transfer after the parts already
13
- * started; `signal` aborts it.
13
+ * started; `signal` aborts it. Reported bytes include the parts before
14
+ * `startOffset`.
14
15
  */
15
16
  export async function multipartTransfer(input) {
16
17
  const { readable, sink, writer, totalBytes, partSize, limiter, context } = input;
17
18
  const { options, hooks, operationId, type } = context;
18
19
  const retry = options.retry ?? DEFAULT_RETRY;
19
- let bytes = 0;
20
+ const firstOffset = Math.floor((input.startOffset ?? 0) / partSize) * partSize;
21
+ let bytes = firstOffset;
20
22
  let items = 0;
21
23
  let stopped = false;
22
24
  async function readPart(sourcePart) {
@@ -78,7 +80,7 @@ export async function multipartTransfer(input) {
78
80
  };
79
81
  }
80
82
  async function* sourceParts() {
81
- for await (const part of rangeReadableMultipartReader(readable, totalBytes, partSize)) {
83
+ for await (const part of rangeReadableMultipartReader(readable, totalBytes, partSize, firstOffset)) {
82
84
  if (options.signal?.aborted)
83
85
  throw createAbortError();
84
86
  if (options.stop?.aborted) {
@@ -2,7 +2,8 @@ import type { Part, PayloadKind, RangeReadable, StreamHandle } from "@flowscript
2
2
  /**
3
3
  * Splits a `RangeReadable` handle into parts of `partSize` bytes covering
4
4
  * `totalSize`. Each part's stream is opened with `readRange(start, end)`
5
- * (`end` exclusive) when the part is pulled.
5
+ * (`end` exclusive) when the part is pulled. With `startOffset`, parts begin
6
+ * at the part containing that offset.
6
7
  */
7
- export declare function rangeReadableMultipartReader<K extends PayloadKind>(handle: StreamHandle<K> & RangeReadable<K>, totalSize: number, partSize: number): AsyncGenerator<Part<K>>;
8
+ export declare function rangeReadableMultipartReader<K extends PayloadKind>(handle: StreamHandle<K> & RangeReadable<K>, totalSize: number, partSize: number, startOffset?: number): AsyncGenerator<Part<K>>;
8
9
  //# sourceMappingURL=rangeReadableMultipartReader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rangeReadableMultipartReader.d.ts","sourceRoot":"","sources":["../../../src/transfer/rangeReadableMultipartReader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,WAAW,EACX,aAAa,EACb,YAAY,EACb,MAAM,0CAA0C,CAAC;AAElD;;;;GAIG;AACH,wBAAuB,4BAA4B,CAAC,CAAC,SAAS,WAAW,EACvE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,EAC1C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAazB"}
1
+ {"version":3,"file":"rangeReadableMultipartReader.d.ts","sourceRoot":"","sources":["../../../src/transfer/rangeReadableMultipartReader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,WAAW,EACX,aAAa,EACb,YAAY,EACb,MAAM,0CAA0C,CAAC;AAElD;;;;;GAKG;AACH,wBAAuB,4BAA4B,CAAC,CAAC,SAAS,WAAW,EACvE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,EAC1C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,WAAW,SAAI,GACd,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAazB"}
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Splits a `RangeReadable` handle into parts of `partSize` bytes covering
3
3
  * `totalSize`. Each part's stream is opened with `readRange(start, end)`
4
- * (`end` exclusive) when the part is pulled.
4
+ * (`end` exclusive) when the part is pulled. With `startOffset`, parts begin
5
+ * at the part containing that offset.
5
6
  */
6
- export async function* rangeReadableMultipartReader(handle, totalSize, partSize) {
7
+ export async function* rangeReadableMultipartReader(handle, totalSize, partSize, startOffset = 0) {
7
8
  const partCount = Math.max(1, Math.ceil(totalSize / partSize));
8
- for (let index = 0; index < partCount; index += 1) {
9
+ for (let index = Math.floor(startOffset / partSize); index < partCount; index += 1) {
9
10
  const start = index * partSize;
10
11
  const end = Math.min(start + partSize, totalSize);
11
12
  yield {
@@ -1 +1 @@
1
- {"version":3,"file":"transferEntry.d.ts","sourceRoot":"","sources":["../../../src/transfer/transferEntry.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,UAAU,EAOhB,MAAM,0CAA0C,CAAC;AAYlD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAI1D,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,qEAAqE;IACrE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC;AAED,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,eAAe,GAAG,MAAM,CAM/F;AAgBD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAmHtF"}
1
+ {"version":3,"file":"transferEntry.d.ts","sourceRoot":"","sources":["../../../src/transfer/transferEntry.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,UAAU,EAShB,MAAM,0CAA0C,CAAC;AAYlD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAI1D,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,qEAAqE;IACrE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC;AAED,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,eAAe,GAAG,MAAM,CAM/F;AAgBD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAwHtF"}
@@ -1,4 +1,4 @@
1
- import { BYTES_PAYLOAD_TYPE, isBufferProvider, isFillReadable, isRangeReadable, PermanentIOError, } from "@flowscripter/pluggable-io-framework-api";
1
+ import { BYTES_PAYLOAD_TYPE, isBufferProvider, isFillReadable, isRangeReadable, isResumableWritable, PermanentIOError, } from "@flowscripter/pluggable-io-framework-api";
2
2
  import { defaultConcurrencyLimiter } from "../concurrency/ConcurrencyLimiter.js";
3
3
  import { DEFAULT_RETRY } from "../retry/RetryOptions.js";
4
4
  import { withRetry } from "../retry/withRetry.js";
@@ -83,15 +83,24 @@ export async function transferEntry(input) {
83
83
  isRangeReadable(readable)) {
84
84
  await cancelStream(readable);
85
85
  const rangeReadable = readable;
86
- outcome = await withRetry(() => multipartTransfer({
87
- readable: rangeReadable,
88
- sink,
89
- writer: multipartWriter(destKey, partSize),
90
- totalBytes: size,
91
- partSize,
92
- limiter: options.concurrencyLimiter ?? defaultConcurrencyLimiter,
93
- context,
94
- }), retry);
86
+ let writer;
87
+ outcome = await withRetry(() => {
88
+ // A retry continues the failed writer's upload when it has a resume token.
89
+ const token = writer && isResumableWritable(writer) ? writer.resumeToken() : undefined;
90
+ writer = token
91
+ ? multipartWriter(destKey, partSize, { resume: token })
92
+ : multipartWriter(destKey, partSize);
93
+ return multipartTransfer({
94
+ readable: rangeReadable,
95
+ sink,
96
+ writer,
97
+ totalBytes: size,
98
+ partSize,
99
+ startOffset: token?.offset,
100
+ limiter: options.concurrencyLimiter ?? defaultConcurrencyLimiter,
101
+ context,
102
+ });
103
+ }, retry);
95
104
  strategy = "multipart";
96
105
  }
97
106
  else {
package/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from "./src/decorators/locallyCached.ts";
4
4
  export * from "./src/decorators/seekable.ts";
5
5
  export * from "./src/registry/detectProtocol.ts";
6
6
  export * from "./src/registry/ProviderRegistry.ts";
7
+ export * from "./src/registry/StructuredLocation.ts";
7
8
  export * from "./src/retry/RetryOptions.ts";
8
9
  export * from "./src/retry/withRetry.ts";
9
10
  export * from "./src/transfer/copyMove.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/pluggable-io-framework",
3
- "version": "2.0.0",
3
+ "version": "2.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",
@@ -40,13 +40,13 @@
40
40
  "test": "bun test"
41
41
  },
42
42
  "dependencies": {
43
- "@flowscripter/dynamic-plugin-framework": "^2.2.14",
44
- "@flowscripter/pluggable-io-framework-api": "^3.0.0"
43
+ "@flowscripter/dynamic-plugin-framework": "^2.2.18",
44
+ "@flowscripter/pluggable-io-framework-api": "^3.1.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/bun": "^1.4.2",
48
- "oxfmt": "0.71.0",
49
- "oxlint": "1.86.0",
48
+ "oxfmt": "0.72.0",
49
+ "oxlint": "1.87.0",
50
50
  "zod": "^4.6.5"
51
51
  },
52
52
  "peerDependencies": {
@@ -13,6 +13,7 @@ import {
13
13
  } from "@flowscripter/pluggable-io-framework-api";
14
14
  import type { TransferOptions } from "../transfer/TransferOptions.ts";
15
15
  import { detectProtocol } from "./detectProtocol.ts";
16
+ import type { StructuredLocation } from "./StructuredLocation.ts";
16
17
  import { DEFAULT_NATIVE_DOMAINS, negotiateTransfer } from "./negotiateTransfer.ts";
17
18
 
18
19
  /** Nested provider resolutions allowed before resolution fails. */
@@ -33,6 +34,10 @@ export interface TransferProviders {
33
34
  readonly options: TransferOptions;
34
35
  }
35
36
 
37
+ function protocolOf(location: string | StructuredLocation): string {
38
+ return typeof location === "string" ? detectProtocol(location) : location.protocol;
39
+ }
40
+
36
41
  function locationFieldNames(factory: IOProviderFactory): string[] {
37
42
  const shape = (factory.locationSchema as { shape?: Record<string, unknown> }).shape;
38
43
  return Object.keys(shape ?? {}).sort();
@@ -41,7 +46,8 @@ function locationFieldNames(factory: IOProviderFactory): string[] {
41
46
  /**
42
47
  * Discovers provider factories and payload converters through a
43
48
  * `dynamic-plugin-framework` `PluginManager`, keyed by protocol and
44
- * payload kind, and creates providers for location strings. It is also the
49
+ * payload kind, and creates providers for location strings or
50
+ * {@link StructuredLocation}s. It is also the
45
51
  * `ProviderResolver` handed to every provider it creates.
46
52
  */
47
53
  export class ProviderRegistry implements ProviderResolver {
@@ -115,7 +121,7 @@ export class ProviderRegistry implements ProviderResolver {
115
121
  }
116
122
 
117
123
  public createProviderForLocation(
118
- location: string,
124
+ location: string | StructuredLocation,
119
125
  options?: { kind?: PayloadKind; domain?: string },
120
126
  ): Promise<ResolvedProvider> {
121
127
  return this.#resolve(location, options, 0);
@@ -126,12 +132,12 @@ export class ProviderRegistry implements ProviderResolver {
126
132
  * (kind, domain) and payload type, or a converter.
127
133
  */
128
134
  public async createProvidersForTransfer(
129
- source: string,
130
- dest: string,
135
+ source: string | StructuredLocation,
136
+ dest: string | StructuredLocation,
131
137
  options?: { kind?: PayloadKind },
132
138
  ): Promise<TransferProviders> {
133
- const sourceProtocol = detectProtocol(source);
134
- const destProtocol = detectProtocol(dest);
139
+ const sourceProtocol = protocolOf(source);
140
+ const destProtocol = protocolOf(dest);
135
141
  const sourceFactories = this.#factoriesFor(sourceProtocol, options?.kind);
136
142
  const destFactories = this.#factoriesFor(destProtocol, options?.kind);
137
143
  const negotiation = negotiateTransfer(
@@ -193,14 +199,14 @@ export class ProviderRegistry implements ProviderResolver {
193
199
  }
194
200
 
195
201
  async #resolve(
196
- location: string,
202
+ location: string | StructuredLocation,
197
203
  options: { kind?: PayloadKind; domain?: string } | undefined,
198
204
  depth: number,
199
205
  ): Promise<ResolvedProvider> {
200
206
  if (depth > MAX_RESOLUTION_DEPTH) {
201
207
  throw new PermanentIOError("provider resolution too deep");
202
208
  }
203
- const protocol = detectProtocol(location);
209
+ const protocol = protocolOf(location);
204
210
  this.#factoriesFor(protocol, options?.kind);
205
211
  const factory = this.getFactory(protocol, options?.kind) as IOProviderFactory;
206
212
  let domain: string | undefined;
@@ -218,11 +224,13 @@ export class ProviderRegistry implements ProviderResolver {
218
224
 
219
225
  async #instantiate(
220
226
  factory: IOProviderFactory,
221
- location: string,
227
+ location: string | StructuredLocation,
222
228
  domain: string | undefined,
223
229
  depth: number,
224
230
  ): Promise<ResolvedProvider> {
225
- const parsed = factory.locationSchema.parse(factory.parseLocationString(location));
231
+ const raw =
232
+ typeof location === "string" ? factory.parseLocationString(location) : location.location;
233
+ const parsed = factory.locationSchema.parse(raw);
226
234
  const { config, target } = factory.toProviderInputs(parsed);
227
235
  const resolver: ProviderResolver = {
228
236
  createProviderForLocation: (inner, opts) =>
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A location given as its protocol plus the raw location object that the
3
+ * protocol's factory `locationSchema` validates, instead of a location
4
+ * string. It can carry fields a string cannot, such as `filename`,
5
+ * `pattern` or credentials.
6
+ */
7
+ export interface StructuredLocation {
8
+ readonly protocol: string;
9
+ readonly location: unknown;
10
+ }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  type IOProvider,
3
3
  type Item,
4
+ type MultipartWriter,
4
5
  type Part,
5
6
  type RangeReadable,
6
7
  type StreamHandle,
@@ -18,9 +19,11 @@ import { rangeReadableMultipartReader } from "./rangeReadableMultipartReader.ts"
18
19
  export interface MultipartTransferInput {
19
20
  readonly readable: StreamHandle & RangeReadable;
20
21
  readonly sink: IOProvider;
21
- readonly writer: { write(parts: AsyncIterable<Part>): Promise<void> };
22
+ readonly writer: MultipartWriter;
22
23
  readonly totalBytes: number;
23
24
  readonly partSize: number;
25
+ /** Where a resumed upload continues: parts start at the part containing this offset. */
26
+ readonly startOffset?: number;
24
27
  readonly limiter: ConcurrencyLimiter;
25
28
  readonly context: EntryContext;
26
29
  }
@@ -30,13 +33,15 @@ export interface MultipartTransferInput {
30
33
  * writer, bounded by `limiter`. A part whose read fails with a
31
34
  * `TransientIOError` is re-read on its own, up to `retry.maxRetries` times,
32
35
  * within the same upload. `stop` ends the transfer after the parts already
33
- * started; `signal` aborts it.
36
+ * started; `signal` aborts it. Reported bytes include the parts before
37
+ * `startOffset`.
34
38
  */
35
39
  export async function multipartTransfer(input: MultipartTransferInput): Promise<EntryOutcome> {
36
40
  const { readable, sink, writer, totalBytes, partSize, limiter, context } = input;
37
41
  const { options, hooks, operationId, type } = context;
38
42
  const retry = options.retry ?? DEFAULT_RETRY;
39
- let bytes = 0;
43
+ const firstOffset = Math.floor((input.startOffset ?? 0) / partSize) * partSize;
44
+ let bytes = firstOffset;
40
45
  let items = 0;
41
46
  let stopped = false;
42
47
 
@@ -103,7 +108,12 @@ export async function multipartTransfer(input: MultipartTransferInput): Promise<
103
108
  }
104
109
 
105
110
  async function* sourceParts(): AsyncGenerator<Part> {
106
- for await (const part of rangeReadableMultipartReader(readable, totalBytes, partSize)) {
111
+ for await (const part of rangeReadableMultipartReader(
112
+ readable,
113
+ totalBytes,
114
+ partSize,
115
+ firstOffset,
116
+ )) {
107
117
  if (options.signal?.aborted) throw createAbortError();
108
118
  if (options.stop?.aborted) {
109
119
  stopped = true;
@@ -8,15 +8,17 @@ import type {
8
8
  /**
9
9
  * Splits a `RangeReadable` handle into parts of `partSize` bytes covering
10
10
  * `totalSize`. Each part's stream is opened with `readRange(start, end)`
11
- * (`end` exclusive) when the part is pulled.
11
+ * (`end` exclusive) when the part is pulled. With `startOffset`, parts begin
12
+ * at the part containing that offset.
12
13
  */
13
14
  export async function* rangeReadableMultipartReader<K extends PayloadKind>(
14
15
  handle: StreamHandle<K> & RangeReadable<K>,
15
16
  totalSize: number,
16
17
  partSize: number,
18
+ startOffset = 0,
17
19
  ): AsyncGenerator<Part<K>> {
18
20
  const partCount = Math.max(1, Math.ceil(totalSize / partSize));
19
- for (let index = 0; index < partCount; index += 1) {
21
+ for (let index = Math.floor(startOffset / partSize); index < partCount; index += 1) {
20
22
  const start = index * partSize;
21
23
  const end = Math.min(start + partSize, totalSize);
22
24
  yield {
@@ -5,7 +5,9 @@ import {
5
5
  isBufferProvider,
6
6
  isFillReadable,
7
7
  isRangeReadable,
8
+ isResumableWritable,
8
9
  type Item,
10
+ type MultipartWriter,
9
11
  PermanentIOError,
10
12
  type StreamHandle,
11
13
  } from "@flowscripter/pluggable-io-framework-api";
@@ -124,19 +126,24 @@ export async function transferEntry(input: TransferEntryInput): Promise<Transfer
124
126
  ) {
125
127
  await cancelStream(readable);
126
128
  const rangeReadable = readable;
127
- outcome = await withRetry(
128
- () =>
129
- multipartTransfer({
130
- readable: rangeReadable,
131
- sink,
132
- writer: multipartWriter(destKey, partSize),
133
- totalBytes: size,
134
- partSize,
135
- limiter: options.concurrencyLimiter ?? defaultConcurrencyLimiter,
136
- context,
137
- }),
138
- retry,
139
- );
129
+ let writer: MultipartWriter | undefined;
130
+ outcome = await withRetry(() => {
131
+ // A retry continues the failed writer's upload when it has a resume token.
132
+ const token = writer && isResumableWritable(writer) ? writer.resumeToken() : undefined;
133
+ writer = token
134
+ ? multipartWriter(destKey, partSize, { resume: token })
135
+ : multipartWriter(destKey, partSize);
136
+ return multipartTransfer({
137
+ readable: rangeReadable,
138
+ sink,
139
+ writer,
140
+ totalBytes: size,
141
+ partSize,
142
+ startOffset: token?.offset,
143
+ limiter: options.concurrencyLimiter ?? defaultConcurrencyLimiter,
144
+ context,
145
+ });
146
+ }, retry);
140
147
  strategy = "multipart";
141
148
  } else {
142
149
  let writable = await withRetry(() => sink.getWritableStream(destKey), retry);