13 KiB
Design — @suaveplan/drive-uri
Tier and dependency posture
Universal tier. Zero node: / bun: imports in src/. Runtime deps:
@suaveplan/error—defineCodes,SuaveplanError@suaveplan/storage-uri—UriPlugin,ParsedUri,parseUri,runUriPluginConformance,StorageUriError(forcausechain typing)@suaveplan/drive—Drive(consumed type),ReadOptions,WriteOptions@suaveplan/drive-template—DriveTemplate(type only — schema is never decoded insidedrive-uri)@suaveplan/types—StorageLike(the contract the adapter shim satisfies)@opentelemetry/api— meter + tracertslib
@suaveplan/drive-mesh, drive-plugin-chunks, drive-transport-https, drive-webrtc are NOT runtime deps. The consumer wires those into the Drive instance they pass in; drive-uri only sees Drive. Same posture as storage-uri and the storage providers.
DevDeps: @suaveplan/testing, @suaveplan/storage, @suaveplan/storage-file, @suaveplan/drive-plugin-chunks, @suaveplan/drive-mesh, @vitest/coverage-istanbul, fast-check, typescript, vite, vite-plugin-dts, vitest. The integration tests require real Drive + chunksPlugin + storage-file pairs (no mocks per CLAUDE.md rule 2).
File layout
packages/drive/drive-uri/
package.json
README.md
src/
index.ts # explicit named re-exports (rule 10)
error-codes/
error-codes.ts # defineCodes("DRIVE_URI", [...]), DriveUriError
error-codes.test.ts
error-codes.md
index.ts
adapter/
drive-storage-like.ts # Drive → StorageLike shim
drive-storage-like.test.ts
drive-storage-like.md
index.ts
plugins/
drive/
drive.ts # driveUriPlugin(options): UriPlugin
drive.streams.ts # createReadStream/createWriteStream variants
drive.test.ts
drive.streams.test.ts
drive.md
drive.streams.md
index.ts
drive-mesh/
drive-mesh.ts # driveMeshUriPlugin(options): UriPlugin
drive-mesh.test.ts
drive-mesh.md
index.ts
drive-template/
drive-template.ts # driveTemplateUriPlugin(options): UriPlugin
drive-template.test.ts
drive-template.md
index.ts
types/
plugin-options.types.ts # DriveUriPluginOptions, DriveMeshPluginOptions, DriveTemplatePluginOptions, TemplateProvider
plugin-options.types.md
index.ts
otel/
otel.ts # meter + tracer wiring, recordOp, recordTemplateHydration
otel.test.ts
otel.md
index.ts
conformance/
conformance.ts # re-export of runUriPluginConformance from storage-uri
conformance.md # how to author the fixture set for a drive-uri plugin
index.ts
tests/
setup/polyfills.ts
integration/
drive-roundtrip.test.ts # real Drive + storage-file
template-roundtrip.test.ts # real Drive + chunksPlugin + storage-file + in-process templateProvider
compose-with-storage-uri.test.ts # one resolver, file:// + drive:// in the same dispatch
tsconfig.json
tsconfig.lib.json
vite.config.ts
vitest.config.ts
Every implementation .ts outside the excluded set (*.test.ts, *.types.ts inside a feature dir, index.ts) has a sibling .md of ≥200 words with the 6 required sections.
Per AGENTS §10 (no redundant naming): the plugins/ parent does NOT cause its children to be named *-plugin.ts — children are drive/drive.ts, drive-mesh/drive-mesh.ts, drive-template/drive-template.ts. Each exports its *UriPlugin factory and the plugin-specific types.
Adapter shim — Drive → StorageLike
export class DriveStorageLikeAdapter implements StorageLike {
constructor(private readonly drive: Drive) {}
async get(key: string): Promise<Uint8Array | undefined> {
try {
return await this.drive.read(key);
} catch (err) {
if (isDriveNotFound(err)) return undefined;
throw err;
}
}
async set(key: string, value: Uint8Array): Promise<void> {
await this.drive.write(key, value);
}
async delete(key: string): Promise<boolean> {
return this.drive.delete(key);
}
async exists(key: string): Promise<boolean> {
return (await this.drive.stat(key)) !== null;
}
async list(options?: { prefix?: string; limit?: number }): Promise<ListResult> {
return this.drive.list(options ?? {});
}
async clear(): Promise<void> {
throw new Error("clear() is not supported on Drive — use targeted delete instead");
}
// Optional: bridge createReadStream onto the resolver's open() fast path.
createReadStream(key: string, signal?: AbortSignal): ReadableStream<Uint8Array> {
return this.drive.createReadStream(key, signal);
}
}
isDriveNotFound(err) is the only place that has to know about DRIVE.IO_FAILED's NOT_FOUND variant. The shim does not own any chunk codec, manifest fetching, peer discovery, or template validation — that all happens inside Drive via its installed middleware.
Three plugins — common shape
export function driveUriPlugin(options: DriveUriPluginOptions): UriPlugin {
validateOptions(options); // throws DRIVE_URI.INVALID_URI / DRIVE_ID_MISMATCH eagerly
const adapter = new DriveStorageLikeAdapter(options.drive);
return {
scheme: "drive",
adapter,
parse(uri) {
const parsed = parseUri(uri);
if (parsed.scheme !== "drive") throw new DriveUriError("INVALID_URI", { uri });
const driveId = parsed.authority ?? "";
if (driveId !== options.driveId) {
throw new DriveUriError("DRIVE_ID_MISMATCH", { uri, expected: options.driveId, actual: driveId });
}
return { key: parsed.path.replace(/^\//, ""), meta: { driveId } };
},
build(key, meta) {
const id = meta?.driveId ?? options.driveId;
return `drive://${id}/${key.replace(/^\//, "")}`;
},
};
}
driveMeshUriPlugin is identical in shape, swapping scheme: "drive+mesh" and capturing swarmId instead of validating driveId. The mesh plugin does not enforce a swarmId match — it captures the swarmId for instrumentation only because the Drive instance is the consumer's choice and they may use the same Drive to read from multiple swarms (the mesh transport handles peer routing).
driveTemplateUriPlugin adds the hydration step in parse (or, more accurately, in get — the parse happens synchronously, the hydration is async):
export function driveTemplateUriPlugin(options: DriveTemplatePluginOptions): UriPlugin {
if (!options.templateProvider) {
throw new DriveUriError("CAPABILITY_MISSING", { reason: "templateProvider is required" });
}
const hydrated = new Map<string, Promise<void>>(); // templateId → in-flight hydration
const adapter = wrapDriveWithHydration(options.drive, options.templateProvider, hydrated);
return {
scheme: "drive+template",
adapter,
parse(uri) { /* extract templateId + path */ },
build(key, meta) { /* re-serialise */ },
};
}
The hydration cache is a per-plugin Map<string, Promise<void>> keyed by templateId. First get(key) for a given template-id awaits templateProvider.fetchTemplate(templateId) → drive.hydrateFrom(template); concurrent reads await the same in-flight promise; subsequent reads see a resolved promise and short-circuit. The cache is intentionally not LRU — the only state it holds is "we've already hydrated this template into the Drive", and re-hydrating is idempotent at the Drive layer via TemplateNonceLru.
Error mapping
Origin (in cause) |
Re-wrap | Why |
|---|---|---|
DRIVE.IO_FAILED with NOT_FOUND |
undefined from adapter.get, then STORAGE_URI.NOT_FOUND from resolver |
Matches StorageLike contract |
DRIVE.IO_FAILED other |
STORAGE_URI.ADAPTER_ERROR (via resolver) |
Generic adapter failure |
DRIVE_MESH.ALL_PEERS_FAILED / CHUNK_UNAVAILABLE / SIG_VERIFY_FAILED |
STORAGE_URI.ADAPTER_ERROR with original code on cause |
Mesh-specific failures preserved for diagnosis |
CHUNKS.AEAD_TAG_INVALID / MANIFEST_NOT_FOUND |
STORAGE_URI.ADAPTER_ERROR with original on cause |
Chunks plugin failures |
DTP_* from hydrateFrom |
DRIVE_URI.TEMPLATE_INVALID with original on cause |
Template validation is a drive-uri-tier semantic concern |
templateProvider.fetchTemplate rejects |
DRIVE_URI.TEMPLATE_NOT_AVAILABLE with original on cause |
Distinct from validation failure |
| AbortSignal aborted before adapter call | DRIVE_URI.ABORTED |
Pre-flight short-circuit |
| URI's driveId ≠ plugin's configured driveId | DRIVE_URI.DRIVE_ID_MISMATCH |
Detected in parse, before any Drive call |
| Malformed URI | DRIVE_URI.INVALID_URI (or STORAGE_URI.INVALID_URI from parseUri) |
Either is acceptable — the resolver surfaces whichever is thrown |
OTel surface
Tracer + meter: @suaveplan/drive-uri.
Histograms:
drive_uri.op_duration_ms{op, scheme, outcome}—outcome ∈ success | not_found | error | aborted
Counters:
drive_uri.reads_total{scheme, outcome}drive_uri.writes_total{scheme, outcome}— increments even though writes throw (so operator can see misuse rate)drive_uri.deletes_total{scheme, outcome}drive_uri.errors_total{scheme, code}—codeis theDRIVE_URI.*enum orADAPTER_ERRORfor downstreamdrive_uri.template_hydrations_total{outcome}— increments once per first-use template; cache-hit reads do not increment
Spans: a single span per public op (drive.uri.read, drive.uri.write, drive.uri.stat, …). Attributes:
drive.uri— full URI minus query stringdrive.uri.schemedrive.uri.drive_id?/drive.uri.swarm_id?/drive.uri.template_id?— depending on schemedrive.uri.path— post-parse path
Query strings are dropped from drive.uri regardless of scheme. This is a contract, not an opt-in.
Conformance harness
drive-uri does not author its own conformance harness. It re-exports runUriPluginConformance from @suaveplan/storage-uri (the canonical home) and adds three planned fixture sets (one per scheme) to its own test suite. Each fixture set covers:
- Valid URIs — three representative shapes per scheme (root path, nested path, path with allowed special chars).
- Invalid URIs — wrong scheme prefix, missing authority, embedded null, oversize (> 4096 chars).
- Round-trip —
build(parse(uri).key, parse(uri).meta) === urifor each valid fixture.
This keeps the conformance contract in one place (storage-uri) and avoids drift between the two URI-plugin packages.
Composition recipe (in README)
import { createStorageUriResolver } from "@suaveplan/storage-uri";
import { fileUriPlugin } from "@suaveplan/storage-file"; // future
import { httpsUriPlugin } from "@suaveplan/storage-http"; // future
import {
driveUriPlugin,
driveMeshUriPlugin,
driveTemplateUriPlugin,
} from "@suaveplan/drive-uri";
const localDrive = new Drive({ /* local-only config */ });
const meshDrive = new Drive({ /* mesh + chunks + transports */ });
const resolver = createStorageUriResolver([
fileUriPlugin({ rootDir: "/var/media" }),
httpsUriPlugin(httpStorage),
driveUriPlugin({ drive: localDrive, driveId: "local-cache-01" }),
driveMeshUriPlugin({ drive: meshDrive, swarmId: "*", instrumentation: { includeSwarmIdInSpans: true } }),
driveTemplateUriPlugin({ drive: meshDrive, templateProvider }),
]);
const stream = await resolver.open("drive+mesh://swarmA/raw/clip.mov");
const buf = await resolver.read("drive+template://templA/manifest/clip.jpg");
One resolver, mixed schemes, same dispatch path. No second resolver class, no fall-through helper, no umbrella registry.
Trade-offs accepted
- No DriveUriResolver class. All routing through
StorageUriResolver. Loses one layer of indirection; gains: no duplicated dispatch code, no conflict-detection seam between two resolvers, simpler composition. - Drive instance is the consumer's responsibility.
drive-uridoes NOT new up a Drive. Cost: the consumer wireschunksPlugin+ transports + templates themselves. Benefit: drive-uri stays decoupled and testable; the same Drive can serve multiple URI schemes (e.g. one mesh-configured Drive backs bothdrive+mesh://anddrive+template://). - Template hydration cache is per-plugin-instance, not global. Two different
driveTemplateUriPlugininstances do not share the cache. Cost: marginal — consumers rarely instantiate two; if they do,Drive.hydrateFrom's nonce LRU still makes re-hydration idempotent at the drive tier. clear()throws. Wiping a Drive via the URI surface is a footgun; the explicit error code points the consumer atDrive.delete(path)instead.- Writes are deferred. See proposal §"Out of scope". Reading is the dominant URI use case (media pipelines, hydration sources, sharing links); writes are a separate design problem with non-trivial fan-out semantics.