147 lines
6.6 KiB
Markdown
147 lines
6.6 KiB
Markdown
# @suaveplan/storage-uri - Universal URI-routed dispatch over @suaveplan StorageLike adapters. One resolver, one URI string in, one stream/byte/metadata out; scheme→plugin registry, AbortSignal everywhere, full OTel surface, zero I/O of its own.
|
|
|
|
Universal URI-routed dispatch over `@suaveplan` `StorageLike` adapters.
|
|
|
|
One resolver, one URI string in, one stream/byte/metadata out.
|
|
Scheme→plugin registry; the resolver performs no I/O of its own — every
|
|
operation delegates to a registered `StorageLike` after URI parsing.
|
|
|
|
<!-- begin:auto-docs -->
|
|
## Documentation tree
|
|
|
|
- [conformance](./src/conformance/conformance.md) — `runUriPluginConformance(plugin, fixtures)` is the test harness adapter packages call from their own suites to prove th…
|
|
- [conformance](./src/conformance/conformance.md) — `runUriPluginConformance(plugin, fixtures)` is the test harness adapter packages call from their own suites to prove th…
|
|
- [error-codes](./src/error-codes/error-codes.md) — Defines the canonical `STORAGE_URI_CODES` registry and the `StorageUriError` class for the `@suaveplan/storage-uri` pac…
|
|
- [error-codes](./src/error-codes/error-codes.md) — Defines the canonical `STORAGE_URI_CODES` registry and the `StorageUriError` class for the `@suaveplan/storage-uri` pac…
|
|
- [otel](./src/otel/otel.md) — Owns the OpenTelemetry instrument bag for `@suaveplan/storage-uri`: meter, tracer, and the six counter + one histogram…
|
|
- [otel](./src/otel/otel.md) — Owns the OpenTelemetry instrument bag for `@suaveplan/storage-uri`: meter, tracer, and the six counter + one histogram…
|
|
- [parse-uri](./src/parse-uri/parse-uri.md) — `parseUri(uri)` is the RFC 3986 subset parser the resolver uses to extract a scheme, optional authority, path, and opti…
|
|
- [parse-uri](./src/parse-uri/parse-uri.md) — `parseUri(uri)` is the RFC 3986 subset parser the resolver uses to extract a scheme, optional authority, path, and opti…
|
|
- [resolver](./src/resolver/resolver.md) — The `StorageUriResolver` class is the entry point for URI-routed dispatch in `@suaveplan/storage-uri`. Constructed once…
|
|
- [resolver.list](./src/resolver/resolver.list.md) — Implements the `list(uri, options?, signal?)` semantics. Treats the input URI's key portion as a listing prefix, delega…
|
|
- [resolver](./src/resolver/resolver.md) — The `StorageUriResolver` class is the entry point for URI-routed dispatch in `@suaveplan/storage-uri`. Constructed once…
|
|
- [resolver.streams](./src/resolver/resolver.streams.md) — Implements the `open(uri, signal?)` semantics for the resolver: forward a native `createReadStream` when the adapter ex…
|
|
- [resolver.writes](./src/resolver/resolver.writes.md) — Implements the `write(uri, body, signal?)` semantics. Accepts either `Uint8Array` or `ReadableStream<Uint8Array>` for t…
|
|
<!-- end:auto-docs -->
|
|
|
|
## Install
|
|
|
|
```bash
|
|
bun add @suaveplan/storage-uri
|
|
```
|
|
|
|
Pair with one or more adapter packages — each ships its own `uriPlugin`:
|
|
|
|
```bash
|
|
bun add @suaveplan/storage-file @suaveplan/storage-http
|
|
```
|
|
|
|
## Modules
|
|
|
|
| Subpath | What |
|
|
|---|---|
|
|
| `@suaveplan/storage-uri` | Full surface — re-exports everything below |
|
|
| `@suaveplan/storage-uri/error-codes` | `STORAGE_URI_CODES`, `StorageUriError` |
|
|
| `@suaveplan/storage-uri/parse-uri` | `parseUri(uri)` RFC 3986 subset |
|
|
| `@suaveplan/storage-uri/resolver` | `StorageUriResolver`, `createStorageUriResolver` |
|
|
| `@suaveplan/storage-uri/otel` | OTel meter+tracer instruments |
|
|
| `@suaveplan/storage-uri/conformance` | `runUriPluginConformance` for adapter authors |
|
|
| `@suaveplan/storage-uri/types` | `UriPlugin`, `ParsedUri`, `StatResult`, list types |
|
|
|
|
## Quick start
|
|
|
|
```ts
|
|
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
|
import { fileUriPlugin } from "@suaveplan/storage-file";
|
|
import { httpsUriPlugin, createHttpStorage } from "@suaveplan/storage-http";
|
|
|
|
const resolver = createStorageUriResolver([
|
|
fileUriPlugin({ rootDir: "/var/cache/media" }),
|
|
httpsUriPlugin(createHttpStorage({ baseUrl: "https://cdn.example.com" })),
|
|
]);
|
|
|
|
const stream = await resolver.open("https://cdn.example.com/clip/master.m3u8");
|
|
await resolver.write("file:///cache/poster.jpg", thumbBytes);
|
|
```
|
|
|
|
## Cross-scheme media-pipeline recipe
|
|
|
|
```ts
|
|
const resolver = createStorageUriResolver([
|
|
fileUriPlugin({ rootDir: "/var/media" }),
|
|
httpsUriPlugin(httpStorage),
|
|
]);
|
|
|
|
async function thumbnail(srcUri: string, dstUri: string): Promise<void> {
|
|
const src = await resolver.open(srcUri);
|
|
const out = await encodeJpegFromStream(src);
|
|
await resolver.write(dstUri, out);
|
|
}
|
|
```
|
|
|
|
A single call site composes a filesystem cache, a CDN origin, and any other
|
|
adapter you register — the resolver hides which scheme served the request.
|
|
|
|
## Authoring a third-party `UriPlugin`
|
|
|
|
```ts
|
|
import type { UriPlugin } from "@suaveplan/storage-uri";
|
|
import type { StorageLike } from "@suaveplan/types";
|
|
|
|
export function memoryUriPlugin(adapter: StorageLike): UriPlugin {
|
|
return {
|
|
scheme: "memory",
|
|
adapter,
|
|
parse(uri) {
|
|
const url = new URL(uri);
|
|
return { key: url.pathname.replace(/^\//, ""), meta: { authority: url.host } };
|
|
},
|
|
build(key, meta) {
|
|
const auth = meta?.authority ?? "default";
|
|
return `memory://${auth}/${key}`;
|
|
},
|
|
};
|
|
}
|
|
```
|
|
|
|
Verify your plugin against the conformance harness from your own test
|
|
suite:
|
|
|
|
```ts
|
|
import { runUriPluginConformance } from "@suaveplan/storage-uri/conformance";
|
|
|
|
runUriPluginConformance(memoryUriPlugin(adapter), {
|
|
validUris: ["memory://x/a"],
|
|
invalidUris: ["bad", "memory:"],
|
|
buildRoundTrip: [{ key: "a", uri: "memory://x/a", meta: { authority: "x" } }],
|
|
});
|
|
```
|
|
|
|
## Error codes
|
|
|
|
| Code | When |
|
|
|---|---|
|
|
| `STORAGE_URI.UNKNOWN_SCHEME` | URI scheme has no plugin registered |
|
|
| `STORAGE_URI.INVALID_URI` | `parseUri` rejected the input |
|
|
| `STORAGE_URI.SCHEME_CONFLICT` | Two distinct plugins claim the same scheme |
|
|
| `STORAGE_URI.NOT_FOUND` | Adapter returned `undefined` for `get`/`open` |
|
|
| `STORAGE_URI.LIST_UNSUPPORTED` | Plugin has no `build()` |
|
|
| `STORAGE_URI.WRITE_UNSUPPORTED` | Adapter threw on `set` (e.g. read-only HTTP) |
|
|
| `STORAGE_URI.ABORTED` | `AbortSignal` fired |
|
|
| `STORAGE_URI.ADAPTER_ERROR` | Adapter threw something else — cause preserved |
|
|
|
|
## OpenTelemetry
|
|
|
|
Tracer + meter name: `@suaveplan/storage-uri`. Counters:
|
|
`reads_total`, `writes_total`, `deletes_total`, `errors_total`,
|
|
`stream_fallback_total`, `buffered_write_total`. Histogram:
|
|
`op_duration_ms{op,scheme,outcome}`. Spans on every public op with
|
|
`storage.uri`, `storage.uri.scheme`, `storage.uri.key` attributes;
|
|
`storage.uri` is redacted (query string + fragment stripped) so presigned
|
|
URLs cannot leak into telemetry sinks.
|
|
|
|
## License
|
|
|
|
MIT
|
|
|