188 lines
9.2 KiB
Markdown
188 lines
9.2 KiB
Markdown
# @suaveplan/storage-uri-plugin-fallback - Universal `fallback://` URI plugin for @suaveplan/storage-uri: ordered multi-source resolution with cycle detection, depth cap, mirror writes, list union, AbortSignal propagation, and OTel + EventBus observability.
|
|
|
|
Universal `fallback://` URI plugin for
|
|
[`@suaveplan/storage-uri`](https://github.com/SuavePlan/suaveplan/tree/main/packages/storage/storage-uri).
|
|
Ships ordered multi-source resolution with cycle detection, depth cap,
|
|
mirror writes, list union, AbortSignal propagation, and OTel +
|
|
EventBusLike observability.
|
|
|
|
<!-- begin:auto-docs -->
|
|
## Documentation tree
|
|
|
|
- [classify](./src/classify/classify.md) — The `classify` module hosts the pure error-classification function that governs fall-through eligibility for every walk…
|
|
- [classify](./src/classify/classify.md) — The `classify` module hosts the pure error-classification function that governs fall-through eligibility for every walk…
|
|
- [conformance](./src/conformance/fallback-conformance.md) — The `fallback-conformance` module exports `runFallbackConformance`, a black-box harness consumers call from their own i…
|
|
- [fallback-conformance](./src/conformance/fallback-conformance.md) — The `fallback-conformance` module exports `runFallbackConformance`, a black-box harness consumers call from their own i…
|
|
- [error-codes](./src/error-codes/error-codes.md) — The `error-codes` module hosts the structured-error registry for the `@suaveplan/storage-uri-plugin-fallback` package.…
|
|
- [error-codes](./src/error-codes/error-codes.md) — The `error-codes` module hosts the structured-error registry for the `@suaveplan/storage-uri-plugin-fallback` package.…
|
|
- [events](./src/observability/events.md) — The `observability/events` module exposes thin emit helpers that forward the package's five canonical event topics to a…
|
|
- [otel](./src/observability/otel.md) — The `observability/otel` module owns the OpenTelemetry instrument bundle for the package: the per-candidate `storage_ur…
|
|
- [delete](./src/ops/delete.md) — The `ops/delete` module implements `fallbackDelete`, the mirror-aware removal operation. It dispatches `remove` to ever…
|
|
- [list](./src/ops/list.md) — The `ops/list` module implements `fallbackList`, the enumeration operation for `fallback://` URIs. It invokes `list` on…
|
|
- [metadata](./src/ops/metadata.md) — The `ops/metadata` module hosts `fallbackExists` and `fallbackStat`, the metadata-side adapter operations. Both walk th…
|
|
- [read](./src/ops/read.md) — The `ops/read` module implements the read-side of the fallback adapter surface. It wires the `fallback://` URI parser,…
|
|
- [write](./src/ops/write.md) — The `ops/write` module implements the write-side semantics of the fallback adapter: `fallbackSet` for byte writes and `…
|
|
- [abort-any](./src/parse-uri/abort-any.md) — The `abort-any` module provides a universal `AbortSignal` composition helper. It prefers the native `AbortSignal.a…
|
|
- [parse-fallback-uri](./src/parse-uri/parse-fallback-uri.md) — The `parse-fallback-uri` module owns the canonical grammar for the `fallback://` URI scheme. It parses input strings in…
|
|
- [plugin](./src/plugin/plugin.md) — The `plugin` module hosts the `fallbackUriPlugin` factory — the public entry point of the package. The factory validate…
|
|
- [plugin](./src/plugin/plugin.md) — The `plugin` module hosts the `fallbackUriPlugin` factory — the public entry point of the package. The factory validate…
|
|
- [recursion](./src/recursion/depth-context.md) — The `depth-context` module tracks the recursion state threaded through nested `fallback://` dispatches: the per-call de…
|
|
- [depth-context](./src/recursion/depth-context.md) — The `depth-context` module tracks the recursion state threaded through nested `fallback://` dispatches: the per-call de…
|
|
- [parallel](./src/walk/parallel.md) — The `walk/parallel` module implements the `Promise.any`-style race strategy activated when `policy.maxParallel > 1`. It…
|
|
- [sequential](./src/walk/sequential.md) — The `walk/sequential` module implements the default ordered fall-through walk strategy. It iterates the candidate list…
|
|
<!-- end:auto-docs -->
|
|
|
|
## Install
|
|
|
|
```bash
|
|
bun add @suaveplan/storage-uri-plugin-fallback
|
|
```
|
|
|
|
## URI grammar
|
|
|
|
```text
|
|
fallback://?u=<pct-encoded-uri>&u=<pct-encoded-uri>[&u=...]
|
|
```
|
|
|
|
- Always empty authority and empty path.
|
|
- One or more ordered `u=` parameters.
|
|
- Encoding via `URLSearchParams` (RFC 3986-compliant).
|
|
- Default `maxCandidates` is 16; hard ceiling 64.
|
|
- Each candidate may itself be a `fallback://` URI (nested up to
|
|
`policy.maxDepth`, default 4 / hard ceiling 8).
|
|
|
|
Use the bundled `buildFallbackUri` helper rather than handcrafting:
|
|
|
|
```ts
|
|
import { buildFallbackUri } from "@suaveplan/storage-uri-plugin-fallback";
|
|
|
|
const uri = buildFallbackUri([
|
|
"cdn://zone-eu/avatars/alice.jpg",
|
|
"drive://primary/avatars/alice.jpg",
|
|
]);
|
|
```
|
|
|
|
## Simple example
|
|
|
|
```ts
|
|
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
|
import {
|
|
buildFallbackUri,
|
|
fallbackUriPlugin,
|
|
} from "@suaveplan/storage-uri-plugin-fallback";
|
|
|
|
// Late-bind the resolver so the fallback plugin can recurse through it.
|
|
let resolver: ReturnType<typeof createStorageUriResolver> | undefined;
|
|
const lazy = new Proxy({}, {
|
|
get(_t, prop) {
|
|
const v = (resolver as unknown as Record<string, unknown>)[prop as string];
|
|
return typeof v === "function" ? v.bind(resolver) : v;
|
|
},
|
|
}) as ReturnType<typeof createStorageUriResolver>;
|
|
|
|
resolver = createStorageUriResolver([
|
|
cdnPlugin,
|
|
drivePlugin,
|
|
fallbackUriPlugin({ resolver: lazy }),
|
|
]);
|
|
|
|
const uri = buildFallbackUri([
|
|
"cdn://zone-eu/avatars/alice.jpg",
|
|
"drive://primary/avatars/alice.jpg",
|
|
]);
|
|
const bytes = await resolver.read(uri);
|
|
```
|
|
|
|
## Advanced example — mirror writes + dedup list
|
|
|
|
```ts
|
|
import { createEventBus } from "@suaveplan/events";
|
|
import {
|
|
buildFallbackUri,
|
|
fallbackUriPlugin,
|
|
type FallbackEventMap,
|
|
} from "@suaveplan/storage-uri-plugin-fallback";
|
|
|
|
const bus = createEventBus<FallbackEventMap>();
|
|
bus.on("storage-uri.fallback.write-mirror-partial", (p) => {
|
|
console.warn("mirror drift:", p);
|
|
});
|
|
|
|
const plugin = fallbackUriPlugin({
|
|
resolver,
|
|
policy: {
|
|
writeMode: "all",
|
|
listFailureMode: "partial",
|
|
maxParallel: 2,
|
|
},
|
|
events: bus,
|
|
});
|
|
|
|
const uri = buildFallbackUri([
|
|
"cdn://zone-eu/photos/2026/",
|
|
"drive://primary/photos/2026/",
|
|
]);
|
|
await resolver.write(uri, photoBytes); // mirrors to both
|
|
const { uris } = await resolver.list(uri); // union, deduped
|
|
```
|
|
|
|
## Policy reference
|
|
|
|
| Knob | Default | Notes |
|
|
|---|---|---|
|
|
| `errorClass` | `"transient"` | `"any"` re-classifies semantic errors as fall-through |
|
|
| `maxParallel` | `1` | `>1` enables `Promise.any`-style race |
|
|
| `stopOnAbort` | `true` | `false` lets the current candidate finish before throwing `ABORTED` |
|
|
| `writeMode` | `"all"` | `"first-success"` returns on first ok + background remainder |
|
|
| `deleteMode` | `"any-success"` | `"all"` raises `WRITE_MIRROR_FAILED` on any reject |
|
|
| `listMode` | `"union"` | `"concat"` returns raw concatenation |
|
|
| `listFailureMode` | `"throw"` | `"partial"` skips failed legs |
|
|
| `maxDepth` | `4` | Hard ceiling 8 |
|
|
| `maxCandidates` | `16` | Hard ceiling 64 |
|
|
|
|
## Error codes
|
|
|
|
`FALLBACK_URI.NO_CANDIDATES`, `INVALID_URI`, `ALL_CANDIDATES_FAILED`,
|
|
`WRITE_MIRROR_FAILED`, `LIST_UNION_FAILED`, `ABORTED`,
|
|
`MAX_DEPTH_EXCEEDED`, `CIRCULAR_REFERENCE` — every code is reachable
|
|
from a real call site exercised by the test suite.
|
|
|
|
## Complementarity with `@suaveplan/storage-mux`
|
|
|
|
`storage-mux` composes pre-constructed `StorageLike` backends at the
|
|
adapter layer; `storage-uri-plugin-fallback` composes URI strings at the
|
|
scheme layer. They share a classification table so transient/semantic/
|
|
abort semantics line up across the stack. Use `storage-mux` when you
|
|
own the backend instances; use `fallback://` when callsites receive
|
|
URIs from manifests, config, or the wire.
|
|
|
|
## Modules
|
|
|
|
- [`error-codes`](./src/error-codes/error-codes.md) — `FALLBACK_URI.*`
|
|
registry + `FallbackUriError`.
|
|
- [`types/policy.types`](./src/types/policy.types.md) — `FallbackPolicy`,
|
|
`FallbackPluginOptions`, event payloads.
|
|
- [`types/candidate.types`](./src/types/candidate.types.md) —
|
|
`CandidateError`, `CandidateAttempt`, `MirrorOutcome`.
|
|
- [`parse-uri/parse-fallback-uri`](./src/parse-uri/parse-fallback-uri.md)
|
|
— grammar, parser, builder.
|
|
- [`parse-uri/abort-any`](./src/parse-uri/abort-any.md) —
|
|
`AbortSignal.any` shim.
|
|
- [`classify/classify`](./src/classify/classify.md) — error class table.
|
|
- [`recursion/depth-context`](./src/recursion/depth-context.md) — depth +
|
|
visited set + registry.
|
|
- [`walk/sequential`](./src/walk/sequential.md) — ordered fall-through.
|
|
- [`walk/parallel`](./src/walk/parallel.md) — race + abort.
|
|
- [`ops/read`](./src/ops/read.md),
|
|
[`write`](./src/ops/write.md),
|
|
[`metadata`](./src/ops/metadata.md),
|
|
[`delete`](./src/ops/delete.md),
|
|
[`list`](./src/ops/list.md) — per-operation surfaces.
|
|
- [`observability/events`](./src/observability/events.md) — `EventBusLike`
|
|
emit helpers.
|
|
- [`observability/otel`](./src/observability/otel.md) — OTel instruments.
|
|
- [`plugin/plugin`](./src/plugin/plugin.md) — `fallbackUriPlugin` factory.
|
|
- [`conformance/fallback-conformance`](./src/conformance/fallback-conformance.md)
|
|
— `runFallbackConformance` consumer harness.
|
|
|