Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
# @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

View File

@@ -0,0 +1,124 @@
{
"name": "@suaveplan/storage-uri",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"description": "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.",
"license": "MIT",
"author": "Suaveplan Team",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./error-codes": {
"import": {
"types": "./dist/error-codes/index.d.ts",
"default": "./dist/error-codes/error-codes.js"
},
"require": {
"types": "./dist/error-codes/index.d.ts",
"default": "./dist/error-codes/error-codes.js"
}
},
"./parse-uri": {
"import": {
"types": "./dist/parse-uri/index.d.ts",
"default": "./dist/parse-uri/parse-uri.js"
},
"require": {
"types": "./dist/parse-uri/index.d.ts",
"default": "./dist/parse-uri/parse-uri.js"
}
},
"./resolver": {
"import": {
"types": "./dist/resolver/index.d.ts",
"default": "./dist/resolver/resolver.js"
},
"require": {
"types": "./dist/resolver/index.d.ts",
"default": "./dist/resolver/resolver.js"
}
},
"./otel": {
"import": {
"types": "./dist/otel/index.d.ts",
"default": "./dist/otel/otel.js"
},
"require": {
"types": "./dist/otel/index.d.ts",
"default": "./dist/otel/otel.js"
}
},
"./conformance": {
"import": {
"types": "./dist/conformance/index.d.ts",
"default": "./dist/conformance/conformance.js"
},
"require": {
"types": "./dist/conformance/index.d.ts",
"default": "./dist/conformance/conformance.js"
}
},
"./types": {
"import": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
},
"require": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
}
}
},
"scripts": {
"build": "vite build",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage --passWithNoTests",
"lint": "bunx biome check src/",
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
"format": "bunx biome format --write --max-diagnostics 500 src/",
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
},
"dependencies": {
"@opentelemetry/api": "catalog:observability",
"@suaveplan/error": "workspace:^",
"@suaveplan/types": "workspace:^",
"tslib": "catalog:build"
},
"devDependencies": {
"@suaveplan/storage-file": "workspace:^",
"@suaveplan/storage-http": "workspace:^",
"@suaveplan/testing": "workspace:^",
"@types/node": "catalog:",
"@vitest/coverage-istanbul": "catalog:testing",
"fast-check": "catalog:testing",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
},
"publishConfig": {
"registry": "https://npmjs.fq.io/",
"access": "public"
},
"files": [
"dist",
"src",
"README.md"
]
}

View File

@@ -0,0 +1,72 @@
# conformance
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`runUriPluginConformance(plugin, fixtures)` is the test harness adapter
packages call from their own suites to prove their `uriPlugin` satisfies
the resolver's contract. It checks scheme alignment, parse success for the
valid fixture set, parse rejection (`STORAGE_URI.INVALID_URI`) for the
invalid set, and — when `build` is implemented — round-trip equality for
every entry in `buildRoundTrip`.
The harness throws `UriPluginConformanceFailure` (a `Error` subclass) on
the first failure rather than collecting them; tests typically wrap one
plugin per `describe` block so a single failure already names the offending
fixture.
## Features
- Scheme alignment check on every valid URI.
- Required-throw assertion on the invalid set.
- Optional round-trip assertion gated on `plugin.build`.
- Single-failure semantics with descriptive messages.
## Usage: Basic
```ts
import { runUriPluginConformance } from "@suaveplan/storage-uri/conformance";
runUriPluginConformance(memoryUriPlugin, {
validUris: ["memory://x/a", "memory://x/b"],
invalidUris: ["bare", "memory:"],
buildRoundTrip: [{ key: "a", uri: "memory://x/a", meta: { authority: "x" } }],
});
```
## Usage: Advanced
```ts
import { describe, it } from "@suaveplan/testing/runner";
import { runUriPluginConformance } from "@suaveplan/storage-uri/conformance";
import { fileUriPlugin } from "./uri-plugin.js";
describe("fileUriPlugin conformance", () => {
it("passes the storage-uri harness", { timeout: 5000 }, () => {
runUriPluginConformance(fileUriPlugin({ rootDir: "/tmp" }), {
validUris: ["file:///a", "file:///b/c"],
invalidUris: ["bad", "file:"],
buildRoundTrip: [{ key: "/a", uri: "file:///a" }],
});
});
});
```
## API Reference
- `interface UriPluginFixtures { validUris; invalidUris; buildRoundTrip? }`
- `class UriPluginConformanceFailure extends Error`
- `function runUriPluginConformance(plugin, fixtures): void`
## Implementation Notes
The harness re-uses `parseUri` from this package as a baseline scheme
extractor so plugins that bypass it still get checked against the canonical
RFC 3986 split. `plugin.build` is bound via `.bind(plugin)` for the same
reason the resolver does it — `this`-bound implementations need to survive
indirect invocation. On round-trip failure the message includes both the
input and the produced output, so the failing fixture is uniquely
identified without log-spelunking.

View File

@@ -0,0 +1,229 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { MemoryStorageLike } from "../../tests/setup/test-helpers.js";
import {
STORAGE_URI_CODES,
StorageUriError,
} from "../error-codes/error-codes.js";
import type { UriPlugin } from "../types/plugin.types.js";
import {
runUriPluginConformance,
UriPluginConformanceFailure,
} from "./conformance.js";
function memoryPlugin(adapter: MemoryStorageLike): UriPlugin {
return {
scheme: "memory",
adapter,
parse(uri) {
if (!uri.startsWith("memory://")) {
throw new StorageUriError(
STORAGE_URI_CODES.INVALID_URI,
"wrong scheme",
{ uri }
);
}
const rest = uri.slice("memory://".length);
const slash = rest.indexOf("/");
const authority = slash >= 0 ? rest.slice(0, slash) : rest;
const key = slash >= 0 ? rest.slice(slash + 1) : "";
if (key.length === 0) {
throw new StorageUriError(
STORAGE_URI_CODES.INVALID_URI,
"missing key",
{ uri }
);
}
return { key, meta: { authority } };
},
build(key, meta) {
const auth = meta?.["authority"] ?? "x";
return `memory://${auth}/${key}`;
},
};
}
describe("runUriPluginConformance", () => {
it("passes for a well-formed plugin", { timeout: 5000 }, () => {
const plugin = memoryPlugin(new MemoryStorageLike());
expect(() =>
runUriPluginConformance(plugin, {
validUris: ["memory://x/a", "memory://x/b/c"],
invalidUris: ["bare-string", "memory:"],
buildRoundTrip: [
{ key: "a", uri: "memory://x/a", meta: { authority: "x" } },
],
})
).not.toThrow();
});
it("fails on broken round-trip", { timeout: 5000 }, () => {
const adapter = new MemoryStorageLike();
const broken: UriPlugin = {
scheme: "memory",
adapter,
parse(uri) {
return { key: uri.slice("memory://".length) };
},
build(_key) {
return "memory://x/WRONG";
},
};
expect(() =>
runUriPluginConformance(broken, {
validUris: ["memory://x/a"],
invalidUris: [],
buildRoundTrip: [{ key: "a", uri: "memory://x/a" }],
})
).toThrow(UriPluginConformanceFailure);
});
it("fails when scheme is not a non-empty string", { timeout: 5000 }, () => {
const ad = new MemoryStorageLike();
const bad: UriPlugin = {
scheme: "",
adapter: ad,
parse: () => ({ key: "a" }),
};
expect(() =>
runUriPluginConformance(bad, { validUris: [], invalidUris: [] })
).toThrow();
});
it("fails when validUris[i] scheme does not match plugin scheme", {
timeout: 5000,
}, () => {
const plugin = memoryPlugin(new MemoryStorageLike());
expect(() =>
runUriPluginConformance(plugin, {
validUris: ["s3://x/a"],
invalidUris: [],
})
).toThrow();
});
it("fails when plugin.parse throws on a valid URI", {
timeout: 5000,
}, () => {
const ad = new MemoryStorageLike();
const bad: UriPlugin = {
scheme: "memory",
adapter: ad,
parse() {
throw new Error("oops");
},
};
expect(() =>
runUriPluginConformance(bad, {
validUris: ["memory://x/a"],
invalidUris: [],
})
).toThrow();
});
it("fails when plugin.parse returns empty key on valid URI", {
timeout: 5000,
}, () => {
const ad = new MemoryStorageLike();
const bad: UriPlugin = {
scheme: "memory",
adapter: ad,
parse() {
return { key: "" };
},
};
expect(() =>
runUriPluginConformance(bad, {
validUris: ["memory://x/a"],
invalidUris: [],
})
).toThrow();
});
it("fails when invalid URI does not throw", { timeout: 5000 }, () => {
const ad = new MemoryStorageLike();
const lax: UriPlugin = {
scheme: "memory",
adapter: ad,
parse() {
return { key: "x" };
},
};
expect(() =>
runUriPluginConformance(lax, {
validUris: [],
invalidUris: ["memory://x/a"],
})
).toThrow();
});
it("fails when invalid URI throws wrong code", { timeout: 5000 }, () => {
const ad = new MemoryStorageLike();
const wrong: UriPlugin = {
scheme: "memory",
adapter: ad,
parse(uri) {
throw new StorageUriError(
STORAGE_URI_CODES.NOT_FOUND,
"wrong",
{ uri }
);
},
};
expect(() =>
runUriPluginConformance(wrong, {
validUris: [],
invalidUris: ["memory://x/a"],
})
).toThrow(UriPluginConformanceFailure);
});
it("accepts an empty buildRoundTrip array", { timeout: 5000 }, () => {
const plugin = memoryPlugin(new MemoryStorageLike());
expect(() =>
runUriPluginConformance(plugin, {
validUris: ["memory://x/a"],
invalidUris: [],
buildRoundTrip: [],
})
).not.toThrow();
});
it("includes a stringified non-Error when plugin.parse throws a non-Error", {
timeout: 5000,
}, () => {
const ad = new MemoryStorageLike();
const bad: UriPlugin = {
scheme: "memory",
adapter: ad,
parse() {
throw "string-thrown";
},
};
expect(() =>
runUriPluginConformance(bad, {
validUris: ["memory://x/a"],
invalidUris: [],
})
).toThrow(UriPluginConformanceFailure);
});
it("fails when buildRoundTrip is supplied but plugin.build is missing", {
timeout: 5000,
}, () => {
const ad = new MemoryStorageLike();
const noBuild: UriPlugin = {
scheme: "memory",
adapter: ad,
parse() {
return { key: "a" };
},
};
expect(() =>
runUriPluginConformance(noBuild, {
validUris: [],
invalidUris: [],
buildRoundTrip: [{ key: "a", uri: "memory://x/a" }],
})
).toThrow();
});
});

View File

@@ -0,0 +1,115 @@
import {
STORAGE_URI_CODES,
StorageUriError,
} from "../error-codes/error-codes.js";
import { parseUri } from "../parse-uri/parse-uri.js";
import type { UriPlugin } from "../types/plugin.types.js";
/** Fixture set passed to `runUriPluginConformance`. */
export interface UriPluginFixtures {
/** URIs the plugin MUST parse without throwing. */
readonly validUris: readonly string[];
/** URIs the plugin MUST reject with `STORAGE_URI.INVALID_URI`. */
readonly invalidUris: readonly string[];
/** Round-trip cases — required only when `plugin.build` is implemented. */
readonly buildRoundTrip?: ReadonlyArray<{
readonly key: string;
readonly meta?: Record<string, string>;
readonly uri: string;
}>;
}
/** Failure produced by the conformance harness. */
export class UriPluginConformanceFailure extends Error {
public constructor(message: string) {
super(message);
this.name = "UriPluginConformanceFailure";
}
}
/**
* Verify that `plugin` satisfies the contract expected by the resolver.
*
* Adapter packages call this from their own test suite to prove their
* `uriPlugin` round-trips correctly, rejects malformed inputs, and matches
* the URI's scheme prefix. This is the bridge between the resolver's spec
* and each adapter's contribution — the test fixtures live with the
* adapter, but the assertions live here.
*/
export function runUriPluginConformance(
plugin: UriPlugin,
fixtures: UriPluginFixtures
): void {
if (typeof plugin.scheme !== "string" || plugin.scheme.length === 0) {
throw new UriPluginConformanceFailure(
"plugin.scheme must be a non-empty string"
);
}
const expectedScheme = plugin.scheme.toLowerCase();
for (const uri of fixtures.validUris) {
const parts = parseUri(uri);
if (parts.scheme !== expectedScheme) {
throw new UriPluginConformanceFailure(
`validUris[${uri}] scheme '${parts.scheme}' does not match plugin.scheme '${expectedScheme}'`
);
}
let parsed: ReturnType<typeof plugin.parse>;
try {
parsed = plugin.parse(uri);
} catch (err) {
throw new UriPluginConformanceFailure(
`plugin.parse threw on validUris[${uri}]: ${
err instanceof Error ? err.message : String(err)
}`
);
}
if (typeof parsed.key !== "string" || parsed.key.length === 0) {
throw new UriPluginConformanceFailure(
`plugin.parse(${uri}).key must be a non-empty string`
);
}
}
for (const uri of fixtures.invalidUris) {
let threw = false;
try {
plugin.parse(uri);
} catch (err) {
threw = true;
if (
err instanceof StorageUriError &&
err.code !== STORAGE_URI_CODES.INVALID_URI
) {
throw new UriPluginConformanceFailure(
`invalidUris[${uri}] threw ${err.code} (expected STORAGE_URI.INVALID_URI)`
);
}
}
if (!threw) {
throw new UriPluginConformanceFailure(
`invalidUris[${uri}] did not throw`
);
}
}
if (
fixtures.buildRoundTrip !== undefined &&
fixtures.buildRoundTrip.length > 0
) {
if (typeof plugin.build !== "function") {
throw new UriPluginConformanceFailure(
"buildRoundTrip fixtures supplied but plugin.build is not implemented"
);
}
const build = plugin.build.bind(plugin);
for (const fx of fixtures.buildRoundTrip) {
const got = build(fx.key, fx.meta);
if (got !== fx.uri) {
throw new UriPluginConformanceFailure(
`build(${fx.key}, ${JSON.stringify(fx.meta)}) returned '${got}', expected '${fx.uri}'`
);
}
}
}
}

View File

@@ -0,0 +1,5 @@
export type { UriPluginFixtures } from "./conformance.js";
export {
runUriPluginConformance,
UriPluginConformanceFailure,
} from "./conformance.js";

View File

@@ -0,0 +1,82 @@
# error-codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the canonical `STORAGE_URI_CODES` registry and the `StorageUriError`
class for the `@suaveplan/storage-uri` package. Every error surfaced by the
resolver — whether raised directly or re-wrapped from a downstream adapter —
carries one of these eight codes and the `uri`/`scheme` discriminants so
operators can correlate failures to the offending request without parsing
free-form message text.
The registry is built via `defineCodes("STORAGE_URI", [...] as const)` from
`@suaveplan/error`, which produces the runtime strings
`STORAGE_URI.UNKNOWN_SCHEME`, `STORAGE_URI.INVALID_URI`,
`STORAGE_URI.SCHEME_CONFLICT`, `STORAGE_URI.NOT_FOUND`,
`STORAGE_URI.LIST_UNSUPPORTED`, `STORAGE_URI.WRITE_UNSUPPORTED`,
`STORAGE_URI.ABORTED`, and `STORAGE_URI.ADAPTER_ERROR`.
## Features
- Frozen 8-code registry, type-narrowing `instanceof` via the
literal-typed `TCode` parameter on `SuaveplanError`.
- `uri` and `scheme` discriminants on every `StorageUriError` instance.
- `cause` chain preserved when the resolver re-wraps an adapter throwable as
`ADAPTER_ERROR` or `WRITE_UNSUPPORTED`.
- Zero runtime dependencies beyond `@suaveplan/error`.
## Usage: Basic
```ts
import { STORAGE_URI_CODES, StorageUriError } from "@suaveplan/storage-uri/error-codes";
throw new StorageUriError(
STORAGE_URI_CODES.UNKNOWN_SCHEME,
"no plugin for ftp",
{ uri: "ftp://x/y", scheme: "ftp" },
);
```
## Usage: Advanced
```ts
try {
await resolver.read("file:///missing");
} catch (err) {
if (err instanceof StorageUriError) {
if (err.code === STORAGE_URI_CODES.NOT_FOUND) return undefined;
if (err.code === STORAGE_URI_CODES.ADAPTER_ERROR) {
// Trace back to the original adapter code via the cause chain.
console.error("upstream failure", err.cause);
}
}
throw err;
}
```
## API Reference
- `STORAGE_URI_CODES` — frozen record `{ UNKNOWN_SCHEME, INVALID_URI,
SCHEME_CONFLICT, NOT_FOUND, LIST_UNSUPPORTED, WRITE_UNSUPPORTED, ABORTED,
ADAPTER_ERROR }`. Each value is the fully-qualified `STORAGE_URI.<REASON>`
string.
- `type StorageUriErrorCode` — union of every value in the registry.
- `interface StorageUriErrorOptions extends SuaveplanErrorOptions` — adds
`uri?: string` and `scheme?: string`.
- `class StorageUriError extends SuaveplanError<StorageUriErrorCode>` —
exposes `readonly uri`, `readonly scheme`, inherits `code`, `cause`,
`context`, `traceId`, `timestamp` from the base class.
## Implementation Notes
`defineCodes` returns a frozen object via `Object.create(null)` so the
registry has no prototype-pollution surface. `StorageUriError`'s constructor
mirrors `StorageHttpError` and `StorageFileError` — `uri`/`scheme` are
captured by direct field assignment rather than through a context map so
they survive structured-clone serialisation. The `name` is set to
`"StorageUriError"` explicitly so `err.name` is stable across V8/JSC/SM
where `new.target.name` could otherwise be munged by minification.

View File

@@ -0,0 +1,61 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { STORAGE_URI_CODES, StorageUriError } from "./error-codes.js";
describe("STORAGE_URI_CODES", () => {
it("contains every documented code with the STORAGE_URI. prefix", {
timeout: 5000,
}, () => {
const expected = [
"UNKNOWN_SCHEME",
"INVALID_URI",
"SCHEME_CONFLICT",
"NOT_FOUND",
"LIST_UNSUPPORTED",
"WRITE_UNSUPPORTED",
"ABORTED",
"ADAPTER_ERROR",
] as const;
for (const key of expected) {
expect(STORAGE_URI_CODES[key]).toBe(`STORAGE_URI.${key}`);
}
});
});
describe("StorageUriError", () => {
it("carries uri and scheme discriminants", { timeout: 5000 }, () => {
const err = new StorageUriError(
STORAGE_URI_CODES.NOT_FOUND,
"missing",
{
uri: "file:///a",
scheme: "file",
}
);
expect(err.uri).toBe("file:///a");
expect(err.scheme).toBe("file");
expect(err.name).toBe("StorageUriError");
expect(err.code).toBe(STORAGE_URI_CODES.NOT_FOUND);
});
it("leaves uri/scheme undefined when options omitted", {
timeout: 5000,
}, () => {
const err = new StorageUriError(STORAGE_URI_CODES.ABORTED, "aborted");
expect(err.uri).toBeUndefined();
expect(err.scheme).toBeUndefined();
});
it("preserves cause via the base class", { timeout: 5000 }, () => {
const original = new Error("disk fault");
const err = new StorageUriError(
STORAGE_URI_CODES.ADAPTER_ERROR,
"wrap",
{
uri: "file:///a",
scheme: "file",
cause: original,
}
);
expect(err.cause).toBe(original);
});
});

View File

@@ -0,0 +1,57 @@
import {
defineCodes,
SuaveplanError,
type SuaveplanErrorOptions,
} from "@suaveplan/error";
/**
* Registry of every error code raised by `@suaveplan/storage-uri`.
*
* Every member is the fully-qualified string `STORAGE_URI.<REASON>` produced
* by `defineCodes`. Eight codes total — `UNKNOWN_SCHEME`, `INVALID_URI`,
* `SCHEME_CONFLICT`, `NOT_FOUND`, `LIST_UNSUPPORTED`, `WRITE_UNSUPPORTED`,
* `ABORTED`, `ADAPTER_ERROR` — all reachable from a real call site exercised
* by the test suite.
*/
export const STORAGE_URI_CODES = defineCodes("STORAGE_URI", [
"UNKNOWN_SCHEME",
"INVALID_URI",
"SCHEME_CONFLICT",
"NOT_FOUND",
"LIST_UNSUPPORTED",
"WRITE_UNSUPPORTED",
"ABORTED",
"ADAPTER_ERROR",
] as const);
export type StorageUriErrorCode =
(typeof STORAGE_URI_CODES)[keyof typeof STORAGE_URI_CODES];
export interface StorageUriErrorOptions extends SuaveplanErrorOptions {
uri?: string;
scheme?: string;
}
/**
* Structured error for URI-routed storage operations.
*
* Carries optional `uri` and `scheme` discriminants so operators can correlate
* a failure with the offending request without parsing message text. The
* original underlying cause (when re-wrapping an adapter throwable) is
* preserved on `cause` via the base class.
*/
export class StorageUriError extends SuaveplanError<StorageUriErrorCode> {
public readonly uri: string | undefined;
public readonly scheme: string | undefined;
public constructor(
code: StorageUriErrorCode,
message: string,
options?: StorageUriErrorOptions
) {
super(code, message, options);
this.uri = options?.uri;
this.scheme = options?.scheme;
this.name = "StorageUriError";
}
}

View File

@@ -0,0 +1,8 @@
export type {
StorageUriErrorCode,
StorageUriErrorOptions,
} from "./error-codes.js";
export {
STORAGE_URI_CODES,
StorageUriError,
} from "./error-codes.js";

View File

@@ -0,0 +1,46 @@
// Side-effect import: ensures the STORAGE_URI.* code registry registers
// even when subpath consumers never reach this barrel.
import "./error-codes/error-codes.js";
export {
runUriPluginConformance,
UriPluginConformanceFailure,
type UriPluginFixtures,
} from "./conformance/conformance.js";
export {
STORAGE_URI_CODES,
StorageUriError,
type StorageUriErrorCode,
type StorageUriErrorOptions,
} from "./error-codes/error-codes.js";
export {
getInstruments,
type RecordOpInput,
recordBufferedWrite,
recordOp,
recordStreamFallback,
redactUri,
resetInstruments,
type StorageUriInstruments,
type StorageUriOp,
type StorageUriOutcome,
setStorageUriMeterProvider,
} from "./otel/otel.js";
export { type ParsedUriParts, parseUri } from "./parse-uri/parse-uri.js";
export {
createStorageUriResolver,
StorageUriResolver,
} from "./resolver/resolver.js";
export { listViaPlugin } from "./resolver/resolver.list.js";
export { openViaPlugin } from "./resolver/resolver.streams.js";
export { writeViaPlugin } from "./resolver/resolver.writes.js";
export type {
ParsedUri,
StatResult,
StreamingStorageExtensions,
UriPlugin,
} from "./types/plugin.types.js";
export type {
ListUriOptions,
ListUriResult,
} from "./types/resolver.types.js";

View File

@@ -0,0 +1,15 @@
export type {
RecordOpInput,
StorageUriInstruments,
StorageUriOp,
StorageUriOutcome,
} from "./otel.js";
export {
getInstruments,
recordBufferedWrite,
recordOp,
recordStreamFallback,
redactUri,
resetInstruments,
setStorageUriMeterProvider,
} from "./otel.js";

View File

@@ -0,0 +1,70 @@
# otel
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Owns the OpenTelemetry instrument bag for `@suaveplan/storage-uri`: meter,
tracer, and the six counter + one histogram metrics emitted on every
resolver operation. The module is intentionally side-effect-free at import
time; instruments are created lazily on first access so tests can swap in
an in-memory `MeterProvider` before any measurement is recorded.
The same module also owns the URI redaction helper `redactUri(uri)` used by
`StorageUriResolver.#run` to strip query strings and fragments from the
`storage.uri` span attribute before emission — presigned-URL secrets must
never leak into telemetry sinks.
## Features
- Lazy instrument cache rebuilt automatically when the meter provider
changes via `setStorageUriMeterProvider(provider)`.
- Six counters: `reads_total`, `writes_total`, `deletes_total`,
`errors_total`, `stream_fallback_total`, `buffered_write_total`.
- One histogram: `op_duration_ms` with `{op, scheme, outcome}` attributes.
- `redactUri(uri)` — drops `?query` and `#fragment` segments.
## Usage: Basic
```ts
import { recordOp } from "@suaveplan/storage-uri/otel";
recordOp({ op: "read", scheme: "file", outcome: "success", durationMs: 12 });
```
## Usage: Advanced
```ts
import { metrics } from "@opentelemetry/api";
import { setStorageUriMeterProvider, resetInstruments } from "@suaveplan/storage-uri/otel";
const provider = new MyInMemoryMeterProvider();
setStorageUriMeterProvider(provider);
resetInstruments();
// ... run resolver ops ...
const snapshot = provider.collect();
```
## API Reference
- `setStorageUriMeterProvider(provider | undefined): void`
- `getInstruments(force?: boolean): StorageUriInstruments`
- `resetInstruments(): void`
- `recordOp({ op, scheme, outcome, durationMs, code? }): void`
- `recordStreamFallback(scheme: string): void`
- `recordBufferedWrite(scheme: string): void`
- `redactUri(uri: string): string`
- Types: `StorageUriOp`, `StorageUriOutcome`, `RecordOpInput`,
`StorageUriInstruments`.
## Implementation Notes
`recordOp` routes the success counter by op category (`read`/`open`/`stat`/
`exists`/`list` → reads, `write` → writes, `remove` → deletes) so consumers
need not subscribe to seven separate counters. Errors always increment
`errors_total` with the resolved `STORAGE_URI.*` code on the `code`
attribute. The instrument cache is intentionally module-local rather than
class-bound — the resolver is constructed many times in tests, but the
instrument bag is provider-scoped, not resolver-scoped.

View File

@@ -0,0 +1,235 @@
import type {
Counter,
Histogram,
Meter,
MeterProvider,
} from "@opentelemetry/api";
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import { MemoryStorageLike } from "../../tests/setup/test-helpers.js";
import { createStorageUriResolver } from "../resolver/resolver.js";
import type { UriPlugin } from "../types/plugin.types.js";
import {
recordBufferedWrite,
recordOp,
recordStreamFallback,
redactUri,
resetInstruments,
setStorageUriMeterProvider,
} from "./otel.js";
interface Sample {
value: number;
attrs: Record<string, unknown> | undefined;
}
class StubCounter implements Counter {
public adds: Sample[] = [];
public add(v: number, attrs?: Record<string, unknown>): void {
this.adds.push({ value: v, attrs });
}
}
class StubHistogram implements Histogram {
public samples: Sample[] = [];
public record(v: number, attrs?: Record<string, unknown>): void {
this.samples.push({ value: v, attrs });
}
}
class StubMeter implements Meter {
public counters: Map<string, StubCounter> = new Map();
public histograms: Map<string, StubHistogram> = new Map();
public createCounter(name: string): Counter {
const c = new StubCounter();
this.counters.set(name, c);
return c;
}
public createHistogram(name: string): Histogram {
const h = new StubHistogram();
this.histograms.set(name, h);
return h;
}
public createUpDownCounter(): never {
throw new Error("unused");
}
public createObservableCounter(): never {
throw new Error("unused");
}
public createObservableUpDownCounter(): never {
throw new Error("unused");
}
public createObservableGauge(): never {
throw new Error("unused");
}
public addBatchObservableCallback(): void {}
public removeBatchObservableCallback(): void {}
public createGauge(): never {
throw new Error("unused");
}
}
class StubMeterProvider implements MeterProvider {
public meter = new StubMeter();
public getMeter(): Meter {
return this.meter;
}
}
function plugin(scheme: string, ad: MemoryStorageLike): UriPlugin {
return {
scheme,
adapter: ad,
parse(uri) {
const after = uri.slice(uri.indexOf("://") + 3);
const slash = after.indexOf("/");
return { key: slash >= 0 ? after.slice(slash + 1) : "" };
},
};
}
describe("storage-uri otel", () => {
let provider: StubMeterProvider;
beforeEach(() => {
provider = new StubMeterProvider();
setStorageUriMeterProvider(provider);
resetInstruments();
});
afterEach(() => {
setStorageUriMeterProvider(undefined);
resetInstruments();
});
it("records duration histogram on success", { timeout: 5000 }, async () => {
const ad = new MemoryStorageLike();
await ad.set("a", new Uint8Array([1]));
const r = createStorageUriResolver([plugin("file", ad)]);
await r.read("file://h/a");
const dur = provider.meter.histograms.get("storage_uri.op_duration_ms");
expect(dur?.samples.length).toBe(1);
expect(dur?.samples[0]?.attrs).toMatchObject({
op: "read",
scheme: "file",
outcome: "success",
});
const reads = provider.meter.counters.get("storage_uri.reads_total");
expect(reads?.adds.length).toBe(1);
});
it("increments errors_total with resolved code", {
timeout: 5000,
}, async () => {
const r = createStorageUriResolver([]);
await expect(r.read("ftp://x/y")).rejects.toBeDefined();
const errs = provider.meter.counters.get("storage_uri.errors_total");
expect(errs?.adds.length).toBe(1);
expect(errs?.adds[0]?.attrs).toMatchObject({
scheme: "ftp",
code: "STORAGE_URI.UNKNOWN_SCHEME",
});
});
it("strips query string from span attributes", { timeout: 5000 }, () => {
expect(redactUri("https://host/path?token=secret")).toBe(
"https://host/path"
);
expect(redactUri("file:///a#frag")).toBe("file:///a");
expect(redactUri("s3://x/y")).toBe("s3://x/y");
});
it("recordOp routes write/remove counters by op category", {
timeout: 5000,
}, () => {
recordOp({
op: "write",
scheme: "file",
outcome: "success",
durationMs: 1,
});
recordOp({
op: "remove",
scheme: "file",
outcome: "success",
durationMs: 1,
});
expect(
provider.meter.counters.get("storage_uri.writes_total")?.adds.length
).toBe(1);
expect(
provider.meter.counters.get("storage_uri.deletes_total")?.adds
.length
).toBe(1);
});
it("recordOp routes every read-class op to reads_total", {
timeout: 5000,
}, () => {
for (const op of ["read", "open", "stat", "exists", "list"] as const) {
recordOp({ op, scheme: "x", outcome: "success", durationMs: 1 });
}
expect(
provider.meter.counters.get("storage_uri.reads_total")?.adds.length
).toBe(5);
});
it("recordOp falls back to UNKNOWN code when none supplied on error", {
timeout: 5000,
}, () => {
recordOp({ op: "read", scheme: "x", outcome: "error", durationMs: 1 });
const errs = provider.meter.counters.get("storage_uri.errors_total");
expect(errs?.adds[0]?.attrs).toMatchObject({ code: "UNKNOWN" });
});
it("stream-fallback and buffered-write helpers are scheme-attributed", {
timeout: 5000,
}, () => {
recordStreamFallback("mem");
recordBufferedWrite("file");
expect(
provider.meter.counters.get("storage_uri.stream_fallback_total")
?.adds[0]?.attrs
).toMatchObject({ scheme: "mem" });
expect(
provider.meter.counters.get("storage_uri.buffered_write_total")
?.adds[0]?.attrs
).toMatchObject({ scheme: "file" });
});
it("getInstruments caches between calls and rebuilds on provider swap", {
timeout: 5000,
}, () => {
const a =
provider.meter.counters.get("storage_uri.reads_total") ?? null;
recordOp({
op: "read",
scheme: "x",
outcome: "success",
durationMs: 1,
});
recordOp({
op: "read",
scheme: "x",
outcome: "success",
durationMs: 1,
});
const counter = provider.meter.counters.get("storage_uri.reads_total");
expect(counter?.adds.length).toBeGreaterThanOrEqual(2);
expect(a).toBeDefined();
const next = new StubMeterProvider();
setStorageUriMeterProvider(next);
recordOp({
op: "read",
scheme: "x",
outcome: "success",
durationMs: 1,
});
expect(
next.meter.counters.get("storage_uri.reads_total")?.adds.length
).toBe(1);
});
});

View File

@@ -0,0 +1,164 @@
import {
type Counter,
type Histogram,
type Meter,
type MeterProvider,
metrics,
type Tracer,
trace,
} from "@opentelemetry/api";
const METER_NAME = "@suaveplan/storage-uri";
const TRACER_NAME = "@suaveplan/storage-uri";
let overrideMeterProvider: MeterProvider | undefined;
/**
* Override the `MeterProvider` used to acquire the storage-uri meter. Pass
* `undefined` to revert to the global provider. Resets the cached instrument
* bag so the next access binds to the new provider.
*/
export function setStorageUriMeterProvider(
provider: MeterProvider | undefined
): void {
overrideMeterProvider = provider;
cachedInstruments = undefined;
}
export type StorageUriOp =
| "open"
| "read"
| "write"
| "stat"
| "exists"
| "remove"
| "list";
export type StorageUriOutcome = "success" | "not_found" | "error" | "aborted";
export interface StorageUriInstruments {
readonly tracer: Tracer;
readonly meter: Meter;
readonly readsTotal: Counter;
readonly writesTotal: Counter;
readonly deletesTotal: Counter;
readonly errorsTotal: Counter;
readonly streamFallbackTotal: Counter;
readonly bufferedWriteTotal: Counter;
readonly opDurationMs: Histogram;
}
let cachedInstruments: StorageUriInstruments | undefined;
/** Lazily build (or return cached) instruments bound to the active provider. */
export function getInstruments(force = false): StorageUriInstruments {
if (cachedInstruments !== undefined && !force) return cachedInstruments;
const tracer = trace.getTracer(TRACER_NAME);
const meter = (overrideMeterProvider ?? metrics).getMeter(METER_NAME);
cachedInstruments = {
tracer,
meter,
readsTotal: meter.createCounter("storage_uri.reads_total", {
description:
"Total successful read operations dispatched via storage-uri",
}),
writesTotal: meter.createCounter("storage_uri.writes_total", {
description:
"Total successful write operations dispatched via storage-uri",
}),
deletesTotal: meter.createCounter("storage_uri.deletes_total", {
description:
"Total successful delete operations dispatched via storage-uri",
}),
errorsTotal: meter.createCounter("storage_uri.errors_total", {
description:
"Operations that ended in error, attributed by STORAGE_URI.* code",
}),
streamFallbackTotal: meter.createCounter(
"storage_uri.stream_fallback_total",
{
description:
"Count of open() calls that fell back to a one-chunk ReadableStream wrap because the adapter has no createReadStream",
}
),
bufferedWriteTotal: meter.createCounter(
"storage_uri.buffered_write_total",
{
description:
"Count of write() calls that drained a ReadableStream body because the adapter has no createWriteStream",
}
),
opDurationMs: meter.createHistogram("storage_uri.op_duration_ms", {
description: "Per-operation duration in milliseconds",
unit: "ms",
}),
};
return cachedInstruments;
}
/** Reset cached instruments — used by tests between provider swaps. */
export function resetInstruments(): void {
cachedInstruments = undefined;
}
const SUCCESS_COUNTER_OF: Record<StorageUriOp, "reads" | "writes" | "deletes"> =
{
read: "reads",
open: "reads",
stat: "reads",
exists: "reads",
list: "reads",
write: "writes",
remove: "deletes",
};
export interface RecordOpInput {
readonly op: StorageUriOp;
readonly scheme: string;
readonly outcome: StorageUriOutcome;
readonly durationMs: number;
/** Resolved `STORAGE_URI.*` code when `outcome === "error"`. */
readonly code?: string;
}
/** Record one full observability bundle for a completed operation. */
export function recordOp(input: RecordOpInput): void {
const inst = getInstruments();
const { op, scheme, outcome, durationMs, code } = input;
inst.opDurationMs.record(durationMs, { op, scheme, outcome });
if (outcome === "success") {
const counter = SUCCESS_COUNTER_OF[op];
(counter === "writes"
? inst.writesTotal
: counter === "deletes"
? inst.deletesTotal
: inst.readsTotal
).add(1, { op, scheme, outcome });
} else {
inst.errorsTotal.add(1, { op, scheme, code: code ?? "UNKNOWN" });
}
}
/** Increment `storage_uri.stream_fallback_total` for the given scheme. */
export function recordStreamFallback(scheme: string): void {
getInstruments().streamFallbackTotal.add(1, { scheme });
}
/** Increment `storage_uri.buffered_write_total` for the given scheme. */
export function recordBufferedWrite(scheme: string): void {
getInstruments().bufferedWriteTotal.add(1, { scheme });
}
/**
* Strip query string and fragment from a URI so that span attributes never
* leak presigned-URL secrets. Returns the input verbatim when it cannot be
* sanitised (e.g. parse failure on a malformed candidate).
*/
export function redactUri(uri: string): string {
const q = uri.indexOf("?");
const h = uri.indexOf("#");
let end = uri.length;
if (q >= 0) end = Math.min(end, q);
if (h >= 0) end = Math.min(end, h);
return uri.slice(0, end);
}

View File

@@ -0,0 +1,2 @@
export type { ParsedUriParts } from "./parse-uri.js";
export { parseUri } from "./parse-uri.js";

View File

@@ -0,0 +1,71 @@
# parse-uri
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`parseUri(uri)` is the RFC 3986 subset parser the resolver uses to extract a
scheme, optional authority, path, and optional query from a URI string. It
exists to keep `StorageUriResolver` scheme-agnostic — every adapter plugin
owns its own scheme-specific addressing and composes against this base
parser.
The parser intentionally rejects more inputs than RFC 3986 strictly
requires: whitespace, NUL bytes, and strings longer than 4096 characters are
all hard-failures. The 4096-cap matches the de-facto path-length ceiling
observed across S3, Rackspace, Bunny, and most filesystems and prevents
quadratic blow-ups inside plugin parsers.
## Features
- Scheme regex `^[A-Za-z][A-Za-z0-9+\-.]*$` per RFC 3986 §3.1.
- Case-insensitive scheme — the returned `scheme` is lower-cased.
- Authority extraction when the rest starts with `//`.
- Query/fragment split with both kept off the path.
- Single error code on rejection: `STORAGE_URI.INVALID_URI` with the
offending input on the `uri` discriminant.
## Usage: Basic
```ts
import { parseUri } from "@suaveplan/storage-uri/parse-uri";
const parts = parseUri("s3://bucket/raw/clip.mov");
// { scheme: "s3", authority: "bucket", path: "/raw/clip.mov" }
```
## Usage: Advanced
```ts
import { parseUri } from "@suaveplan/storage-uri/parse-uri";
import { STORAGE_URI_CODES, StorageUriError } from "@suaveplan/storage-uri/error-codes";
function parseOrNull(uri: string) {
try {
return parseUri(uri);
} catch (err) {
if (err instanceof StorageUriError && err.code === STORAGE_URI_CODES.INVALID_URI) {
return null;
}
throw err;
}
}
```
## API Reference
- `interface ParsedUriParts { scheme: string; authority?: string; path: string; query?: string }`
- `function parseUri(uri: string): ParsedUriParts` — throws
`StorageUriError(STORAGE_URI.INVALID_URI)` on rejection.
## Implementation Notes
Authority termination is hand-rolled instead of regex-based so the parser
avoids re-walking the input. The `findAuthorityEnd` helper scans for the
first `/`, `?`, or `#` character — these are the three RFC 3986 component
delimiters that may follow an authority. Empty path is permitted (e.g.
`s3://bucket`); empty rest after the colon (`scheme:`) is not. The
maximum-length check happens BEFORE the regex test so pathological inputs
cannot pin a regex engine.

View File

@@ -0,0 +1,94 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
STORAGE_URI_CODES,
StorageUriError,
} from "../error-codes/error-codes.js";
import { parseUri } from "./parse-uri.js";
const NUL = String.fromCharCode(0);
describe("parseUri", () => {
it("parses well-formed URIs", { timeout: 5000 }, () => {
const a = parseUri("s3://bucket/key");
expect(a.scheme).toBe("s3");
expect(a.authority).toBe("bucket");
expect(a.path).toBe("/key");
const b = parseUri("file:///abs/path");
expect(b.scheme).toBe("file");
expect(b.authority).toBe("");
expect(b.path).toBe("/abs/path");
const c = parseUri("https://host/p?q=1");
expect(c.scheme).toBe("https");
expect(c.authority).toBe("host");
expect(c.path).toBe("/p");
expect(c.query).toBe("q=1");
});
it("throws INVALID_URI for malformed inputs", { timeout: 5000 }, () => {
for (const bad of [
"bare-string",
"scheme:",
"s3://bucket /key",
"x".repeat(4097),
]) {
let threw: unknown;
try {
parseUri(bad);
} catch (err) {
threw = err;
}
expect(threw).toBeInstanceOf(StorageUriError);
expect((threw as StorageUriError).code).toBe(
STORAGE_URI_CODES.INVALID_URI
);
}
});
it("rejects empty input", { timeout: 5000 }, () => {
expect(() => parseUri("")).toThrow(StorageUriError);
});
it("rejects NUL bytes", { timeout: 5000 }, () => {
expect(() => parseUri(`s3://b/k${NUL}`)).toThrow(StorageUriError);
});
it("lowercases the scheme per RFC 3986 §3.1", { timeout: 5000 }, () => {
expect(parseUri("S3://bucket/key").scheme).toBe("s3");
expect(parseUri("HTTP://host/p").scheme).toBe("http");
});
it("rejects scheme starting with a non-letter", { timeout: 5000 }, () => {
expect(() => parseUri("1bad://x")).toThrow(StorageUriError);
});
it("handles a fragment by stripping it from the path", {
timeout: 5000,
}, () => {
const out = parseUri("https://host/p#frag");
expect(out.path).toBe("/p");
});
it("rejects non-string input via coercion guard", { timeout: 5000 }, () => {
expect(() => parseUri(123 as unknown as string)).toThrow(
StorageUriError
);
});
it("parses an authority that is not terminated by /, ?, or #", {
timeout: 5000,
}, () => {
const out = parseUri("s3://just-bucket");
expect(out.scheme).toBe("s3");
expect(out.authority).toBe("just-bucket");
expect(out.path).toBe("");
});
it("parses a URI with no authority component", { timeout: 5000 }, () => {
const out = parseUri("urn:example:foo");
expect(out.scheme).toBe("urn");
expect(out.authority).toBeUndefined();
expect(out.path).toBe("example:foo");
});
});

View File

@@ -0,0 +1,117 @@
import {
STORAGE_URI_CODES,
StorageUriError,
} from "../error-codes/error-codes.js";
/** Lowercased scheme + the structural components used by adapter plugins. */
export interface ParsedUriParts {
/** RFC 3986 scheme, lowercased per §3.1 case-insensitivity. */
readonly scheme: string;
/** Authority component (host[:port]) when present, else `undefined`. */
readonly authority?: string;
/** Path component — always defined, may be empty. */
readonly path: string;
/** Query component without the leading `?`, when present. */
readonly query?: string;
}
const SCHEME_RE = /^[A-Za-z][A-Za-z0-9+\-.]*$/;
const MAX_LEN = 4096;
const NUL_CHAR = String.fromCharCode(0);
/**
* Split an input string into RFC 3986 components.
*
* Rejects every input that:
*
* - is empty or has no `:` delimiter after the scheme
* - has a scheme that fails `^[A-Za-z][A-Za-z0-9+\-.]*$`
* - contains a whitespace character (space, tab, newline, return)
* - contains a U+0000 NUL byte
* - exceeds 4096 characters
*
* On rejection, throws `StorageUriError(STORAGE_URI.INVALID_URI)` with the
* offending input on the `uri` discriminant.
*/
export function parseUri(uri: string): ParsedUriParts {
if (typeof uri !== "string" || uri.length === 0) {
throw new StorageUriError(
STORAGE_URI_CODES.INVALID_URI,
"URI must be a non-empty string",
{ uri: typeof uri === "string" ? uri : String(uri) }
);
}
if (uri.length > MAX_LEN) {
throw new StorageUriError(
STORAGE_URI_CODES.INVALID_URI,
`URI exceeds ${MAX_LEN}-character limit`,
{ uri: uri.slice(0, 64) }
);
}
if (uri.includes(NUL_CHAR)) {
throw new StorageUriError(
STORAGE_URI_CODES.INVALID_URI,
"URI contains a NUL byte",
{ uri }
);
}
if (/\s/.test(uri)) {
throw new StorageUriError(
STORAGE_URI_CODES.INVALID_URI,
"URI contains whitespace",
{ uri }
);
}
const colon = uri.indexOf(":");
if (colon <= 0 || colon === uri.length - 1) {
throw new StorageUriError(
STORAGE_URI_CODES.INVALID_URI,
"URI must be of the form '<scheme>:<rest>' with a non-empty rest",
{ uri }
);
}
const rawScheme = uri.slice(0, colon);
if (!SCHEME_RE.test(rawScheme)) {
throw new StorageUriError(
STORAGE_URI_CODES.INVALID_URI,
`URI scheme '${rawScheme}' does not match RFC 3986 §3.1`,
{ uri, scheme: rawScheme }
);
}
const scheme = rawScheme.toLowerCase();
let rest = uri.slice(colon + 1);
let authority: string | undefined;
if (rest.startsWith("//")) {
const afterSlashes = rest.slice(2);
const pathStart = findAuthorityEnd(afterSlashes);
authority = afterSlashes.slice(0, pathStart);
rest = afterSlashes.slice(pathStart);
}
let query: string | undefined;
const q = rest.indexOf("?");
if (q >= 0) {
query = rest.slice(q + 1);
rest = rest.slice(0, q);
}
const hash = rest.indexOf("#");
const path = hash >= 0 ? rest.slice(0, hash) : rest;
const out: ParsedUriParts = {
scheme,
path,
...(authority !== undefined ? { authority } : {}),
...(query !== undefined ? { query } : {}),
};
return out;
}
function findAuthorityEnd(s: string): number {
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
// `/` (47), `?` (63), `#` (35)
if (c === 47 || c === 63 || c === 35) return i;
}
return s.length;
}

View File

@@ -0,0 +1,7 @@
export {
createStorageUriResolver,
StorageUriResolver,
} from "./resolver.js";
export { listViaPlugin } from "./resolver.list.js";
export { openViaPlugin } from "./resolver.streams.js";
export { writeViaPlugin } from "./resolver.writes.js";

View File

@@ -0,0 +1,66 @@
# resolver.list
## Purpose
Implements the `list(uri, options?, signal?)` semantics. Treats the input
URI's key portion as a listing prefix, delegates to
`StorageLike.list({ prefix, limit, continuationToken })`, then re-serialises
each returned adapter key back to a fully-qualified URI via the plugin's
`build(key, meta)` function. Plugins without `build` cause `list()` to throw
`STORAGE_URI.LIST_UNSUPPORTED`.
The function lives in its own file so the prefix-and-rebuild logic stays
isolated from the rest of the resolver and so its specific error mapping
can evolve without touching adjacent ops.
## Features
- Prefix derived from the plugin's `parse(uri).key`.
- Limit and continuation-token forwarded verbatim when supplied.
- `LIST_UNSUPPORTED` raised at the top of the function when `build` is
missing — before any adapter call is issued.
- `hasMore`/`continuationToken` round-tripped opaquely from the adapter.
## Usage: Basic
```ts
import { listViaPlugin } from "@suaveplan/storage-uri/resolver";
const out = await listViaPlugin(plugin, "s3://bucket/raw/", "raw/", { bucket: "bucket" }, { limit: 10 });
// { uris: ["s3://bucket/raw/a.mov", ...], hasMore: false }
```
## Usage: Advanced
```ts
async function listAll(plugin, uri, prefix, meta) {
const acc: string[] = [];
let token: string | undefined;
do {
const opts = token === undefined ? { limit: 100 } : { limit: 100, continuationToken: token };
const page = await listViaPlugin(plugin, uri, prefix, meta, opts);
acc.push(...page.uris);
token = page.continuationToken;
} while (token !== undefined);
return acc;
}
```
## API Reference
- `function listViaPlugin(plugin, uri, prefix, meta?, options?): Promise<ListUriResult>`
## Implementation Notes
`build` is captured via `.bind(plugin)` before the map so `this`-bound
plugin implementations work without surprise. The output object is
constructed via spread so `continuationToken` is omitted entirely when the
adapter returned `undefined` — matters under
`exactOptionalPropertyTypes: true` and keeps the resolver's contract
consistent with `StorageLike.list`. The capability-check fires before any
adapter call so an unsupported list does not waste an upstream request:
operators can probe for list support cheaply by catching
`STORAGE_URI.LIST_UNSUPPORTED` once and caching the result per-plugin.
`meta` is forwarded verbatim to `build` so plugins that carry a bucket,
container, or zone in `meta` get the same value back; downstream consumers
that paginate across pages will see the same authority on every URI.

View File

@@ -0,0 +1,110 @@
import { beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
import { MemoryStorageLike } from "../../tests/setup/test-helpers.js";
import {
STORAGE_URI_CODES,
StorageUriError,
} from "../error-codes/error-codes.js";
import type { UriPlugin } from "../types/plugin.types.js";
import { createStorageUriResolver } from "./resolver.js";
function withBuildPlugin(
scheme: string,
adapter: MemoryStorageLike
): UriPlugin {
return {
scheme,
adapter,
parse(uri) {
const after = uri.slice(uri.indexOf("://") + 3);
const slash = after.indexOf("/");
const authority = slash >= 0 ? after.slice(0, slash) : after;
const key = slash >= 0 ? after.slice(slash + 1) : "";
return { key, meta: { bucket: authority } };
},
build(key, meta) {
const bucket = meta?.["bucket"] ?? "default";
return `${scheme}://${bucket}/${key}`;
},
};
}
function noBuildPlugin(scheme: string, adapter: MemoryStorageLike): UriPlugin {
return {
scheme,
adapter,
parse(uri) {
const after = uri.slice(uri.indexOf("://") + 3);
const slash = after.indexOf("/");
return { key: slash >= 0 ? after.slice(slash + 1) : "" };
},
};
}
describe("resolver.list", () => {
let adapter: MemoryStorageLike;
beforeEach(() => {
adapter = new MemoryStorageLike();
});
it("re-serialises keys to URIs", { timeout: 5000 }, async () => {
await adapter.set("raw/a.mov", new Uint8Array([1]));
await adapter.set("raw/b.mov", new Uint8Array([2]));
await adapter.set("other/c.mov", new Uint8Array([3]));
const r = createStorageUriResolver([withBuildPlugin("s3", adapter)]);
const out = await r.list("s3://bucket/raw/", { limit: 10 });
expect([...out.uris].sort()).toEqual([
"s3://bucket/raw/a.mov",
"s3://bucket/raw/b.mov",
]);
expect(out.hasMore).toBe(false);
});
it("throws LIST_UNSUPPORTED when plugin omits build", {
timeout: 5000,
}, async () => {
const r = createStorageUriResolver([noBuildPlugin("memory", adapter)]);
let threw: unknown;
try {
await r.list("memory://x/y/");
} catch (err) {
threw = err;
}
expect(threw).toBeInstanceOf(StorageUriError);
expect((threw as StorageUriError).code).toBe(
STORAGE_URI_CODES.LIST_UNSUPPORTED
);
});
it("round-trips continuation token opaquely", {
timeout: 5000,
}, async () => {
const ad = new MemoryStorageLike();
// Override list to inject a continuation token.
ad.list = async () => ({
keys: ["x/a", "x/b"],
total: 2,
hasMore: true,
continuationToken: "def",
});
const r = createStorageUriResolver([withBuildPlugin("s3", ad)]);
const out = await r.list("s3://bucket/x/", {
continuationToken: "abc",
});
expect(out.continuationToken).toBe("def");
expect(out.hasMore).toBe(true);
});
it("forwards limit option through to adapter.list", {
timeout: 5000,
}, async () => {
let captured: unknown;
const ad = new MemoryStorageLike();
ad.list = async (opts) => {
captured = opts;
return { keys: [], total: 0, hasMore: false };
};
const r = createStorageUriResolver([withBuildPlugin("s3", ad)]);
await r.list("s3://bucket/p/", { limit: 7 });
expect(captured).toMatchObject({ prefix: "p/", limit: 7 });
});
});

View File

@@ -0,0 +1,55 @@
import {
STORAGE_URI_CODES,
StorageUriError,
} from "../error-codes/error-codes.js";
import type { UriPlugin } from "../types/plugin.types.js";
import type { ListUriOptions, ListUriResult } from "../types/resolver.types.js";
/**
* Implement `list(uri, options?)` semantics for a dispatched plugin.
*
* The input URI's key portion is treated as the listing prefix. The plugin
* MUST expose a `build()` function so each returned adapter key can be
* re-serialised back to a fully-qualified URI; plugins without `build` throw
* `STORAGE_URI.LIST_UNSUPPORTED`.
*
* The adapter's `hasMore` and `continuationToken` fields are forwarded
* verbatim — the resolver does not interpret tokens.
*/
export async function listViaPlugin(
plugin: UriPlugin,
uri: string,
prefix: string,
meta: Record<string, string> | undefined,
options: ListUriOptions | undefined
): Promise<ListUriResult> {
if (typeof plugin.build !== "function") {
throw new StorageUriError(
STORAGE_URI_CODES.LIST_UNSUPPORTED,
"plugin does not implement build(); list() requires it",
{ uri, scheme: plugin.scheme }
);
}
const build = plugin.build.bind(plugin);
const listOpts: {
prefix: string;
limit?: number;
continuationToken?: string;
} = {
prefix,
};
if (options?.limit !== undefined) listOpts.limit = options.limit;
if (options?.continuationToken !== undefined) {
listOpts.continuationToken = options.continuationToken;
}
const result = await plugin.adapter.list(listOpts);
const uris = result.keys.map((k) => build(k, meta));
const out: ListUriResult = {
uris,
hasMore: result.hasMore,
...(result.continuationToken !== undefined
? { continuationToken: result.continuationToken }
: {}),
};
return out;
}

View File

@@ -0,0 +1,94 @@
# resolver
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `StorageUriResolver` class is the entry point for URI-routed dispatch in
`@suaveplan/storage-uri`. Constructed once at boot from an array of
`UriPlugin`s, it exposes `open`/`read`/`write`/`stat`/`exists`/`remove`/
`list` keyed by URI string and delegates each call to the plugin matching
the URI's scheme. The resolver performs no I/O of its own — every byte goes
through an existing `StorageLike` adapter — and is fully immutable after
construction.
`createStorageUriResolver(plugins)` is the preferred factory. It throws
`STORAGE_URI.SCHEME_CONFLICT` when two distinct plugin references claim the
same scheme; registering the same reference twice is a silent no-op. The
result is frozen via `Object.freeze`.
## Features
- O(1) scheme dispatch via `Map<scheme, UriPlugin>` (case-insensitive).
- `AbortSignal` propagated to every adapter call and short-circuited
pre-aborted with `STORAGE_URI.ABORTED`.
- `SuaveplanError` subclasses thrown by an adapter (any package's namespaced
error — `DriveUriError`, `StorageHttpError`, etc.) pass through the resolver
unchanged so consumers can `switch (err.code)` on the original code. Only
non-namespaced throwables (plain `Error`, foreign objects) are wrapped as
`STORAGE_URI.ADAPTER_ERROR` with the original preserved as `cause`.
- One span per public op (`storage.uri.{open,read,write,stat,exists,remove,list}`)
on the `@suaveplan/storage-uri` tracer.
- `op_duration_ms`, `reads_total`, `writes_total`, `deletes_total`,
`errors_total` counters emitted automatically.
## Usage: Basic
```ts
import { createStorageUriResolver } from "@suaveplan/storage-uri";
const resolver = createStorageUriResolver([fileUriPlugin, httpsUriPlugin]);
const bytes = await resolver.read("file:///cache/poster.jpg");
await resolver.write("file:///cache/poster.jpg", newBytes);
```
## Usage: Advanced
```ts
const stream = await resolver.open("https://cdn.example.com/clip/master.m3u8");
const reader = stream.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
sink.write(value);
}
} finally {
reader.releaseLock();
}
// Prefix list with continuation paging.
let token: string | undefined;
do {
const page = await resolver.list("s3://bucket/raw/", { limit: 100, ...(token !== undefined ? { continuationToken: token } : {}) });
for (const uri of page.uris) consume(uri);
token = page.continuationToken;
} while (token !== undefined);
```
## API Reference
- `class StorageUriResolver`
- `constructor(plugins: readonly UriPlugin[])`
- `get schemeCount(): number`
- `hasScheme(scheme: string): boolean`
- `open(uri, signal?): Promise<ReadableStream<Uint8Array>>`
- `read(uri, signal?): Promise<Uint8Array>`
- `write(uri, body, signal?): Promise<void>`
- `stat(uri, signal?): Promise<StatResult | undefined>`
- `exists(uri, signal?): Promise<boolean>`
- `remove(uri, signal?): Promise<boolean>`
- `list(uri, options?, signal?): Promise<ListUriResult>`
- `function createStorageUriResolver(plugins): StorageUriResolver`
## Implementation Notes
Dispatch is split into `#dispatch(uri)` (parse + lookup + plugin-`parse`)
and per-op handlers. Every public method funnels through the `#run`
generic, which owns the pre-aborted check, span start/end, error wrap, and
duration emission. Splitting `open`/`write`/`list` into sibling modules
keeps each file under the 300-line code budget without forcing the call
site to know which file owns which method. The class field is `#plugins`,
not `_plugins`, to make accidental external access a hard syntax error.

View File

@@ -0,0 +1,115 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import type { StorageLike } from "@suaveplan/types";
import { MemoryStorageLike } from "../../tests/setup/test-helpers.js";
import { STORAGE_URI_CODES } from "../error-codes/error-codes.js";
import type { UriPlugin } from "../types/plugin.types.js";
import { createStorageUriResolver } from "./resolver.js";
describe("storage-uri spec conformance", () => {
it("MemoryStorageLike satisfies the StorageLike contract structurally", {
timeout: 5000,
}, () => {
const adapter: StorageLike = new MemoryStorageLike();
expect(typeof adapter.get).toBe("function");
expect(typeof adapter.set).toBe("function");
expect(typeof adapter.delete).toBe("function");
expect(typeof adapter.exists).toBe("function");
expect(typeof adapter.list).toBe("function");
expect(typeof adapter.clear).toBe("function");
});
it("the resolver class has the expected public surface", {
timeout: 5000,
}, () => {
const r = createStorageUriResolver([]);
expect(typeof r.open).toBe("function");
expect(typeof r.read).toBe("function");
expect(typeof r.write).toBe("function");
expect(typeof r.stat).toBe("function");
expect(typeof r.exists).toBe("function");
expect(typeof r.remove).toBe("function");
expect(typeof r.list).toBe("function");
expect(typeof r.hasScheme).toBe("function");
expect(typeof r.schemeCount).toBe("number");
});
it("every STORAGE_URI code is reachable from a real call site", {
timeout: 5000,
}, async () => {
// UNKNOWN_SCHEME
const empty = createStorageUriResolver([]);
await expect(empty.read("ftp://x/y")).rejects.toMatchObject({
code: STORAGE_URI_CODES.UNKNOWN_SCHEME,
});
// INVALID_URI
await expect(empty.read("bad")).rejects.toMatchObject({
code: STORAGE_URI_CODES.INVALID_URI,
});
// SCHEME_CONFLICT
const ad = new MemoryStorageLike();
const p1: UriPlugin = {
scheme: "x",
adapter: ad,
parse: () => ({ key: "" }),
};
const p2: UriPlugin = {
scheme: "x",
adapter: ad,
parse: () => ({ key: "" }),
};
expect(() => createStorageUriResolver([p1, p2])).toThrow();
// NOT_FOUND
const r = createStorageUriResolver([
{
scheme: "m",
adapter: ad,
parse: () => ({ key: "missing" }),
},
]);
await expect(r.read("m://x/missing")).rejects.toMatchObject({
code: STORAGE_URI_CODES.NOT_FOUND,
});
// LIST_UNSUPPORTED
await expect(r.list("m://x/p/")).rejects.toMatchObject({
code: STORAGE_URI_CODES.LIST_UNSUPPORTED,
});
// WRITE_UNSUPPORTED
const ro = new MemoryStorageLike();
ro.set = async () => {
throw new Error("read-only");
};
const wr = createStorageUriResolver([
{
scheme: "ro",
adapter: ro,
parse: () => ({ key: "a" }),
},
]);
await expect(
wr.write("ro://x/a", new Uint8Array([1]))
).rejects.toMatchObject({
code: STORAGE_URI_CODES.WRITE_UNSUPPORTED,
});
// ABORTED
const ctrl = new AbortController();
ctrl.abort();
await expect(r.read("m://x/k", ctrl.signal)).rejects.toMatchObject({
code: STORAGE_URI_CODES.ABORTED,
});
// ADAPTER_ERROR
const broken = new MemoryStorageLike();
broken.exists = async () => {
throw new Error("boom");
};
const br = createStorageUriResolver([
{
scheme: "b",
adapter: broken,
parse: () => ({ key: "a" }),
},
]);
await expect(br.exists("b://x/a")).rejects.toMatchObject({
code: STORAGE_URI_CODES.ADAPTER_ERROR,
});
});
});

View File

@@ -0,0 +1,61 @@
# resolver.streams
## Purpose
Implements the `open(uri, signal?)` semantics for the resolver: forward a
native `createReadStream` when the adapter exposes one, otherwise fall back
to wrapping `adapter.get(key)` in a one-chunk `ReadableStream` and increment
`storage_uri.stream_fallback_total{scheme}`. Splitting this branch into its
own file keeps `resolver.ts` under the code-line budget while letting the
streaming path evolve independently.
The function is exported so adapter authors and integration tests can call
it directly without instantiating the full `StorageUriResolver` — useful for
isolating the fallback behaviour in unit tests.
## Features
- Duck-typed `createReadStream` detection on the adapter.
- One-chunk `ReadableStream` synthesis from a buffered `get` result.
- `STORAGE_URI.NOT_FOUND` mapping when `get` returns `undefined`.
- `storage_uri.stream_fallback_total` counter increment on the fallback
path (not on the forwarding path).
## Usage: Basic
```ts
import { openViaPlugin } from "@suaveplan/storage-uri/resolver";
const stream = await openViaPlugin(plugin, "memory://x/a", "a", undefined);
```
## Usage: Advanced
```ts
const ctrl = new AbortController();
try {
const stream = await openViaPlugin(plugin, uri, key, ctrl.signal);
pipe(stream, sink, { signal: ctrl.signal });
} finally {
ctrl.abort();
}
```
## API Reference
- `function openViaPlugin(plugin, uri, key, signal): Promise<ReadableStream<Uint8Array>>`
## Implementation Notes
The function does NOT do the pre-aborted check; that lives in `#run` so
every public method shares one short-circuit point. The check for
`typeof adapter.createReadStream === "function"` is done on the casted
`adapter` value because `StorageLike` does not declare the method — the
duck-typing is intentional so any adapter exposing the right shape is
forwarded automatically. The fallback path materialises the entire object
in memory, which is correct for small payloads but a hazard for large ones
behind a non-streaming adapter; operators should alert on the
`storage_uri.stream_fallback_total` counter to detect mis-composed
deployments before they OOM. The one-chunk `ReadableStream` close is
deterministic — the queue is enqueued and the controller closed in the
same microtask, so consumers see exactly one chunk before `done: true`.

View File

@@ -0,0 +1,169 @@
import type { Counter, Meter, MeterProvider } from "@opentelemetry/api";
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import { MemoryStorageLike } from "../../tests/setup/test-helpers.js";
import {
STORAGE_URI_CODES,
StorageUriError,
} from "../error-codes/error-codes.js";
import { resetInstruments, setStorageUriMeterProvider } from "../otel/otel.js";
import type { UriPlugin } from "../types/plugin.types.js";
import { createStorageUriResolver } from "./resolver.js";
function basicPlugin(
scheme: string,
adapter: MemoryStorageLike,
extras: Partial<{
createReadStream(
key: string,
signal?: AbortSignal
): ReadableStream<Uint8Array>;
}> = {}
): UriPlugin {
const merged = Object.assign(adapter, extras);
return {
scheme,
adapter: merged,
parse(uri) {
const after = uri.slice(uri.indexOf("://") + 3);
const slash = after.indexOf("/");
return { key: slash >= 0 ? after.slice(slash + 1) : "" };
},
};
}
interface Sample {
value: number;
attrs: Record<string, unknown> | undefined;
}
class StubCounter implements Counter {
public adds: Sample[] = [];
public constructor(public readonly name: string) {}
public add(v: number, attrs?: Record<string, unknown>): void {
this.adds.push({ value: v, attrs });
}
}
class StubHistogram {
public samples: Sample[] = [];
public record(v: number, attrs?: Record<string, unknown>): void {
this.samples.push({ value: v, attrs });
}
}
class StubMeter implements Meter {
public counters: Map<string, StubCounter> = new Map();
public histograms: Map<string, StubHistogram> = new Map();
public createCounter(name: string): Counter {
const c = new StubCounter(name);
this.counters.set(name, c);
return c;
}
public createHistogram(name: string): StubHistogram {
const h = new StubHistogram();
this.histograms.set(name, h);
return h as unknown as StubHistogram;
}
public createUpDownCounter(): never {
throw new Error("unused");
}
public createObservableCounter(): never {
throw new Error("unused");
}
public createObservableUpDownCounter(): never {
throw new Error("unused");
}
public createObservableGauge(): never {
throw new Error("unused");
}
public addBatchObservableCallback(): void {}
public removeBatchObservableCallback(): void {}
public createGauge(): never {
throw new Error("unused");
}
}
class StubMeterProvider implements MeterProvider {
public meter = new StubMeter();
public getMeter(): Meter {
return this.meter;
}
}
describe("resolver.streams (open)", () => {
let provider: StubMeterProvider;
beforeEach(() => {
provider = new StubMeterProvider();
setStorageUriMeterProvider(provider);
resetInstruments();
});
afterEach(() => {
setStorageUriMeterProvider(undefined);
resetInstruments();
});
it("forwards adapter.createReadStream when available", {
timeout: 5000,
}, async () => {
const ad = new MemoryStorageLike();
const native = new ReadableStream<Uint8Array>({
start(c) {
c.enqueue(new Uint8Array([1]));
c.close();
},
});
const plugin = basicPlugin("mem", ad, {
createReadStream: () => native,
});
const r = createStorageUriResolver([plugin]);
const out = await r.open("mem://x/a");
expect(out).toBe(native);
expect(
provider.meter.counters.get("storage_uri.stream_fallback_total")
?.adds.length ?? 0
).toBe(0);
});
it("falls back to one-chunk stream and increments counter", {
timeout: 5000,
}, async () => {
const ad = new MemoryStorageLike();
await ad.set("a", new Uint8Array([1, 2, 3]));
const r = createStorageUriResolver([basicPlugin("mem", ad)]);
const stream = await r.open("mem://x/a");
const reader = stream.getReader();
const chunk = await reader.read();
expect(chunk.done).toBe(false);
expect(Array.from(chunk.value as Uint8Array)).toEqual([1, 2, 3]);
const end = await reader.read();
expect(end.done).toBe(true);
const counter = provider.meter.counters.get(
"storage_uri.stream_fallback_total"
);
expect(counter?.adds.length).toBe(1);
expect(counter?.adds[0]?.attrs).toMatchObject({ scheme: "mem" });
});
it("throws NOT_FOUND when adapter returns undefined", {
timeout: 5000,
}, async () => {
const ad = new MemoryStorageLike();
const r = createStorageUriResolver([basicPlugin("mem", ad)]);
let threw: unknown;
try {
await r.open("mem://x/missing");
} catch (err) {
threw = err;
}
expect(threw).toBeInstanceOf(StorageUriError);
expect((threw as StorageUriError).code).toBe(
STORAGE_URI_CODES.NOT_FOUND
);
});
});

View File

@@ -0,0 +1,54 @@
import {
STORAGE_URI_CODES,
StorageUriError,
} from "../error-codes/error-codes.js";
import { recordStreamFallback } from "../otel/otel.js";
import type {
StreamingStorageExtensions,
UriPlugin,
} from "../types/plugin.types.js";
/**
* Implement `open(uri)` semantics for a dispatched plugin.
*
* When `plugin.adapter.createReadStream` exists, the resolver forwards the
* call verbatim (no fallback counter increment). Otherwise it issues a
* standard `get(key)`, throws `STORAGE_URI.NOT_FOUND` on `undefined`, wraps
* the bytes in a one-chunk `ReadableStream`, and increments
* `storage_uri.stream_fallback_total{scheme}`.
*
* The pre-aborted check and span/metrics bookkeeping is owned by the caller
* (resolver `open` wrapper) so this function stays focused on the dispatch
* branch.
*/
export async function openViaPlugin(
plugin: UriPlugin,
uri: string,
key: string,
signal: AbortSignal | undefined
): Promise<ReadableStream<Uint8Array>> {
const adapter = plugin.adapter as typeof plugin.adapter &
StreamingStorageExtensions;
if (typeof adapter.createReadStream === "function") {
const stream = await adapter.createReadStream(key, signal);
return stream;
}
const bytes = await adapter.get(key);
if (bytes === undefined) {
throw new StorageUriError(
STORAGE_URI_CODES.NOT_FOUND,
"resource not found",
{
uri,
scheme: plugin.scheme,
}
);
}
recordStreamFallback(plugin.scheme);
return new ReadableStream<Uint8Array>({
start(ctrl) {
ctrl.enqueue(bytes);
ctrl.close();
},
});
}

View File

@@ -0,0 +1,317 @@
import { SuaveplanError } from "@suaveplan/error";
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import { MemoryStorageLike } from "../../tests/setup/test-helpers.js";
import {
STORAGE_URI_CODES,
StorageUriError,
} from "../error-codes/error-codes.js";
import { resetInstruments } from "../otel/otel.js";
import type { UriPlugin } from "../types/plugin.types.js";
import {
createStorageUriResolver,
type StorageUriResolver,
} from "./resolver.js";
function memoryPlugin(scheme: string, adapter: MemoryStorageLike): UriPlugin {
return {
scheme,
adapter,
parse(uri) {
const idx = uri.indexOf("://");
const after =
idx >= 0 ? uri.slice(idx + 3) : uri.slice(uri.indexOf(":") + 1);
const slash = after.indexOf("/");
const authority = slash >= 0 ? after.slice(0, slash) : after;
const key = slash >= 0 ? after.slice(slash + 1) : "";
return { key, meta: { authority } };
},
build(key, meta) {
const auth = meta?.["authority"] ?? "x";
return `${scheme}://${auth}/${key}`;
},
};
}
describe("StorageUriResolver", () => {
let adapter: MemoryStorageLike;
let plugin: UriPlugin;
let resolver: StorageUriResolver;
beforeEach(() => {
resetInstruments();
adapter = new MemoryStorageLike();
plugin = memoryPlugin("s3", adapter);
resolver = createStorageUriResolver([plugin]);
});
afterEach(() => {
resetInstruments();
});
it("dispatches by scheme to the registered plugin", {
timeout: 5000,
}, async () => {
await adapter.set("k", new Uint8Array([1, 2, 3]));
const out = await resolver.read("s3://bucket/k");
expect(Array.from(out)).toEqual([1, 2, 3]);
expect(adapter.getCalls).toBe(1);
});
it("throws UNKNOWN_SCHEME for unregistered scheme", {
timeout: 5000,
}, async () => {
await expect(resolver.read("ftp://host/path")).rejects.toMatchObject({
code: STORAGE_URI_CODES.UNKNOWN_SCHEME,
scheme: "ftp",
});
});
it("matches scheme case-insensitively", { timeout: 5000 }, async () => {
await adapter.set("k", new Uint8Array([9]));
const out = await resolver.read("S3://bucket/k");
expect(Array.from(out)).toEqual([9]);
});
it("rejects conflicting scheme registrations", { timeout: 5000 }, () => {
const a = memoryPlugin("s3", new MemoryStorageLike());
const b = memoryPlugin("s3", new MemoryStorageLike());
let threw: unknown;
try {
createStorageUriResolver([a, b]);
} catch (err) {
threw = err;
}
expect(threw).toBeInstanceOf(StorageUriError);
expect((threw as StorageUriError).code).toBe(
STORAGE_URI_CODES.SCHEME_CONFLICT
);
expect((threw as StorageUriError).scheme).toBe("s3");
});
it("accepts idempotent reference re-registration", {
timeout: 5000,
}, async () => {
const r = createStorageUriResolver([plugin, plugin]);
expect(r.schemeCount).toBe(1);
expect(r.hasScheme("s3")).toBe(true);
await adapter.set("k", new Uint8Array([4]));
const out = await r.read("s3://b/k");
expect(out[0]).toBe(4);
});
it("short-circuits on pre-aborted signal", { timeout: 5000 }, async () => {
const ctrl = new AbortController();
ctrl.abort();
await expect(
resolver.read("s3://bucket/k", ctrl.signal)
).rejects.toMatchObject({ code: STORAGE_URI_CODES.ABORTED });
expect(adapter.getCalls).toBe(0);
});
it("forwards AbortSignal to adapter unchanged", {
timeout: 5000,
}, async () => {
const ctrl = new AbortController();
let captured: AbortSignal | undefined;
const ad = new MemoryStorageLike();
const sig = ctrl.signal;
const p: UriPlugin = {
scheme: "memory",
adapter: Object.assign(ad, {
async createReadStream(_k: string, s?: AbortSignal) {
captured = s;
return new ReadableStream<Uint8Array>({
start(c) {
c.enqueue(new Uint8Array([1]));
c.close();
},
});
},
}),
parse(uri) {
return { key: uri.slice("memory://x/".length) };
},
};
const r = createStorageUriResolver([p]);
await r.open("memory://x/a", sig);
expect(captured).toBe(sig);
});
it("wraps non-SuaveplanError throwables as ADAPTER_ERROR", {
timeout: 5000,
}, async () => {
const ad = new MemoryStorageLike();
const original = new Error("disk fault");
ad.get = async () => {
throw original;
};
const r = createStorageUriResolver([memoryPlugin("s3", ad)]);
let threw: unknown;
try {
await r.read("s3://bucket/k");
} catch (err) {
threw = err;
}
expect(threw).toBeInstanceOf(StorageUriError);
const e = threw as StorageUriError;
expect(e.code).toBe(STORAGE_URI_CODES.ADAPTER_ERROR);
expect(e.cause).toBe(original);
expect(e.uri).toBe("s3://bucket/k");
expect(e.scheme).toBe("s3");
});
it("passes through thrown SuaveplanError subclasses unchanged", {
timeout: 5000,
}, async () => {
// A SuaveplanError subclass from a foreign namespace (representing
// any downstream package's namespaced error — DriveUriError,
// StorageHttpError, etc.) must reach the caller untouched so
// `err.code === "FOO.BAR"` discrimination works directly.
class ForeignError extends SuaveplanError<"FOO.BAR"> {
public constructor() {
super("FOO.BAR", "downstream-defined failure");
}
}
const original = new ForeignError();
const ad = new MemoryStorageLike();
ad.get = async () => {
throw original;
};
const r = createStorageUriResolver([memoryPlugin("s3", ad)]);
let threw: unknown;
try {
await r.read("s3://bucket/k");
} catch (err) {
threw = err;
}
expect(threw).toBe(original);
expect((threw as ForeignError).code).toBe("FOO.BAR");
expect(threw).not.toBeInstanceOf(StorageUriError);
});
it("re-throws StorageUriError unchanged", { timeout: 5000 }, async () => {
const inner = new StorageUriError(STORAGE_URI_CODES.NOT_FOUND, "nope", {
uri: "s3://x/y",
scheme: "s3",
});
const ad = new MemoryStorageLike();
ad.get = async () => {
throw inner;
};
const r = createStorageUriResolver([memoryPlugin("s3", ad)]);
let threw: unknown;
try {
await r.read("s3://x/y");
} catch (err) {
threw = err;
}
expect(threw).toBe(inner);
});
it("exists delegates to adapter.exists", { timeout: 5000 }, async () => {
await adapter.set("k", new Uint8Array([1]));
expect(await resolver.exists("s3://b/k")).toBe(true);
expect(adapter.existsCalls).toBe(1);
});
it("remove returns adapter.delete boolean", { timeout: 5000 }, async () => {
expect(await resolver.remove("s3://b/missing")).toBe(false);
await adapter.set("k", new Uint8Array([1]));
expect(await resolver.remove("s3://b/k")).toBe(true);
});
it("stat returns undefined for adapters without metadata path", {
timeout: 5000,
}, async () => {
const out = await resolver.stat("s3://b/k");
expect(out).toBeUndefined();
});
it("stat returns adapter metadata when getMetadata is implemented", {
timeout: 5000,
}, async () => {
const ad = new MemoryStorageLike();
Object.assign(ad, {
async getMetadata(_k: string) {
return { size: 42, etag: "abc" };
},
});
const r = createStorageUriResolver([memoryPlugin("s3", ad)]);
const out = await r.stat("s3://b/k");
expect(out?.size).toBe(42);
expect(out?.etag).toBe("abc");
});
it("throws INVALID_URI when parseUri rejects the input", {
timeout: 5000,
}, async () => {
await expect(resolver.read("bare-string")).rejects.toMatchObject({
code: STORAGE_URI_CODES.INVALID_URI,
});
});
it("wraps non-Error throwables as ADAPTER_ERROR with no cause", {
timeout: 5000,
}, async () => {
const ad = new MemoryStorageLike();
ad.get = async () => {
throw "string-thrown";
};
const r = createStorageUriResolver([memoryPlugin("s3", ad)]);
let threw: unknown;
try {
await r.read("s3://b/k");
} catch (err) {
threw = err;
}
expect(threw).toBeInstanceOf(StorageUriError);
expect((threw as StorageUriError).code).toBe(
STORAGE_URI_CODES.ADAPTER_ERROR
);
expect((threw as StorageUriError).cause).toBeUndefined();
});
it("parses authority terminated by end-of-string (no path)", {
timeout: 5000,
}, async () => {
// Plugin parse extracts authority only; resolver still dispatches.
const ad = new MemoryStorageLike();
const noPath: UriPlugin = {
scheme: "x",
adapter: ad,
parse(_uri) {
return { key: "" };
},
};
const r = createStorageUriResolver([noPath]);
// parseUri must accept "x://just-authority" — exercises findAuthorityEnd
// fallthrough.
await expect(r.exists("x://just-authority")).resolves.toBe(false);
});
it("forwards createReadStream when present", {
timeout: 5000,
}, async () => {
const ad = new MemoryStorageLike();
const native = new ReadableStream<Uint8Array>({
start(c) {
c.enqueue(new Uint8Array([7]));
c.close();
},
});
Object.assign(ad, {
async createReadStream() {
return native;
},
});
const r = createStorageUriResolver([memoryPlugin("mem", ad)]);
const out = await r.open("mem://x/a");
expect(out).toBe(native);
});
});

View File

@@ -0,0 +1,251 @@
import { SuaveplanError } from "@suaveplan/error";
import {
STORAGE_URI_CODES,
StorageUriError,
} from "../error-codes/error-codes.js";
import { getInstruments, recordOp, redactUri } from "../otel/otel.js";
import { parseUri } from "../parse-uri/parse-uri.js";
import type {
StatResult,
StreamingStorageExtensions,
UriPlugin,
} from "../types/plugin.types.js";
import type { ListUriOptions, ListUriResult } from "../types/resolver.types.js";
import { listViaPlugin } from "./resolver.list.js";
import { openViaPlugin } from "./resolver.streams.js";
import { writeViaPlugin } from "./resolver.writes.js";
interface Dispatched {
readonly plugin: UriPlugin;
readonly key: string;
readonly meta?: Record<string, string>;
readonly scheme: string;
}
type ResolverOp =
| "open"
| "read"
| "write"
| "stat"
| "exists"
| "remove"
| "list";
/**
* URI-routed dispatch over a frozen `scheme → UriPlugin` registry.
*
* The resolver performs no I/O of its own; every public method parses the
* input URI, looks up a plugin by scheme (case-insensitive), and delegates
* to the plugin's `StorageLike` adapter. Adapter throwables that are
* `SuaveplanError` subclasses (any package's namespaced error — including
* `StorageUriError`, `DriveUriError`, `StorageHttpError`, …) pass through
* unchanged so consumers can route on the original `code`. Non-namespaced
* throwables (plain `Error`, foreign objects) are wrapped as
* `STORAGE_URI.ADAPTER_ERROR` with the original preserved as `cause`.
*/
export class StorageUriResolver {
readonly #plugins: ReadonlyMap<string, UriPlugin>;
public constructor(plugins: readonly UriPlugin[]) {
const map = new Map<string, UriPlugin>();
for (const plugin of plugins) {
const scheme = plugin.scheme.toLowerCase();
const existing = map.get(scheme);
if (existing !== undefined && existing !== plugin) {
throw new StorageUriError(
STORAGE_URI_CODES.SCHEME_CONFLICT,
`two distinct plugins registered for scheme '${scheme}'`,
{ scheme }
);
}
map.set(scheme, plugin);
}
this.#plugins = map;
Object.freeze(this);
}
/** Number of distinct schemes registered. */
public get schemeCount(): number {
return this.#plugins.size;
}
/** Test whether `scheme` is registered (case-insensitive). */
public hasScheme(scheme: string): boolean {
return this.#plugins.has(scheme.toLowerCase());
}
#dispatch(uri: string): Dispatched {
const parts = parseUri(uri);
const plugin = this.#plugins.get(parts.scheme);
if (plugin === undefined) {
throw new StorageUriError(
STORAGE_URI_CODES.UNKNOWN_SCHEME,
`no plugin registered for scheme '${parts.scheme}'`,
{ uri, scheme: parts.scheme }
);
}
const parsed = plugin.parse(uri);
const out: Dispatched = {
plugin,
scheme: parts.scheme,
key: parsed.key,
...(parsed.meta !== undefined ? { meta: parsed.meta } : {}),
};
return out;
}
/** Open a `ReadableStream<Uint8Array>` view of the resource at `uri`. */
public async open(
uri: string,
signal?: AbortSignal
): Promise<ReadableStream<Uint8Array>> {
return this.#run("open", uri, signal, async (d) =>
openViaPlugin(d.plugin, uri, d.key, signal)
);
}
/** Read the entire resource at `uri` into a `Uint8Array`. */
public async read(uri: string, signal?: AbortSignal): Promise<Uint8Array> {
return this.#run("read", uri, signal, async (d) => {
const bytes = await d.plugin.adapter.get(d.key);
if (bytes === undefined) {
throw new StorageUriError(
STORAGE_URI_CODES.NOT_FOUND,
"resource not found",
{ uri, scheme: d.scheme }
);
}
return bytes;
});
}
/** Write `body` to the resource at `uri`. */
public async write(
uri: string,
body: Uint8Array | ReadableStream<Uint8Array>,
signal?: AbortSignal
): Promise<void> {
await this.#run("write", uri, signal, async (d) => {
await writeViaPlugin(d.plugin, uri, d.key, body, signal);
return undefined;
});
}
/** Fetch metadata or `undefined` if the adapter cannot answer cheaply. */
public async stat(
uri: string,
signal?: AbortSignal
): Promise<StatResult | undefined> {
return this.#run("stat", uri, signal, async (d) => {
const adapter = d.plugin.adapter as typeof d.plugin.adapter &
StreamingStorageExtensions;
if (typeof adapter.getMetadata !== "function") return undefined;
return adapter.getMetadata(d.key, signal);
});
}
/** Test whether a resource exists at `uri`. */
public async exists(uri: string, signal?: AbortSignal): Promise<boolean> {
return this.#run("exists", uri, signal, async (d) =>
d.plugin.adapter.exists(d.key)
);
}
/** Delete the resource at `uri`. Returns the adapter's boolean. */
public async remove(uri: string, signal?: AbortSignal): Promise<boolean> {
return this.#run("remove", uri, signal, async (d) =>
d.plugin.adapter.delete(d.key)
);
}
/** Enumerate URIs sharing the input's key as a prefix. */
public async list(
uri: string,
options?: ListUriOptions,
signal?: AbortSignal
): Promise<ListUriResult> {
return this.#run("list", uri, signal, async (d) =>
listViaPlugin(d.plugin, uri, d.key, d.meta, options)
);
}
async #run<T>(
op: ResolverOp,
uri: string,
signal: AbortSignal | undefined,
body: (d: Dispatched) => Promise<T>
): Promise<T> {
const start = Date.now();
let scheme = "unknown";
const tracer = getInstruments().tracer;
const span = tracer.startSpan(`storage.uri.${op}`, {
attributes: { "storage.uri": redactUri(uri) },
});
try {
if (signal?.aborted === true) {
throw new StorageUriError(
STORAGE_URI_CODES.ABORTED,
"signal aborted",
{
uri,
}
);
}
const dispatched = this.#dispatch(uri);
scheme = dispatched.scheme;
span.setAttribute("storage.uri.scheme", scheme);
span.setAttribute("storage.uri.key", dispatched.key);
const result = await body(dispatched);
recordOp({
op,
scheme,
outcome: "success",
durationMs: Date.now() - start,
});
return result;
} catch (err) {
const mapped = this.#wrap(err, uri, scheme);
const mappedScheme =
mapped instanceof StorageUriError
? (mapped.scheme ?? scheme)
: scheme;
const outcome: "not_found" | "aborted" | "error" =
mapped.code === STORAGE_URI_CODES.NOT_FOUND
? "not_found"
: mapped.code === STORAGE_URI_CODES.ABORTED
? "aborted"
: "error";
recordOp({
op,
scheme: mappedScheme,
outcome,
durationMs: Date.now() - start,
code: mapped.code,
});
throw mapped;
} finally {
span.end();
}
}
#wrap(err: unknown, uri: string, scheme: string): SuaveplanError {
if (err instanceof SuaveplanError) return err;
const opts: { uri: string; scheme: string; cause?: unknown } = {
uri,
scheme,
};
if (err instanceof Error) opts.cause = err;
return new StorageUriError(
STORAGE_URI_CODES.ADAPTER_ERROR,
"adapter threw a non-SuaveplanError",
opts
);
}
}
/** Frozen-resolver factory; preferred over `new StorageUriResolver(...)`. */
export function createStorageUriResolver(
plugins: readonly UriPlugin[]
): StorageUriResolver {
return new StorageUriResolver(plugins);
}

View File

@@ -0,0 +1,63 @@
# resolver.writes
## Purpose
Implements the `write(uri, body, signal?)` semantics. Accepts either
`Uint8Array` or `ReadableStream<Uint8Array>` for the body, forwards a native
`createWriteStream` when the adapter exposes one and the body is a stream,
and otherwise drains the stream (via `new Response(stream).arrayBuffer()`)
into a single `adapter.set` call. The drain path increments
`storage_uri.buffered_write_total{scheme}` so operators can alert on
mis-composed adapters that quietly buffer large payloads.
`SuaveplanError` subclasses thrown by `adapter.set` (any package's
namespaced error — `DriveUriError`, `StorageHttpError`, etc.) pass through
unchanged so consumers can route on the original `code`. Non-namespaced
throwables (plain `Error`, foreign objects) are wrapped as
`STORAGE_URI.WRITE_UNSUPPORTED` with the original preserved as `cause`,
giving a normalised failure mode for read-only adapters that lack their
own error class.
## Features
- Branch on `body` shape via `getReader` duck-test.
- Forward `createWriteStream` only when present AND body is a stream.
- Drain-and-set fallback for both buffered bodies and missing
`createWriteStream`.
- `SuaveplanError` pass-through on `adapter.set` throw; `WRITE_UNSUPPORTED`
wrap with `cause` preserved for non-namespaced throwables only.
## Usage: Basic
```ts
import { writeViaPlugin } from "@suaveplan/storage-uri/resolver";
await writeViaPlugin(plugin, "file:///a.bin", "a.bin", new Uint8Array([1, 2, 3]), undefined);
```
## Usage: Advanced
```ts
async function writeStream(resolver: StorageUriResolver, uri: string, src: AsyncIterable<Uint8Array>): Promise<void> {
const stream = new ReadableStream<Uint8Array>({
async start(ctrl) {
for await (const chunk of src) ctrl.enqueue(chunk);
ctrl.close();
},
});
await resolver.write(uri, stream);
}
```
## API Reference
- `function writeViaPlugin(plugin, uri, key, body, signal): Promise<void>`
## Implementation Notes
`new Response(stream).arrayBuffer()` is the cheapest cross-runtime way to
drain a Web Stream into a contiguous buffer; it works in Node 18+, Bun, and
every browser without any polyfill. The `WRITE_UNSUPPORTED` re-wrap is
distinct from the `ADAPTER_ERROR` wrap in `resolver.ts` because callers
typically branch on these two conditions differently — `WRITE_UNSUPPORTED`
is a capability miss while `ADAPTER_ERROR` is a transient upstream fault.

View File

@@ -0,0 +1,247 @@
import type { Counter, Meter, MeterProvider } from "@opentelemetry/api";
import { SuaveplanError } from "@suaveplan/error";
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import { MemoryStorageLike } from "../../tests/setup/test-helpers.js";
import {
STORAGE_URI_CODES,
StorageUriError,
} from "../error-codes/error-codes.js";
import { resetInstruments, setStorageUriMeterProvider } from "../otel/otel.js";
import type { UriPlugin } from "../types/plugin.types.js";
import { createStorageUriResolver } from "./resolver.js";
function basicPlugin(scheme: string, adapter: MemoryStorageLike): UriPlugin {
return {
scheme,
adapter,
parse(uri) {
const after = uri.slice(uri.indexOf("://") + 3);
const slash = after.indexOf("/");
return { key: slash >= 0 ? after.slice(slash + 1) : "" };
},
};
}
interface Sample {
value: number;
attrs: Record<string, unknown> | undefined;
}
class StubCounter implements Counter {
public adds: Sample[] = [];
public add(v: number, attrs?: Record<string, unknown>): void {
this.adds.push({ value: v, attrs });
}
}
class StubMeter implements Meter {
public counters: Map<string, StubCounter> = new Map();
public createCounter(name: string): Counter {
const c = new StubCounter();
this.counters.set(name, c);
return c;
}
public createHistogram(): { record(): void } {
return { record: () => {} };
}
public createUpDownCounter(): never {
throw new Error("unused");
}
public createObservableCounter(): never {
throw new Error("unused");
}
public createObservableUpDownCounter(): never {
throw new Error("unused");
}
public createObservableGauge(): never {
throw new Error("unused");
}
public addBatchObservableCallback(): void {}
public removeBatchObservableCallback(): void {}
public createGauge(): never {
throw new Error("unused");
}
}
class StubMeterProvider implements MeterProvider {
public meter = new StubMeter();
public getMeter(): Meter {
return this.meter;
}
}
describe("resolver.writes", () => {
let provider: StubMeterProvider;
beforeEach(() => {
provider = new StubMeterProvider();
setStorageUriMeterProvider(provider);
resetInstruments();
});
afterEach(() => {
setStorageUriMeterProvider(undefined);
resetInstruments();
});
it("forwards Uint8Array body directly to adapter.set", {
timeout: 5000,
}, async () => {
const ad = new MemoryStorageLike();
const r = createStorageUriResolver([basicPlugin("file", ad)]);
const bytes = new Uint8Array([1, 2, 3]);
await r.write("file://host/a.bin", bytes);
expect(ad.setCalls).toBe(1);
expect(ad.store.get("a.bin")).toEqual(bytes);
const counter = provider.meter.counters.get(
"storage_uri.buffered_write_total"
);
expect(counter?.adds.length ?? 0).toBe(0);
});
it("drains ReadableStream into single set call", {
timeout: 5000,
}, async () => {
const ad = new MemoryStorageLike();
const r = createStorageUriResolver([basicPlugin("file", ad)]);
const chunks = [
new Uint8Array([1, 2]),
new Uint8Array([3, 4]),
new Uint8Array([5]),
];
const stream = new ReadableStream<Uint8Array>({
start(c) {
for (const ch of chunks) c.enqueue(ch);
c.close();
},
});
await r.write("file://host/a.bin", stream);
expect(ad.setCalls).toBe(1);
expect(Array.from(ad.store.get("a.bin") as Uint8Array)).toEqual([
1, 2, 3, 4, 5,
]);
const counter = provider.meter.counters.get(
"storage_uri.buffered_write_total"
);
expect(counter?.adds.length).toBe(1);
expect(counter?.adds[0]?.attrs).toMatchObject({ scheme: "file" });
});
it("wraps non-Suaveplan throw on set as WRITE_UNSUPPORTED", {
timeout: 5000,
}, async () => {
const ad = new MemoryStorageLike();
const original = new Error("read-only");
ad.set = async () => {
throw original;
};
const r = createStorageUriResolver([basicPlugin("https", ad)]);
let threw: unknown;
try {
await r.write("https://cdn/x", new Uint8Array([1]));
} catch (err) {
threw = err;
}
expect(threw).toBeInstanceOf(StorageUriError);
const e = threw as StorageUriError;
expect(e.code).toBe(STORAGE_URI_CODES.WRITE_UNSUPPORTED);
expect(e.cause).toBe(original);
});
it("passes through thrown SuaveplanError subclasses on write", {
timeout: 5000,
}, async () => {
// A namespaced SuaveplanError thrown from `adapter.set` (e.g. a
// `DriveUriError("WRITE_UNSUPPORTED")`) must reach the caller
// with its original `code` — no `STORAGE_URI.WRITE_UNSUPPORTED`
// envelope is added.
class ForeignError extends SuaveplanError<"FOO.WRITE_DENIED"> {
public constructor() {
super("FOO.WRITE_DENIED", "downstream-defined write rejection");
}
}
const original = new ForeignError();
const ad = new MemoryStorageLike();
ad.set = async () => {
throw original;
};
const r = createStorageUriResolver([basicPlugin("drive", ad)]);
let threw: unknown;
try {
await r.write("drive://primary/x", new Uint8Array([1]));
} catch (err) {
threw = err;
}
expect(threw).toBe(original);
expect((threw as ForeignError).code).toBe("FOO.WRITE_DENIED");
expect(threw).not.toBeInstanceOf(StorageUriError);
});
it("forwards createWriteStream when adapter exposes it and body is a stream", {
timeout: 5000,
}, async () => {
const ad = new MemoryStorageLike();
let captured: ReadableStream<Uint8Array> | undefined;
Object.assign(ad, {
async createWriteStream(
_k: string,
body: ReadableStream<Uint8Array>
) {
captured = body;
},
});
const r = createStorageUriResolver([basicPlugin("file", ad)]);
const stream = new ReadableStream<Uint8Array>({
start(c) {
c.enqueue(new Uint8Array([1]));
c.close();
},
});
await r.write("file://h/a", stream);
expect(captured).toBe(stream);
expect(ad.setCalls).toBe(0);
});
it("wraps non-Error set throwables as WRITE_UNSUPPORTED with no cause", {
timeout: 5000,
}, async () => {
const ad = new MemoryStorageLike();
ad.set = async () => {
throw "string-thrown";
};
const r = createStorageUriResolver([basicPlugin("file", ad)]);
let threw: unknown;
try {
await r.write("file://h/a", new Uint8Array([1]));
} catch (err) {
threw = err;
}
expect(threw).toBeInstanceOf(StorageUriError);
expect((threw as StorageUriError).code).toBe(
STORAGE_URI_CODES.WRITE_UNSUPPORTED
);
expect((threw as StorageUriError).cause).toBeUndefined();
});
it("re-throws StorageUriError unchanged when adapter raises one on set", {
timeout: 5000,
}, async () => {
const inner = new StorageUriError(STORAGE_URI_CODES.NOT_FOUND, "x");
const ad = new MemoryStorageLike();
ad.set = async () => {
throw inner;
};
const r = createStorageUriResolver([basicPlugin("file", ad)]);
let threw: unknown;
try {
await r.write("file://h/a", new Uint8Array([1]));
} catch (err) {
threw = err;
}
expect(threw).toBe(inner);
});
});

View File

@@ -0,0 +1,75 @@
import { SuaveplanError } from "@suaveplan/error";
import {
STORAGE_URI_CODES,
StorageUriError,
} from "../error-codes/error-codes.js";
import { recordBufferedWrite } from "../otel/otel.js";
import type {
StreamingStorageExtensions,
UriPlugin,
} from "../types/plugin.types.js";
/**
* Implement `write(uri, body)` semantics for a dispatched plugin.
*
* `body` may be `Uint8Array` or `ReadableStream<Uint8Array>`. When the
* adapter exposes `createWriteStream` AND `body` is a stream, the call is
* forwarded directly. Otherwise the body is drained into a single
* `Uint8Array` (via `new Response(body).arrayBuffer()` for stream inputs)
* and a single `adapter.set(key, bytes)` call is issued. The drain path
* increments `storage_uri.buffered_write_total{scheme}`.
*
* Adapter throwables that are `SuaveplanError` subclasses (any package's
* namespaced error — e.g. `DriveUriError`, `StorageHttpError`) pass through
* unchanged so consumers can route on the original `code`. Non-namespaced
* throwables (plain `Error`, foreign objects) are wrapped as
* `STORAGE_URI.WRITE_UNSUPPORTED` with the original preserved as `cause`,
* so a read-only adapter without its own error class still surfaces a
* single normalised failure mode.
*/
export async function writeViaPlugin(
plugin: UriPlugin,
uri: string,
key: string,
body: Uint8Array | ReadableStream<Uint8Array>,
signal: AbortSignal | undefined
): Promise<void> {
const adapter = plugin.adapter as typeof plugin.adapter &
StreamingStorageExtensions;
const isStream =
typeof (body as ReadableStream<Uint8Array>).getReader === "function";
try {
if (isStream && typeof adapter.createWriteStream === "function") {
await adapter.createWriteStream(
key,
body as ReadableStream<Uint8Array>,
signal
);
return;
}
let bytes: Uint8Array;
if (isStream) {
const buf = await new Response(
body as ReadableStream<Uint8Array>
).arrayBuffer();
bytes = new Uint8Array(buf);
recordBufferedWrite(plugin.scheme);
} else {
bytes = body as Uint8Array;
}
await adapter.set(key, bytes);
} catch (err) {
if (err instanceof SuaveplanError) throw err;
const opts: {
uri: string;
scheme: string;
cause?: unknown;
} = { uri, scheme: plugin.scheme };
if (err instanceof Error) opts.cause = err;
throw new StorageUriError(
STORAGE_URI_CODES.WRITE_UNSUPPORTED,
"adapter rejected write",
opts
);
}
}

View File

@@ -0,0 +1,10 @@
export type {
ParsedUri,
StatResult,
StreamingStorageExtensions,
UriPlugin,
} from "./plugin.types.js";
export type {
ListUriOptions,
ListUriResult,
} from "./resolver.types.js";

View File

@@ -0,0 +1,72 @@
# plugin.types
## Purpose
Declares the public type surface every adapter package targets when shipping
its own `uriPlugin`. The resolver knows nothing about S3 buckets, CDN zones,
or filesystem roots; instead it accepts any object conforming to
`UriPlugin`, which is the structural contract between this package and every
sibling `storage-*` adapter.
The same module declares the optional `StreamingStorageExtensions` interface
the resolver duck-types for `createReadStream`, `createWriteStream`, and
`getMetadata`, plus the `StatResult` shape returned by
`StorageUriResolver.stat`. None of these types carry runtime code.
## Features
- `UriPlugin` — `{ scheme, adapter, parse, build? }` carrying the entire
scheme-handler contract.
- `ParsedUri` — return type of `plugin.parse(uri)`; `{ key, meta? }`.
- `StreamingStorageExtensions` — duck-typed optional surface.
- `StatResult` — `{ size?, etag?, lastModified?, contentType? }`.
## Usage: Basic
```ts
import type { UriPlugin } from "@suaveplan/storage-uri/types";
const plugin: UriPlugin = {
scheme: "memory",
adapter,
parse(uri) { return { key: uri.slice("memory://".length) }; },
};
```
## Usage: Advanced
```ts
import type { UriPlugin, StreamingStorageExtensions } from "@suaveplan/storage-uri/types";
function makePlugin(adapter: StorageLike & StreamingStorageExtensions): UriPlugin {
return {
scheme: "rackspace",
adapter,
parse(uri) {
const url = new URL(uri);
return { key: url.pathname.slice(1), meta: { container: url.host } };
},
build(key, meta) {
const container = meta?.container ?? "";
return `rackspace://${container}/${key}`;
},
};
}
```
## API Reference
- `interface ParsedUri { key: string; meta?: Record<string,string> }`
- `interface UriPlugin { scheme; adapter; parse; build? }`
- `interface StreamingStorageExtensions { createReadStream?; createWriteStream?; getMetadata? }`
- `interface StatResult { size?; etag?; lastModified?; contentType? }`
## Implementation Notes
`adapter` is typed as `StorageLike` (from `@suaveplan/types`), NOT
`AbstractStorage` from `@suaveplan/storage`. That lets in-memory
test stubs satisfy the contract without
pulling in the storage runtime. `meta` is a `Record<string, string>` rather
than a generic shape so all adapter-specific addressing (bucket, zone,
container, query) is stringly-typed at the plugin boundary — each adapter is
free to expose typed helpers on top.

View File

@@ -0,0 +1,67 @@
import type { StorageLike } from "@suaveplan/types";
/**
* Result of parsing a scheme-specific URI into its addressable storage key
* plus optional auxiliary addressing dimensions (CDN zone, S3 bucket, container).
*/
export interface ParsedUri {
/** The storage key passed to the underlying `StorageLike` adapter. */
readonly key: string;
/**
* Adapter-specific extras (`bucket`, `container`, `zone`, `query`, ...).
* The resolver never inspects this — plugins are responsible for forwarding
* the values to their bound adapter as needed.
*/
readonly meta?: Record<string, string>;
}
/**
* A scheme handler registered with a `StorageUriResolver`.
*
* Each plugin owns ONE scheme (e.g. `"s3"`, `"file"`, `"https"`) and exposes:
*
* - `adapter` — a `StorageLike` against which `parse(uri).key` is dispatched.
* - `parse(uri)` — splits the URI into a storage key plus optional meta.
* - `build(key, meta?)` — OPTIONAL inverse used by `list()` re-serialisation
* and by the conformance harness's round-trip assertions. Plugins without
* `build` cause `list()` to throw `STORAGE_URI.LIST_UNSUPPORTED`.
*/
export interface UriPlugin {
readonly scheme: string;
readonly adapter: StorageLike;
parse(uri: string): ParsedUri;
build?(key: string, meta?: Record<string, string>): string;
}
/**
* Optional duck-typed extensions an adapter MAY implement on top of
* `StorageLike`. Used by `open()` and `write()` to forward native streaming
* surfaces when present and fall back to the buffered path when not.
*/
export interface StreamingStorageExtensions {
createReadStream?(
key: string,
signal?: AbortSignal
): ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>;
createWriteStream?(
key: string,
body: ReadableStream<Uint8Array>,
signal?: AbortSignal
): Promise<void>;
getMetadata?(
key: string,
signal?: AbortSignal
): Promise<StatResult | undefined>;
}
/**
* Metadata returned by `StorageUriResolver.stat(uri)`. Adapters that cannot
* answer cheaply return `undefined`; callers MUST treat that as "unknown",
* not "absent" (use `exists()` for absence).
*/
export interface StatResult {
readonly size?: number;
readonly etag?: string;
readonly lastModified?: Date;
readonly contentType?: string;
}

View File

@@ -0,0 +1,65 @@
# resolver.types
## Purpose
Declares the option and result types carried by `StorageUriResolver.list`.
These live in their own module so consumers can import the shapes without
pulling in the resolver class itself — handy when type-checking plugin
authoring code that never instantiates a resolver but does need to type its
own list results.
## Features
- `ListUriOptions` — `{ limit?, continuationToken? }`. The `prefix` is
derived from the input URI by the resolver; consumers never set it
directly.
- `ListUriResult` — `{ uris, hasMore, continuationToken? }` returned by
`StorageUriResolver.list`.
## Usage: Basic
```ts
import type { ListUriOptions, ListUriResult } from "@suaveplan/storage-uri/types";
const opts: ListUriOptions = { limit: 50 };
const out: ListUriResult = await resolver.list("s3://bucket/raw/", opts);
```
## Usage: Advanced
```ts
import type { ListUriOptions, ListUriResult } from "@suaveplan/storage-uri/types";
async function* drain(
resolver: StorageUriResolver,
base: string,
): AsyncIterable<string> {
let token: string | undefined;
do {
const opts: ListUriOptions = token === undefined ? {} : { continuationToken: token };
const page: ListUriResult = await resolver.list(base, opts);
yield* page.uris;
token = page.continuationToken;
} while (token !== undefined);
}
```
## API Reference
- `interface ListUriOptions { limit?: number; continuationToken?: string }`
- `interface ListUriResult { uris: readonly string[]; hasMore: boolean; continuationToken?: string }`
## Implementation Notes
`uris` is a `readonly string[]` rather than a mutable array so consumers
cannot accidentally mutate the resolver's return value. `continuationToken`
is intentionally opaque — the resolver forwards whatever the adapter
returned, and consumers MUST pass it back unchanged. Adapters that cannot
paginate return `hasMore: false` and never populate `continuationToken`;
consumers SHOULD treat `hasMore === false` as the loop's termination
condition rather than `continuationToken === undefined`. The split between
`ListUriOptions` and `ListUriResult` mirrors `StorageLike.list`'s own
input/output shapes one-for-one, so adapter authors who already implement
`StorageLike.list` need no extra adaptation layer to satisfy
`StorageUriResolver.list` — only `build()` for URI re-serialisation. Both
shapes are deeply readonly so consumers cannot mutate them after receipt.

View File

@@ -0,0 +1,21 @@
/**
* Options accepted by `StorageUriResolver.list(uri, options?, signal?)`.
*
* `prefix` is implicit — derived from the input URI's key portion — so this
* type carries only the pagination knobs forwarded to `StorageLike.list`.
*/
export interface ListUriOptions {
readonly limit?: number;
readonly continuationToken?: string;
}
/**
* Result returned by `StorageUriResolver.list`. Keys are re-serialised back to
* fully-qualified URIs via the plugin's `build()` function; `hasMore` and
* `continuationToken` are forwarded verbatim from the underlying adapter.
*/
export interface ListUriResult {
readonly uris: readonly string[];
readonly hasMore: boolean;
readonly continuationToken?: string;
}

View File

@@ -0,0 +1,108 @@
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createNodeFsStorage } from "@suaveplan/storage-file";
import { createHttpStorage } from "@suaveplan/storage-http";
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import type { StorageLike } from "@suaveplan/types";
import { createStorageUriResolver } from "../../src/index.js";
import type { UriPlugin } from "../../src/types/plugin.types.js";
function fileUriPlugin(adapter: StorageLike, rootDir: string): UriPlugin {
return {
scheme: "file",
adapter,
parse(uri) {
// file:///abs/path → key relative to rootDir.
const prefix = "file://";
const rest = uri.startsWith(prefix)
? uri.slice(prefix.length)
: uri;
const abs = rest.startsWith("/") ? rest : `/${rest}`;
const root = rootDir.endsWith("/") ? rootDir : `${rootDir}/`;
const key = abs.startsWith(root)
? abs.slice(root.length)
: abs.replace(/^\//, "");
return { key };
},
build(key) {
const root = rootDir.endsWith("/") ? rootDir : `${rootDir}/`;
return `file://${root}${key}`;
},
};
}
function httpsUriPlugin(adapter: StorageLike): UriPlugin {
return {
scheme: "https",
adapter,
parse(uri) {
const url = new URL(uri);
return {
key: url.pathname.replace(/^\//, ""),
meta: { host: url.host },
};
},
};
}
describe("cross-scheme integration", () => {
let root: string;
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), "storage-uri-int-"));
});
afterEach(async () => {
await rm(root, { recursive: true, force: true });
});
it("composes a resolver with real storage-file and storage-http adapters", {
timeout: 15000,
}, async () => {
const fileAdapter = createNodeFsStorage({ rootDir: root });
// In-process fetch stub serving a single payload at a known URL.
const payload = new TextEncoder().encode("from-http");
const httpAdapter = createHttpStorage({
baseUrl: "https://origin.test/",
fetch: async (input: unknown) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: input instanceof Request
? input.url
: String((input as { url?: string }).url ?? input);
if (url.endsWith("/poster.bin")) {
return new Response(payload, { status: 200 });
}
return new Response("not found", { status: 404 });
},
});
const resolver = createStorageUriResolver([
fileUriPlugin(fileAdapter, root),
httpsUriPlugin(httpAdapter),
]);
// HTTP read.
const httpBytes = await resolver.read("https://origin.test/poster.bin");
expect(new TextDecoder().decode(httpBytes)).toBe("from-http");
// File round-trip via resolver.
const fileUri = `file://${root}/cache/copy.bin`;
await resolver.write(fileUri, httpBytes);
const back = await resolver.read(fileUri);
expect(new TextDecoder().decode(back)).toBe("from-http");
expect(await resolver.exists(fileUri)).toBe(true);
});
});

View File

@@ -0,0 +1,5 @@
// Polyfills for test environment — none required; Node 18+ provides
// `URL`, `AbortController`, `ReadableStream`, and `Response` natively.
// File exists so vitest has a stable setup entry and to mirror sibling
// storage packages.
export {};

View File

@@ -0,0 +1,54 @@
// Test-only helpers used by the resolver test suite. Coverage excluded via
// vitest config's `*.test.ts` filter — this file is `.ts` rather than
// `.test.ts` so it can be imported from multiple test files.
import type { StorageLike } from "@suaveplan/types";
/**
* Minimal in-memory `StorageLike` for resolver dispatch tests. NOT a mock —
* it's a real implementation of the structural contract, written by hand
* because rule 2 forbids `vi.mock`. Test files attach this to a
* `UriPlugin` to exercise scheme dispatch.
*/
export class MemoryStorageLike implements StorageLike {
public readonly store = new Map<string, Uint8Array>();
public lastSignal: AbortSignal | undefined;
public getCalls = 0;
public setCalls = 0;
public deleteCalls = 0;
public existsCalls = 0;
public listCalls = 0;
public async get(key: string): Promise<Uint8Array | undefined> {
this.getCalls += 1;
return this.store.get(key);
}
public async set(key: string, value: Uint8Array): Promise<void> {
this.setCalls += 1;
this.store.set(key, value);
}
public async delete(key: string): Promise<boolean> {
this.deleteCalls += 1;
return this.store.delete(key);
}
public async exists(key: string): Promise<boolean> {
this.existsCalls += 1;
return this.store.has(key);
}
public async list(options?: { prefix?: string; limit?: number }): Promise<{
keys: string[];
total: number;
hasMore: boolean;
continuationToken?: string | undefined;
}> {
this.listCalls += 1;
const prefix = options?.prefix ?? "";
const all = [...this.store.keys()].filter((k) => k.startsWith(prefix));
const limit = options?.limit ?? all.length;
const keys = all.slice(0, limit);
return { keys, total: all.length, hasMore: keys.length < all.length };
}
public async clear(): Promise<void> {
this.store.clear();
}
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts", "tests/**/*.ts"],
"exclude": ["dist", "node_modules"]
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts", "dist", "node_modules"]
}

View File

@@ -0,0 +1,35 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
root: __dirname,
cacheDir: "../../../node_modules/.vite/packages/storage-uri",
plugins: [
dts({
entryRoot: "src",
tsconfigPath: "./tsconfig.lib.json",
insertTypesEntry: true,
}),
],
build: {
outDir: "./dist",
emptyOutDir: true,
reportCompressedSize: true,
sourcemap: true,
lib: { entry: "src/index.ts", formats: ["es"] },
rollupOptions: {
treeshake: true,
output: {
preserveModules: true,
preserveModulesRoot: "src",
entryFileNames: "[name].js",
},
external: [
"@opentelemetry/api",
"@suaveplan/error",
"@suaveplan/types",
"tslib",
],
},
},
});

View File

@@ -0,0 +1,28 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: false,
environment: "node",
pool: "forks",
setupFiles: ["./tests/setup/polyfills.ts"],
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
exclude: ["dist/**", "node_modules/**"],
coverage: {
provider: "istanbul",
reporter: ["text", "html", "lcov"],
include: ["src/**/*.ts"],
exclude: [
"src/**/*.test.ts",
"src/**/*.spec-conformance.test.ts",
"src/**/*.types.ts",
"src/**/index.ts",
"src/**/*.bench.ts",
"src/__benchmarks__/**",
"tests/**",
],
thresholds: { statements: 100, branches: 100, functions: 100, lines: 100 },
},
testTimeout: 30000,
},
});