first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.