first commit
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# classify
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `classify` module hosts the pure error-classification function that
|
||||
governs fall-through eligibility for every walk strategy. It maps an
|
||||
arbitrary throwable into one of three classes — `"transient"`,
|
||||
`"semantic"`, or `"abort"` — based on the error's namespaced code, its
|
||||
shape (`AbortError` name), and the configured `FallbackPolicy.errorClass`
|
||||
knob. The function is deterministic and side-effect-free.
|
||||
|
||||
## Features
|
||||
|
||||
- Recognises `STORAGE_URI.ABORTED` and `FALLBACK_URI.ABORTED` as
|
||||
`"abort"`.
|
||||
- Treats `MAX_DEPTH_EXCEEDED` and `CIRCULAR_REFERENCE` as `"semantic"`
|
||||
regardless of policy (programmer-bug surfaces must not silently fall
|
||||
through).
|
||||
- Maps `STORAGE_URI.ADAPTER_ERROR` and `STORAGE_URI.NOT_FOUND` to
|
||||
`"transient"` by default.
|
||||
- Treats other `STORAGE_URI.*` semantic codes (UNKNOWN_SCHEME,
|
||||
INVALID_URI, WRITE_UNSUPPORTED, LIST_UNSUPPORTED, SCHEME_CONFLICT) as
|
||||
`"semantic"` under default policy and `"transient"` under
|
||||
`errorClass: "any"`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { classify } from "@suaveplan/storage-uri-plugin-fallback/classify";
|
||||
|
||||
const klass = classify(err, policy);
|
||||
if (klass === "abort") throw err;
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { classify } from "@suaveplan/storage-uri-plugin-fallback/classify";
|
||||
import { FallbackPolicySchema } from "@suaveplan/storage-uri-plugin-fallback/types";
|
||||
|
||||
const relaxed = FallbackPolicySchema.parse({ errorClass: "any" });
|
||||
for (const err of collected) {
|
||||
if (classify(err, relaxed) === "transient") continue;
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ErrorClass` — `"transient" | "semantic" | "abort"`.
|
||||
- `classify(err: unknown, policy: FallbackPolicy): ErrorClass`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The classifier never re-throws; callers own the decision to surface or
|
||||
swallow. Unknown thrown values default to `"transient"` because the
|
||||
resolver normalises foreign throwables into `STORAGE_URI.ADAPTER_ERROR`
|
||||
upstream of the walk, so the unknown branch is a safety net for
|
||||
deliberately bypassed paths (e.g. a candidate plugin that throws a plain
|
||||
`Error` synchronously inside its `parse`).
|
||||
|
||||
This module is part of the universal fallback URI plugin family. It coordinates with the recursion, walk, ops, observability, and classification modules through small, narrowly-typed value contracts. The implementation deliberately keeps surface area small so each piece can be substituted or instrumented independently without touching consumers. Refer to the package README for the end-to-end picture and to the spec delta for the requirement matrix this module helps satisfy.
|
||||
@@ -0,0 +1,72 @@
|
||||
# conformance/fallback-conformance
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `fallback-conformance` module exports `runFallbackConformance`, a
|
||||
black-box harness consumers call from their own integration tests to
|
||||
prove their wiring honours the `fallback://` plugin's contract end-to-
|
||||
end. The harness exercises the K-fail-then-success path, the
|
||||
all-candidates-fail path, pre-aborted signal short-circuit, cycle
|
||||
detection, and mirror-write fan-out — the five end-to-end invariants
|
||||
every consumer should keep green.
|
||||
|
||||
## Features
|
||||
|
||||
- Asserts presence of the `fallback` scheme on the supplied resolver.
|
||||
- Verifies first-success on a mixed walk.
|
||||
- Verifies `ALL_CANDIDATES_FAILED` with N entries on full failure.
|
||||
- Verifies `ABORTED` on a pre-aborted signal.
|
||||
- Verifies cycle / recursion-protection surfaces a structured error.
|
||||
- Verifies mirror writes propagate to every leg via direct reads.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { runFallbackConformance } from "@suaveplan/storage-uri-plugin-fallback/conformance";
|
||||
|
||||
await runFallbackConformance(resolver, {
|
||||
successCandidates: ["mem://k/seed", "mem2://k/v"],
|
||||
failureCandidates: ["mem://k/missing", "mem://k/also-missing"],
|
||||
expectedSecondBytes: new Uint8Array([1, 2, 3]),
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
FallbackConformanceFailure,
|
||||
runFallbackConformance,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/conformance";
|
||||
|
||||
try {
|
||||
await runFallbackConformance(resolver, fixtures);
|
||||
} catch (e) {
|
||||
if (e instanceof FallbackConformanceFailure) {
|
||||
console.error("wiring drift:", e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `runFallbackConformance(resolver, fixtures): Promise<void>`
|
||||
- `FallbackConformanceFixtures` — `{ successCandidates, failureCandidates,
|
||||
expectedSecondBytes, events? }`.
|
||||
- `FallbackConformanceFailure` — thrown when an expectation is violated.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The harness is intentionally lenient on the cycle-detection path: an
|
||||
implementation may surface either `CIRCULAR_REFERENCE` or
|
||||
`ALL_CANDIDATES_FAILED` depending on whether the cycling candidates
|
||||
resolve through a transient walker first. Consumers needing strict
|
||||
behaviour can wrap the harness call with stricter assertions of their
|
||||
own.
|
||||
|
||||
This module is part of the universal fallback URI plugin family. It coordinates with the recursion, walk, ops, observability, and classification modules through small, narrowly-typed value contracts. The implementation deliberately keeps surface area small so each piece can be substituted or instrumented independently without touching consumers. Refer to the package README for the end-to-end picture and to the spec delta for the requirement matrix this module helps satisfy.
|
||||
@@ -0,0 +1,78 @@
|
||||
# error-codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `error-codes` module hosts the structured-error registry for the
|
||||
`@suaveplan/storage-uri-plugin-fallback` package. It declares the eight
|
||||
canonical `FALLBACK_URI.*` codes via `defineCodes` and exposes a single
|
||||
`FallbackUriError` subclass that any walk, ops, or recursion failure can
|
||||
throw. The registry is the source of truth for how the package surfaces
|
||||
failures to consumers and to OpenSpec gates.
|
||||
|
||||
## Features
|
||||
|
||||
- Eight canonical, fully-qualified error codes (`NO_CANDIDATES`,
|
||||
`INVALID_URI`, `ALL_CANDIDATES_FAILED`, `WRITE_MIRROR_FAILED`,
|
||||
`LIST_UNION_FAILED`, `ABORTED`, `MAX_DEPTH_EXCEEDED`, `CIRCULAR_REFERENCE`).
|
||||
- Typed discriminants: `uri`, `candidateIndex`, `candidateUri`,
|
||||
`candidateErrors`, `mirrorOutcomes`, `cycleChain`, `depth`.
|
||||
- Side-effect-free constants suitable for tree-shaking via the package's
|
||||
`"sideEffects": false` flag.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
FALLBACK_URI_CODES,
|
||||
FallbackUriError,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/error-codes";
|
||||
|
||||
throw new FallbackUriError(
|
||||
FALLBACK_URI_CODES.ALL_CANDIDATES_FAILED,
|
||||
"every candidate failed",
|
||||
{ uri: "fallback://?u=https%3A%2F%2Fa", candidateErrors: [] },
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
FALLBACK_URI_CODES,
|
||||
FallbackUriError,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/error-codes";
|
||||
|
||||
function handle(err: unknown): void {
|
||||
if (err instanceof FallbackUriError) {
|
||||
if (err.code === FALLBACK_URI_CODES.WRITE_MIRROR_FAILED) {
|
||||
for (const o of err.mirrorOutcomes ?? []) {
|
||||
console.error(o.uri, o.status, o.error?.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `FALLBACK_URI_CODES` — frozen record of `<reason → "FALLBACK_URI.<reason>">`.
|
||||
- `FallbackUriError extends SuaveplanError<FallbackUriErrorCode>` — error
|
||||
carrying every optional discriminant.
|
||||
- `FallbackUriErrorOptions` — extension of `SuaveplanErrorOptions`
|
||||
exposing the per-code discriminant fields.
|
||||
- `CandidateErrorOption`, `MirrorOutcomeOption` — payload shapes attached
|
||||
to per-leg failure reports.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The codes registry uses `defineCodes` so every reason yields the
|
||||
fully-qualified namespaced string at compile time. Discriminants are
|
||||
declared optional because no single code populates all of them — the
|
||||
constructor copies only those supplied by the caller. The class name is
|
||||
set verbatim for stable structured logging and serialization. The
|
||||
registry is consumed by the OpenSpec reachability sweep and by the
|
||||
package's side-effect import in `src/index.ts`.
|
||||
@@ -0,0 +1,73 @@
|
||||
# observability/events
|
||||
|
||||
## Purpose
|
||||
|
||||
The `observability/events` module exposes thin emit helpers that forward
|
||||
the package's five canonical event topics to a caller-supplied
|
||||
`EventBusLike`. Each helper accepts an optional bus argument so the
|
||||
plugin can be constructed without one — the helpers are no-ops in that
|
||||
case. The helpers exist so the rest of the package never reaches for the
|
||||
bus directly; topic strings live alongside the payload types in
|
||||
`policy.types`.
|
||||
|
||||
## Features
|
||||
|
||||
- Five emit helpers — `attempt-failed`, `attempt-succeeded`,
|
||||
`write-mirror-partial`, `cycle-detected`, `depth-exceeded`.
|
||||
- Each helper is a no-op when no bus is provided.
|
||||
- Topic strings centralised in `FALLBACK_TOPICS` for tree-shake-safe
|
||||
string sharing.
|
||||
- Type-safe payloads via `FallbackEventMap`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
emitAttemptFailed,
|
||||
emitAttemptSucceeded,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/observability";
|
||||
|
||||
emitAttemptFailed(bus, {
|
||||
uri,
|
||||
candidateIndex: 0,
|
||||
candidateUri: "https://a/x",
|
||||
code: "STORAGE_URI.NOT_FOUND",
|
||||
});
|
||||
emitAttemptSucceeded(bus, {
|
||||
uri,
|
||||
candidateIndex: 1,
|
||||
candidateUri: "drive://primary/x",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
emitCycleDetected,
|
||||
emitDepthExceeded,
|
||||
emitWriteMirrorPartial,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/observability";
|
||||
|
||||
emitWriteMirrorPartial(bus, { uri, successCount: 1, failureCount: 1 });
|
||||
emitCycleDetected(bus, { uri, cycleChain: [canonical] });
|
||||
emitDepthExceeded(bus, { uri, depth: 5 });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `emitAttemptFailed(bus, payload)`
|
||||
- `emitAttemptSucceeded(bus, payload)`
|
||||
- `emitWriteMirrorPartial(bus, payload)`
|
||||
- `emitCycleDetected(bus, payload)`
|
||||
- `emitDepthExceeded(bus, payload)`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The helpers do not buffer or retry — they fire synchronously and forget.
|
||||
A misbehaving subscriber that throws will propagate, so consumers should
|
||||
wrap risky handlers themselves. The plugin never assumes the bus is
|
||||
present; passing `undefined` is the supported way to silence the
|
||||
observability surface entirely.
|
||||
|
||||
This module is part of the universal fallback URI plugin family. It coordinates with the recursion, walk, ops, observability, and classification modules through small, narrowly-typed value contracts. The implementation deliberately keeps surface area small so each piece can be substituted or instrumented independently without touching consumers. Refer to the package README for the end-to-end picture and to the spec delta for the requirement matrix this module helps satisfy.
|
||||
@@ -0,0 +1,79 @@
|
||||
# observability/otel
|
||||
|
||||
## Purpose
|
||||
|
||||
The `observability/otel` module owns the OpenTelemetry instrument bundle
|
||||
for the package: the per-candidate `storage_uri.fallback_total` counter,
|
||||
the per-walk `storage_uri.fallback_walk_ms` histogram, and the parent +
|
||||
per-attempt span helpers. Instruments are lazily allocated against the
|
||||
active `MeterProvider`; a test seam (`setFallbackMeterProvider`,
|
||||
`resetFallbackInstruments`) lets test suites swap providers without a
|
||||
process restart.
|
||||
|
||||
## Features
|
||||
|
||||
- Lazy instrument acquisition with force-refresh seam for tests.
|
||||
- `recordAttempt(index, scheme, outcome)` + `recordWalk(durationMs, outcome)`.
|
||||
- Parent span helper (`startParentSpan`) with caller-provided attribute
|
||||
bag and per-attempt child span helper (`startAttemptSpan`).
|
||||
- Provider override via `setFallbackMeterProvider(provider | undefined)`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
recordAttempt,
|
||||
recordWalk,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/observability";
|
||||
|
||||
const start = Date.now();
|
||||
recordAttempt(0, "https", "transient");
|
||||
recordAttempt(1, "drive", "ok");
|
||||
recordWalk(Date.now() - start, "ok");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
setFallbackMeterProvider,
|
||||
startAttemptSpan,
|
||||
startParentSpan,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/observability";
|
||||
|
||||
setFallbackMeterProvider(myMeterProvider);
|
||||
const parent = startParentSpan("read", {
|
||||
"fallback.policy.max_parallel": 2,
|
||||
});
|
||||
try {
|
||||
const child = startAttemptSpan(0, "https");
|
||||
try {
|
||||
await dispatch();
|
||||
} finally {
|
||||
child.end();
|
||||
}
|
||||
} finally {
|
||||
parent.end();
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `getFallbackInstruments(force?: boolean): FallbackInstruments`
|
||||
- `resetFallbackInstruments()`
|
||||
- `setFallbackMeterProvider(provider | undefined)`
|
||||
- `recordAttempt(candidateIndex, scheme, outcome)`
|
||||
- `recordWalk(durationMs, outcome)`
|
||||
- `startParentSpan(op, attrs): Span`
|
||||
- `startAttemptSpan(candidateIndex, scheme): Span`
|
||||
- `CandidateOutcome`, `WalkOutcome`, `FallbackInstruments`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Counters and histograms attach attribute bags so consumers can slice by
|
||||
candidate index, scheme, and outcome without separate metrics per
|
||||
combination. The tracer name and meter name share the package name
|
||||
verbatim so an OTel pipeline configured for `@suaveplan/*` picks them up
|
||||
without additional wiring.
|
||||
|
||||
This module is part of the universal fallback URI plugin family. It coordinates with the recursion, walk, ops, observability, and classification modules through small, narrowly-typed value contracts. The implementation deliberately keeps surface area small so each piece can be substituted or instrumented independently without touching consumers. Refer to the package README for the end-to-end picture and to the spec delta for the requirement matrix this module helps satisfy.
|
||||
@@ -0,0 +1,68 @@
|
||||
# ops/delete
|
||||
|
||||
## Purpose
|
||||
|
||||
The `ops/delete` module implements `fallbackDelete`, the mirror-aware
|
||||
removal operation. It dispatches `remove` to every candidate via
|
||||
`Promise.allSettled`. Under the default `deleteMode: "any-success"` it
|
||||
returns `true` when at least one candidate confirmed removal; under
|
||||
`"all"` it throws `WRITE_MIRROR_FAILED` if any candidate rejects. The
|
||||
result matches `StorageLike.delete`'s boolean contract while preserving
|
||||
structured failure metadata.
|
||||
|
||||
## Features
|
||||
|
||||
- Mirror fan-out via `Promise.allSettled`.
|
||||
- `deleteMode: "any-success"` (default) and `"all"` matrix.
|
||||
- Pre-aborted signal short-circuit under `stopOnAbort`.
|
||||
- Foreign throwables wrapped in `MirrorOutcome.error` per leg.
|
||||
- Surfaces total failure with `WRITE_MIRROR_FAILED` when every leg fails.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { fallbackDelete } from "@suaveplan/storage-uri-plugin-fallback/ops";
|
||||
|
||||
const removed = await fallbackDelete(
|
||||
uri,
|
||||
{ resolver, policy, events },
|
||||
signal,
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { fallbackDelete } from "@suaveplan/storage-uri-plugin-fallback/ops";
|
||||
|
||||
try {
|
||||
await fallbackDelete(
|
||||
uri,
|
||||
{
|
||||
resolver,
|
||||
policy: { ...policy, deleteMode: "all" },
|
||||
events,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
} catch (e) {
|
||||
if ((e as { code?: string }).code === "FALLBACK_URI.WRITE_MIRROR_FAILED") {
|
||||
console.error("delete left a stale mirror behind");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `fallbackDelete(uri, deps, signal): Promise<boolean>`
|
||||
- `DeleteDeps` — `{ resolver, policy, events }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`any-success` resolves `false` when every candidate cleanly reported the
|
||||
key was absent (no failures, no removals). It only throws when every leg
|
||||
actively rejects. `deleteMode: "all"` upgrades any leg failure to a
|
||||
structured throw so callers needing strict mirror parity can pattern
|
||||
match on the code.
|
||||
|
||||
This module is part of the universal fallback URI plugin family. It coordinates with the recursion, walk, ops, observability, and classification modules through small, narrowly-typed value contracts. The implementation deliberately keeps surface area small so each piece can be substituted or instrumented independently without touching consumers. Refer to the package README for the end-to-end picture and to the spec delta for the requirement matrix this module helps satisfy.
|
||||
@@ -0,0 +1,69 @@
|
||||
# ops/list
|
||||
|
||||
## Purpose
|
||||
|
||||
The `ops/list` module implements `fallbackList`, the enumeration
|
||||
operation for `fallback://` URIs. It invokes `list` on every candidate
|
||||
via `Promise.allSettled`, then either unions the resulting URI sets with
|
||||
first-occurrence dedup (default `listMode: "union"`) or returns the raw
|
||||
concatenation (`"concat"`). Per-candidate failures escalate to
|
||||
`LIST_UNION_FAILED` under `listFailureMode: "throw"` (default) or are
|
||||
excluded from the partial union under `"partial"`.
|
||||
|
||||
## Features
|
||||
|
||||
- Concurrent fan-out over every candidate.
|
||||
- Stable dedup by canonical URI string, first-occurrence wins.
|
||||
- `hasMore` propagated as the logical OR across candidates.
|
||||
- `continuationToken` deliberately omitted (per-adapter, non-mergeable).
|
||||
- Failure modes: `throw` (raise structured) or `partial` (emit warning).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { fallbackList } from "@suaveplan/storage-uri-plugin-fallback/ops";
|
||||
|
||||
const result = await fallbackList(
|
||||
uri,
|
||||
{ limit: 100 },
|
||||
{ resolver, policy, events },
|
||||
signal,
|
||||
);
|
||||
console.log(result.uris.length, result.hasMore);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { fallbackList } from "@suaveplan/storage-uri-plugin-fallback/ops";
|
||||
|
||||
const partial = await fallbackList(
|
||||
uri,
|
||||
undefined,
|
||||
{
|
||||
resolver,
|
||||
policy: {
|
||||
...policy,
|
||||
listMode: "concat",
|
||||
listFailureMode: "partial",
|
||||
},
|
||||
events,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `fallbackList(uri, options, deps, signal): Promise<ListUriResult>`
|
||||
- `ListDeps` — `{ resolver, policy, events }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Dedup occurs only under `listMode: "union"`; `"concat"` preserves every
|
||||
candidate's keys verbatim so callers counting per-candidate hits can
|
||||
distinguish duplicates. Failures are wrapped in
|
||||
`FALLBACK_URI.LIST_UNION_FAILED` so consumers can pattern-match without
|
||||
having to introspect each `MirrorOutcome.error`.
|
||||
|
||||
This module is part of the universal fallback URI plugin family. It coordinates with the recursion, walk, ops, observability, and classification modules through small, narrowly-typed value contracts. The implementation deliberately keeps surface area small so each piece can be substituted or instrumented independently without touching consumers. Refer to the package README for the end-to-end picture and to the spec delta for the requirement matrix this module helps satisfy.
|
||||
@@ -0,0 +1,60 @@
|
||||
# ops/metadata
|
||||
|
||||
## Purpose
|
||||
|
||||
The `ops/metadata` module hosts `fallbackExists` and `fallbackStat`, the
|
||||
metadata-side adapter operations. Both walk the candidate list
|
||||
sequentially and return as soon as a candidate produces an authoritative
|
||||
answer. Failures fall through cleanly: `exists` resolves `false` when no
|
||||
candidate confirms presence, and `stat` resolves `undefined` when no
|
||||
candidate can answer cheaply.
|
||||
|
||||
## Features
|
||||
|
||||
- Sequential walk with first-truthy semantics for `exists`.
|
||||
- Sequential walk returning the first defined `StatResult` for `stat`.
|
||||
- Soft failures: aggregate exhaustion does not raise, mirroring the
|
||||
semantics of `StorageLike.exists`.
|
||||
- Recursion-context propagation via the shared `DepthContext`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
fallbackExists,
|
||||
fallbackStat,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/ops";
|
||||
|
||||
const present = await fallbackExists(uri, ctx, deps, signal);
|
||||
const meta = await fallbackStat(uri, ctx, deps, signal);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { fallbackStat } from "@suaveplan/storage-uri-plugin-fallback/ops";
|
||||
|
||||
const meta = await fallbackStat(
|
||||
uri,
|
||||
ctx,
|
||||
{ resolver, policy: { ...policy, errorClass: "any" }, events },
|
||||
signal,
|
||||
);
|
||||
if (meta?.etag) console.log("etag:", meta.etag);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `fallbackExists(uri, ctx, deps, signal): Promise<boolean>`
|
||||
- `fallbackStat(uri, ctx, deps, signal): Promise<StatResult | undefined>`
|
||||
- `MetadataDeps` — `{ resolver, policy, events }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Both functions internally throw a sentinel error when a candidate
|
||||
answers "no" or "no-stat" so the shared walk machinery treats the leg as
|
||||
a transient failure and continues. The outer `try/catch` collapses the
|
||||
walk-exhausted `ALL_CANDIDATES_FAILED` into the soft return values the
|
||||
`StorageLike` contract expects from these methods.
|
||||
|
||||
This module is part of the universal fallback URI plugin family. It coordinates with the recursion, walk, ops, observability, and classification modules through small, narrowly-typed value contracts. The implementation deliberately keeps surface area small so each piece can be substituted or instrumented independently without touching consumers. Refer to the package README for the end-to-end picture and to the spec delta for the requirement matrix this module helps satisfy.
|
||||
@@ -0,0 +1,62 @@
|
||||
# ops/read
|
||||
|
||||
## Purpose
|
||||
|
||||
The `ops/read` module implements the read-side of the fallback adapter
|
||||
surface. It wires the `fallback://` URI parser, the recursion-depth
|
||||
context, and the configured walk strategy (sequential or parallel) into
|
||||
two adapter methods: `fallbackGet` (returns `Uint8Array`) and
|
||||
`fallbackOpen` (returns `ReadableStream<Uint8Array>`). Both delegate
|
||||
per-candidate dispatch to the bound `StorageUriResolver`.
|
||||
|
||||
## Features
|
||||
|
||||
- Sequential default + parallel race based on `policy.maxParallel`.
|
||||
- Honours `policy.stopOnAbort` on pre-aborted signals for `open`.
|
||||
- Delegates per-candidate failures to the configured walk's classifier.
|
||||
- Foreign throwables wrapped via the walk's standard cause chain.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { fallbackGet } from "@suaveplan/storage-uri-plugin-fallback/ops";
|
||||
|
||||
const bytes = await fallbackGet(
|
||||
uri,
|
||||
ctx,
|
||||
{ resolver, policy, events },
|
||||
signal,
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { fallbackOpen } from "@suaveplan/storage-uri-plugin-fallback/ops";
|
||||
|
||||
const stream = await fallbackOpen(
|
||||
uri,
|
||||
ctx,
|
||||
{ resolver, policy: { ...policy, maxParallel: 3 }, events },
|
||||
signal,
|
||||
);
|
||||
for await (const chunk of streamAsyncIterable(stream)) {
|
||||
process(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `fallbackGet(uri, ctx, deps, signal): Promise<Uint8Array>`
|
||||
- `fallbackOpen(uri, ctx, deps, signal): Promise<ReadableStream<Uint8Array>>`
|
||||
- `ReadDeps` — `{ resolver, policy, events }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`fallbackOpen` short-circuits on a pre-aborted caller signal with
|
||||
`FALLBACK_URI.ABORTED` before parsing the URI; `fallbackGet` relies on
|
||||
the walk to surface the same error mid-walk. Range reads are forwarded
|
||||
verbatim because the resolver already exposes range parameters to each
|
||||
candidate's adapter; the fallback walk is transparent on that axis.
|
||||
|
||||
This module is part of the universal fallback URI plugin family. It coordinates with the recursion, walk, ops, observability, and classification modules through small, narrowly-typed value contracts. The implementation deliberately keeps surface area small so each piece can be substituted or instrumented independently without touching consumers. Refer to the package README for the end-to-end picture and to the spec delta for the requirement matrix this module helps satisfy.
|
||||
@@ -0,0 +1,69 @@
|
||||
# ops/write
|
||||
|
||||
## Purpose
|
||||
|
||||
The `ops/write` module implements the write-side semantics of the
|
||||
fallback adapter: `fallbackSet` for byte writes and `fallbackWriteStream`
|
||||
for streamed writes. Under `policy.writeMode: "all"` (default) the
|
||||
operation fans out to every candidate via `Promise.allSettled` so reads
|
||||
from any leg subsequently observe the same content; under
|
||||
`"first-success"` it walks sequentially and returns on the first leg to
|
||||
succeed, letting remaining writes settle in the background.
|
||||
|
||||
## Features
|
||||
|
||||
- Mirror fan-out with structured `WRITE_MIRROR_FAILED` on any leg failure.
|
||||
- `first-success` mode with background remainder + best-effort partial
|
||||
emission on the event bus.
|
||||
- AbortSignal short-circuit before dispatch under `stopOnAbort`.
|
||||
- Stream-input variant buffers chunks then routes through `fallbackSet`.
|
||||
- Foreign throwables wrapped per leg in `MirrorOutcome.error`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { fallbackSet } from "@suaveplan/storage-uri-plugin-fallback/ops";
|
||||
|
||||
await fallbackSet(
|
||||
uri,
|
||||
new Uint8Array([1, 2, 3]),
|
||||
ctx,
|
||||
{ resolver, policy, events },
|
||||
signal,
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { fallbackWriteStream } from "@suaveplan/storage-uri-plugin-fallback/ops";
|
||||
|
||||
await fallbackWriteStream(
|
||||
uri,
|
||||
body,
|
||||
ctx,
|
||||
{
|
||||
resolver,
|
||||
policy: { ...policy, writeMode: "first-success" },
|
||||
events,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `fallbackSet(uri, bytes, ctx, deps, signal): Promise<void>`
|
||||
- `fallbackWriteStream(uri, body, ctx, deps, signal): Promise<void>`
|
||||
- `WriteDeps` — `{ resolver, policy, events }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Mirror writes preserve every leg's structured error inside
|
||||
`mirrorOutcomes[i].error` — foreign throwables are wrapped in
|
||||
`FALLBACK_URI.WRITE_MIRROR_FAILED` so consumers always receive a
|
||||
`SuaveplanError`. `first-success` mode emits a single
|
||||
`write-mirror-partial` event when at least one background remainder
|
||||
write fails so operators can detect mirror drift.
|
||||
|
||||
This module is part of the universal fallback URI plugin family. It coordinates with the recursion, walk, ops, observability, and classification modules through small, narrowly-typed value contracts. The implementation deliberately keeps surface area small so each piece can be substituted or instrumented independently without touching consumers. Refer to the package README for the end-to-end picture and to the spec delta for the requirement matrix this module helps satisfy.
|
||||
@@ -0,0 +1,60 @@
|
||||
# abort-any
|
||||
|
||||
## Purpose
|
||||
|
||||
The `abort-any` module provides a universal `AbortSignal`
|
||||
composition helper. It prefers the native `AbortSignal.any` (ES2024) when
|
||||
the runtime offers it; otherwise it transparently falls back to an
|
||||
`AbortController`-driven implementation that listens on each input
|
||||
signal. The fallback path lets the package keep its tier-purity contract
|
||||
on runtimes that have not yet shipped `AbortSignal.any`.
|
||||
|
||||
## Features
|
||||
|
||||
- Detects native `AbortSignal.any` and forwards verbatim when present.
|
||||
- Pure-DOM fallback that respects already-aborted inputs.
|
||||
- Cleans up its listeners on first abort to avoid leaks.
|
||||
- Accepts `undefined` entries so callers can pass partial signal arrays
|
||||
(`[callerSignal, perCandidateTimeoutSignal]`).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { anySignal } from "@suaveplan/storage-uri-plugin-fallback/parse-uri";
|
||||
|
||||
const ctrlA = new AbortController();
|
||||
const ctrlB = new AbortController();
|
||||
const composed = anySignal([ctrlA.signal, ctrlB.signal]);
|
||||
ctrlA.abort();
|
||||
console.log(composed.aborted); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { anySignal } from "@suaveplan/storage-uri-plugin-fallback/parse-uri";
|
||||
|
||||
function withTimeout(
|
||||
callerSignal: AbortSignal | undefined,
|
||||
timeoutMs: number,
|
||||
): AbortSignal {
|
||||
const timer = new AbortController();
|
||||
setTimeout(() => timer.abort(new Error("timeout")), timeoutMs);
|
||||
return anySignal([callerSignal, timer.signal]);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `anySignal(signals: ReadonlyArray<AbortSignal | undefined>): AbortSignal`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The fallback path inspects every input for `aborted === true` before
|
||||
attaching listeners — a pre-aborted input aborts the composite
|
||||
immediately and skips the listener setup. On normal abort, the helper
|
||||
removes every registered listener so signals that outlive the composite
|
||||
(e.g. a long-lived caller signal) do not retain a reference back to the
|
||||
composite controller.
|
||||
|
||||
This module is part of the universal fallback URI plugin family. It coordinates with the recursion, walk, ops, observability, and classification modules through small, narrowly-typed value contracts. The implementation deliberately keeps surface area small so each piece can be substituted or instrumented independently without touching consumers. Refer to the package README for the end-to-end picture and to the spec delta for the requirement matrix this module helps satisfy.
|
||||
@@ -0,0 +1,75 @@
|
||||
# parse-fallback-uri
|
||||
|
||||
## Purpose
|
||||
|
||||
The `parse-fallback-uri` module owns the canonical grammar for the
|
||||
`fallback://` URI scheme. It parses input strings into ordered candidate
|
||||
URI lists, rejects malformed inputs with structured `FALLBACK_URI.*`
|
||||
errors, builds canonical URIs from candidate arrays, and exposes helpers
|
||||
to attach or strip the internal recursion-context query parameter used
|
||||
for cycle detection across resolver re-entries.
|
||||
|
||||
## Features
|
||||
|
||||
- `parseFallbackUri(uri, maxCandidates?)` — split into candidates +
|
||||
canonical re-serialisation; throws `NO_CANDIDATES` / `INVALID_URI`.
|
||||
- `buildFallbackUri(uris, maxCandidates?)` — construct a canonical URI
|
||||
using stdlib `URLSearchParams`; rejects empty / oversized inputs.
|
||||
- `stripContext(uri)` / `withContextId(uri, id)` — manipulate the
|
||||
internal `__fbctx=<id>` recursion-context marker.
|
||||
- Hard ceilings (`MAX_URI_LEN` 4096, `MAX_CANDIDATES_HARD` 64) enforced
|
||||
even when a more permissive policy value is supplied.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildFallbackUri,
|
||||
parseFallbackUri,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/parse-uri";
|
||||
|
||||
const uri = buildFallbackUri([
|
||||
"https://cdn.example.com/foo",
|
||||
"drive://primary/foo",
|
||||
]);
|
||||
const { candidates } = parseFallbackUri(uri);
|
||||
console.log(candidates);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
parseFallbackUri,
|
||||
withContextId,
|
||||
stripContext,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/parse-uri";
|
||||
|
||||
const decorated = withContextId(uri, "ctx-42");
|
||||
const parsed = parseFallbackUri(decorated);
|
||||
if (parsed.contextId === "ctx-42") {
|
||||
const canonical = stripContext(decorated);
|
||||
console.log("recursion key:", canonical);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `parseFallbackUri(uri: string, maxCandidates?: number): ParsedFallbackUri`
|
||||
- `buildFallbackUri(uris: readonly string[], maxCandidates?: number): string`
|
||||
- `stripContext(uri: string): string`
|
||||
- `withContextId(uri: string, contextId: string): string`
|
||||
- `FB_CTX_PARAM` — `"__fbctx"` marker name.
|
||||
- `MAX_URI_LEN` — 4096.
|
||||
- `MAX_CANDIDATES_HARD` — 64.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Parsing is delegated entirely to the Web-standard `URL` and
|
||||
`URLSearchParams` constructors, eliminating every custom regex outside
|
||||
syntactic guards. Candidate ordering is preserved because
|
||||
`URLSearchParams.getAll` is ordered. Round-trip safety is verified by a
|
||||
fast-check property test in the sibling `.test.ts` file. The
|
||||
`__fbctx` parameter is stripped from the canonicalised visited-set key so
|
||||
nested re-entries cannot defeat cycle detection by carrying different
|
||||
context ids.
|
||||
@@ -0,0 +1,77 @@
|
||||
# plugin
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `plugin` module hosts the `fallbackUriPlugin` factory — the public
|
||||
entry point of the package. The factory validates its options via Zod,
|
||||
binds the supplied `StorageUriResolver` for recursive dispatch, builds a
|
||||
streaming-extended adapter implementing every `StorageLike` method, and
|
||||
returns a frozen `UriPlugin` ready for registration alongside other
|
||||
scheme handlers. The plugin's adapter routes every operation through the
|
||||
walk + ops modules, threading a per-call `DepthContext` so nested
|
||||
`fallback://` URIs remain bounded.
|
||||
|
||||
## Features
|
||||
|
||||
- Boundary Zod validation of options + policy.
|
||||
- Frozen `UriPlugin` with explicit `parse` / `build` round-trip.
|
||||
- Full streaming-extended `StorageLike` adapter: `get`, `set`, `delete`,
|
||||
`exists`, `list`, `clear`, `createReadStream`, `createWriteStream`,
|
||||
`getMetadata`, plus `listUri` for raw `ListUriResult`.
|
||||
- Recursion-safe via `DepthContext` registered through the parsed
|
||||
`__fbctx` query parameter.
|
||||
- Refuses `clear()` (heterogeneous-mirror semantics not well-defined).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
createStorageUriResolver,
|
||||
} from "@suaveplan/storage-uri";
|
||||
import { fallbackUriPlugin } from "@suaveplan/storage-uri-plugin-fallback/plugin";
|
||||
|
||||
const resolver = createStorageUriResolver([
|
||||
httpPlugin,
|
||||
drivePlugin,
|
||||
fallbackUriPlugin({
|
||||
resolver: () => resolver, // late-bound via a proxy in real wiring
|
||||
} as never),
|
||||
]);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { fallbackUriPlugin } from "@suaveplan/storage-uri-plugin-fallback/plugin";
|
||||
|
||||
const plugin = fallbackUriPlugin({
|
||||
resolver,
|
||||
policy: {
|
||||
maxParallel: 2,
|
||||
writeMode: "all",
|
||||
listFailureMode: "partial",
|
||||
},
|
||||
events: bus,
|
||||
});
|
||||
console.log(plugin.scheme); // "fallback"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `fallbackUriPlugin(options: FallbackPluginOptions): UriPlugin`
|
||||
- `FallbackAdapter` — adapter shape attached to `plugin.adapter`.
|
||||
- `buildFallbackUri` / `parseFallbackUri` re-exports for caller
|
||||
convenience.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The plugin holds the resolver by reference, allowing late binding via a
|
||||
proxy when the resolver instance has not yet been constructed. Each
|
||||
operation parses the input URI to discover any embedded context id; when
|
||||
present, it rehydrates the parent `DepthContext` from the registry,
|
||||
otherwise it creates a fresh root context. The root context is cleaned
|
||||
up in the `finally` block to keep the registry leak-free.
|
||||
@@ -0,0 +1,84 @@
|
||||
# depth-context
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `depth-context` module tracks the recursion state threaded through
|
||||
nested `fallback://` dispatches: the per-call depth counter, the visited
|
||||
set used for cycle detection, and the process-wide context registry that
|
||||
lets recursive resolver re-entries rehydrate their parent context purely
|
||||
from the URI string (via the `__fbctx` marker). It is the cornerstone of
|
||||
the package's two recursion safeguards.
|
||||
|
||||
## Features
|
||||
|
||||
- `DepthContext` value carrying `id`, `depth`, `maxDepth`, `visited`.
|
||||
- `enterDepth` / `leaveDepth` lifecycle with depth-cap enforcement.
|
||||
- `hasVisited` canonicalises URIs via `stripContext` so context-id
|
||||
variations of the same `fallback://` URI deduplicate correctly.
|
||||
- Process-wide registry (`registerContext`, `lookupContext`,
|
||||
`unregisterContext`) keyed by `ctx.id` so child dispatches survive a
|
||||
full resolver round trip.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
createDepthContext,
|
||||
enterDepth,
|
||||
leaveDepth,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/recursion";
|
||||
|
||||
const ctx = createDepthContext(4);
|
||||
enterDepth(ctx, "fallback://?u=mem%3A%2F%2Fa");
|
||||
try {
|
||||
// walk candidates
|
||||
} finally {
|
||||
leaveDepth(ctx);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
createDepthContext,
|
||||
lookupContext,
|
||||
registerContext,
|
||||
unregisterContext,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/recursion";
|
||||
|
||||
const ctx = createDepthContext(6);
|
||||
registerContext(ctx);
|
||||
try {
|
||||
// Pass ctx.id via the URI's __fbctx parameter so recursive resolver
|
||||
// dispatches re-enter the same context state.
|
||||
await dispatchWithContextId(uri, ctx.id);
|
||||
} finally {
|
||||
unregisterContext(ctx);
|
||||
}
|
||||
|
||||
console.log(lookupContext(ctx.id)); // undefined post-cleanup
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createDepthContext(maxDepth: number): DepthContext`
|
||||
- `enterDepth(ctx, uri)` — throws `MAX_DEPTH_EXCEEDED` past cap.
|
||||
- `leaveDepth(ctx)` — decrements; safe to over-call.
|
||||
- `hasVisited(ctx, uri): boolean`
|
||||
- `registerContext(ctx)` / `unregisterContext(ctx)` / `lookupContext(id)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Depth contexts are mutable inside one walk and never shared across
|
||||
unrelated calls — the registry is keyed by an opaque id so concurrent
|
||||
walks remain isolated. The visited set retains entries across
|
||||
`leaveDepth` because cycle detection cares about whether a URI has been
|
||||
seen anywhere in the call chain, not just whether it is currently on the
|
||||
stack.
|
||||
|
||||
This module is part of the universal fallback URI plugin family. It coordinates with the recursion, walk, ops, observability, and classification modules through small, narrowly-typed value contracts. The implementation deliberately keeps surface area small so each piece can be substituted or instrumented independently without touching consumers. Refer to the package README for the end-to-end picture and to the spec delta for the requirement matrix this module helps satisfy.
|
||||
@@ -0,0 +1,67 @@
|
||||
# candidate.types
|
||||
|
||||
## Purpose
|
||||
|
||||
The `candidate.types` module defines structural records that flow through
|
||||
the fallback walk and fan-out paths but are not part of the public policy
|
||||
boundary. Walk strategies and ops modules collect `CandidateError` and
|
||||
`MirrorOutcome` instances to assemble the structured errors raised at the
|
||||
edges of the package.
|
||||
|
||||
## Features
|
||||
|
||||
- `CandidateError` — one candidate's per-leg failure (`uri`, `error`).
|
||||
- `CandidateAttempt` — trace record for an attempt (`index`, `uri`,
|
||||
`scheme`, `outcome`, `durationMs`).
|
||||
- `MirrorOutcome` — per-leg result of a mirror-fan-out write or delete.
|
||||
- All types are `readonly` to keep walk state immutable.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
CandidateError,
|
||||
MirrorOutcome,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/types";
|
||||
|
||||
const errors: CandidateError[] = [];
|
||||
errors.push({ uri: "https://a/x", error: someErr });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
CandidateAttempt,
|
||||
MirrorOutcome,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/types";
|
||||
|
||||
function summarise(
|
||||
attempts: readonly CandidateAttempt[],
|
||||
outcomes: readonly MirrorOutcome[],
|
||||
): { failed: number; mirrored: number } {
|
||||
return {
|
||||
failed: attempts.filter((a) => a.outcome === "transient").length,
|
||||
mirrored: outcomes.filter((o) => o.status === "fulfilled").length,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `CandidateError` — `{ uri: string; error: SuaveplanError }`.
|
||||
- `CandidateAttempt` — `{ index: number; uri: string; scheme: string;
|
||||
outcome: "ok" | "transient" | "semantic" | "abort"; durationMs: number;
|
||||
error?: SuaveplanError }`.
|
||||
- `MirrorOutcome` — `{ uri: string; status: "fulfilled" | "rejected";
|
||||
error?: SuaveplanError }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
These records are deliberately leaf-structural so producers (`walk/`,
|
||||
`ops/`) and consumers (`error-codes`, observability emitters) can share
|
||||
them without coupling. The `error` field is typed as `SuaveplanError`
|
||||
because foreign throwables are normalised upstream by the resolver or
|
||||
wrapped by the walk before populating these records.
|
||||
|
||||
This module is part of the universal fallback URI plugin family. It coordinates with the recursion, walk, ops, observability, and classification modules through small, narrowly-typed value contracts. The implementation deliberately keeps surface area small so each piece can be substituted or instrumented independently without touching consumers. Refer to the package README for the end-to-end picture and to the spec delta for the requirement matrix this module helps satisfy.
|
||||
@@ -0,0 +1,71 @@
|
||||
# policy.types
|
||||
|
||||
## Purpose
|
||||
|
||||
The `policy.types` module defines the user-facing knobs governing how the
|
||||
`fallback://` plugin walks candidates, classifies errors, mirrors writes,
|
||||
unions lists, and caps recursion. It also declares the event-bus payload
|
||||
shapes emitted by the observability surface. These types are the only
|
||||
public boundary configuration consumers reach for when constructing the
|
||||
plugin.
|
||||
|
||||
## Features
|
||||
|
||||
- `FallbackPolicy` — every walk + fan-out knob with documented defaults.
|
||||
- `FallbackPluginOptions` — factory-input bundle including the resolver,
|
||||
policy overrides, logger, event bus, and clock.
|
||||
- `FALLBACK_TOPICS` — frozen topic-string constants for the event bus.
|
||||
- Typed payload interfaces for every emitted event topic.
|
||||
- `FallbackEventMap` — type-map intersected with `Record<string, unknown>`
|
||||
for `EventBusLike<FallbackEventMap>` compatibility.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
FallbackPluginOptions,
|
||||
FallbackPolicy,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/types";
|
||||
|
||||
const policy: Partial<FallbackPolicy> = {
|
||||
maxParallel: 1,
|
||||
writeMode: "all",
|
||||
};
|
||||
const options: FallbackPluginOptions = { resolver, policy };
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
type FallbackEventMap,
|
||||
FALLBACK_TOPICS,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/types";
|
||||
import { createEventBus } from "@suaveplan/events";
|
||||
|
||||
const bus = createEventBus<FallbackEventMap>();
|
||||
bus.on(FALLBACK_TOPICS.attemptFailed, (p) =>
|
||||
console.warn(`candidate ${p.candidateIndex} failed: ${p.code}`),
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `FallbackPolicy` — `errorClass`, `maxParallel`, `stopOnAbort`,
|
||||
`writeMode`, `deleteMode`, `listMode`, `listFailureMode`, `maxDepth`,
|
||||
`maxCandidates`.
|
||||
- `FallbackPluginOptions` — `{ resolver, policy?, logger?, events?, clock? }`.
|
||||
- `FALLBACK_TOPICS` — `attemptFailed`, `attemptSucceeded`,
|
||||
`writeMirrorPartial`, `cycleDetected`, `depthExceeded`.
|
||||
- `AttemptFailedPayload`, `AttemptSucceededPayload`,
|
||||
`WriteMirrorPartialPayload`, `CycleDetectedPayload`,
|
||||
`DepthExceededPayload`, `FallbackEventMap`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Defaults live on the Zod schemas in `policy.schemas.ts`; this file is
|
||||
type-only. `FallbackEventMap` is declared as a type intersection with
|
||||
`Record<string, unknown>` so it satisfies the `TMap extends Record<string,
|
||||
unknown>` constraint on `EventBusLike` without losing per-topic narrowing.
|
||||
|
||||
This module is part of the universal fallback URI plugin family. It coordinates with the recursion, walk, ops, observability, and classification modules through small, narrowly-typed value contracts. The implementation deliberately keeps surface area small so each piece can be substituted or instrumented independently without touching consumers. Refer to the package README for the end-to-end picture and to the spec delta for the requirement matrix this module helps satisfy.
|
||||
@@ -0,0 +1,68 @@
|
||||
# walk/parallel
|
||||
|
||||
## Purpose
|
||||
|
||||
The `walk/parallel` module implements the `Promise.any`-style race
|
||||
strategy activated when `policy.maxParallel > 1`. It dispatches up to
|
||||
`maxParallel` candidates concurrently, returns the first to resolve, and
|
||||
aborts every pending candidate via a composed `AbortSignal` so they can
|
||||
release any held resources. Failures arriving before the first success
|
||||
are collected; if every candidate ends in transient failure, the walk
|
||||
throws `ALL_CANDIDATES_FAILED` with the per-leg cause chain populated.
|
||||
|
||||
## Features
|
||||
|
||||
- Concurrency cap honouring `policy.maxParallel` and the candidate count.
|
||||
- Composed abort propagation via `anySignal([callerSignal, internal])`.
|
||||
- Per-candidate counter and `attempt-failed` / `attempt-succeeded` events.
|
||||
- Semantic failures surface immediately, halting the race.
|
||||
- Honours `policy.stopOnAbort` on pre-aborted caller signals.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { walkParallel } from "@suaveplan/storage-uri-plugin-fallback/walk";
|
||||
|
||||
const bytes = await walkParallel(
|
||||
{
|
||||
uri,
|
||||
candidates,
|
||||
ctx,
|
||||
policy: { ...policy, maxParallel: 3 },
|
||||
events,
|
||||
signal,
|
||||
},
|
||||
async (candidate, s) => resolver.read(candidate, s),
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { walkParallel } from "@suaveplan/storage-uri-plugin-fallback/walk";
|
||||
|
||||
await walkParallel(
|
||||
{ uri, candidates, ctx, policy, events, signal },
|
||||
async (candidate, s) => {
|
||||
const res = await fetch(candidate, { signal: s });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return new Uint8Array(await res.arrayBuffer());
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `walkParallel<T>(opts: ParallelWalkOptions, dispatch: Dispatch<T>): Promise<T>`
|
||||
- `ParallelWalkOptions` — `{ uri, candidates, ctx, policy, events, signal }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The walk uses an internal `AbortController` that fires as soon as one
|
||||
candidate resolves; pending candidates observing the composed signal
|
||||
abort cooperatively. Completion bookkeeping (`completed >= total`) is the
|
||||
only path that throws `ALL_CANDIDATES_FAILED` after exhausting the work
|
||||
queue, ensuring the structured error always carries one entry per
|
||||
attempted candidate.
|
||||
|
||||
This module is part of the universal fallback URI plugin family. It coordinates with the recursion, walk, ops, observability, and classification modules through small, narrowly-typed value contracts. The implementation deliberately keeps surface area small so each piece can be substituted or instrumented independently without touching consumers. Refer to the package README for the end-to-end picture and to the spec delta for the requirement matrix this module helps satisfy.
|
||||
@@ -0,0 +1,70 @@
|
||||
# walk/sequential
|
||||
|
||||
## Purpose
|
||||
|
||||
The `walk/sequential` module implements the default ordered
|
||||
fall-through walk strategy. It iterates the candidate list in declaration
|
||||
order, dispatches each candidate via a caller-supplied dispatcher, and
|
||||
returns the first successful result. Transient failures fall through to
|
||||
the next candidate; semantic failures surface immediately; aborts cut the
|
||||
walk per policy. On exhaustion it throws `ALL_CANDIDATES_FAILED` with
|
||||
every per-leg error preserved, or `CIRCULAR_REFERENCE` when every
|
||||
candidate cycles back to an already-visited URI.
|
||||
|
||||
## Features
|
||||
|
||||
- Ordered fall-through with `EventBusLike` `attempt-failed` /
|
||||
`attempt-succeeded` emissions per candidate.
|
||||
- Cycle skipping via `hasVisited` + `cycle-detected` event emission.
|
||||
- Per-candidate OTel counter increments (`fallback_total`).
|
||||
- Foreign throwable wrapping into structured `FallbackUriError`.
|
||||
- `decorateRecursive` injects the recursion-context id into nested
|
||||
`fallback://` candidates so re-entries see the parent context.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { walkSequential } from "@suaveplan/storage-uri-plugin-fallback/walk";
|
||||
|
||||
const value = await walkSequential(
|
||||
{
|
||||
uri,
|
||||
candidates,
|
||||
ctx,
|
||||
policy,
|
||||
events,
|
||||
signal,
|
||||
},
|
||||
async (candidate, s) => resolver.read(candidate, s),
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
candidateScheme,
|
||||
decorateRecursive,
|
||||
walkSequential,
|
||||
} from "@suaveplan/storage-uri-plugin-fallback/walk";
|
||||
|
||||
const decorated = candidates.map((c) => decorateRecursive(c, ctx));
|
||||
console.log(candidateScheme(decorated[0]));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `walkSequential<T>(opts: SequentialWalkOptions, dispatch: Dispatch<T>): Promise<T>`
|
||||
- `SequentialWalkOptions` — `{ uri, candidates, ctx, policy, events, signal }`.
|
||||
- `Dispatch<T>` — `(candidateUri, signal) => Promise<T>`.
|
||||
- `candidateScheme(uri)` — extracts the lowercased scheme.
|
||||
- `decorateRecursive(candidate, ctx)` — appends `__fbctx=<ctx.id>` to
|
||||
`fallback://` candidates.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The walk respects `policy.stopOnAbort`: when `true` (default), an aborted
|
||||
caller signal is checked before each candidate and throws
|
||||
`FALLBACK_URI.ABORTED` immediately. The recorded counter outcome for
|
||||
cycle-skipped candidates is `"semantic"` to align with the classifier's
|
||||
treatment of cycle detection as a non-transient surface.
|
||||
Reference in New Issue
Block a user