first commit
This commit is contained in:
159
docs/networking/drive/drive-uri/README.md
Normal file
159
docs/networking/drive/drive-uri/README.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# @suaveplan/drive-uri - URI plugins (drive://, drive+mesh://, drive+template://) for @suaveplan/storage-uri's StorageUriResolver. Wraps a caller-supplied Drive instance in a StorageLike adapter so URI-addressed drive content resolves through the same dispatcher as storage adapters.
|
||||
|
||||
URI plugins (`drive://`, `drive+mesh://`, `drive+template://`) for
|
||||
`@suaveplan/storage-uri`'s `StorageUriResolver`. Wraps a caller-supplied
|
||||
`Drive` instance in a `StorageLike` adapter so URI-addressed drive content
|
||||
resolves through the same dispatcher as `file://`, `https://`, `s3://`, and
|
||||
the other storage adapters — without re-implementing chunk decoding, peer
|
||||
discovery, or template validation. The drive itself remains the single
|
||||
source of truth for those concerns; `drive-uri` is a routing layer.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [adapter](./src/adapter/drive-storage-like.md) — Defines `DriveStorageLikeAdapter`, the internal shim that exposes a `Drive` instance as a `StorageLike` so it can be re…
|
||||
- [drive-storage-like](./src/adapter/drive-storage-like.md) — Defines `DriveStorageLikeAdapter`, the internal shim that exposes a `Drive` instance as a `StorageLike` so it can be re…
|
||||
- [conformance](./src/conformance/conformance.md) — Re-exports the canonical URI-plugin conformance harness from `@suaveplan/storage-uri` (`runUriPluginConformance`, `UriP…
|
||||
- [conformance](./src/conformance/conformance.md) — Re-exports the canonical URI-plugin conformance harness from `@suaveplan/storage-uri` (`runUriPluginConformance`, `UriP…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Declares the seven `DRIVE_URI.*` error codes raised by `@suaveplan/drive-uri` plugins and the `DriveUriError` discrimin…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Declares the seven `DRIVE_URI.*` error codes raised by `@suaveplan/drive-uri` plugins and the `DriveUriError` discrimin…
|
||||
- [otel](./src/otel/otel.md) — OpenTelemetry instrumentation surface for `@suaveplan/drive-uri`. Exposes a meter named `@suaveplan/drive-uri` carrying…
|
||||
- [otel](./src/otel/otel.md) — OpenTelemetry instrumentation surface for `@suaveplan/drive-uri`. Exposes a meter named `@suaveplan/drive-uri` carrying…
|
||||
- [drive](./src/plugins/drive/drive.md) — `driveUriPlugin(options)` is a factory returning a `UriPlugin` registered against the `drive://` scheme. It wraps a con…
|
||||
- [drive](./src/plugins/drive/drive.md) — `driveUriPlugin(options)` is a factory returning a `UriPlugin` registered against the `drive://` scheme. It wraps a con…
|
||||
- [drive.streams](./src/plugins/drive/drive.streams.md) — `InstrumentedDriveAdapter` is the OTel-instrumented `StorageLike` shared by all three URI plugins (`drive://`, `drive+m…
|
||||
- [drive-mesh](./src/plugins/drive-mesh/drive-mesh.md) — `driveMeshUriPlugin(options)` is a factory returning a `UriPlugin` registered against the `drive+mesh://` scheme. It pa…
|
||||
- [drive-mesh](./src/plugins/drive-mesh/drive-mesh.md) — `driveMeshUriPlugin(options)` is a factory returning a `UriPlugin` registered against the `drive+mesh://` scheme. It pa…
|
||||
- [drive-template](./src/plugins/drive-template/drive-template.md) — `driveTemplateUriPlugin(options)` is a factory returning a `UriPlugin` registered against the `drive+template://` schem…
|
||||
- [drive-template](./src/plugins/drive-template/drive-template.md) — `driveTemplateUriPlugin(options)` is a factory returning a `UriPlugin` registered against the `drive+template://` schem…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/drive-uri @suaveplan/storage-uri @suaveplan/drive
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import {
|
||||
driveMeshUriPlugin,
|
||||
driveTemplateUriPlugin,
|
||||
driveUriPlugin,
|
||||
} from "@suaveplan/drive-uri";
|
||||
|
||||
const localDrive = new Drive(localStorage);
|
||||
await localDrive.initialize();
|
||||
|
||||
const meshDrive = await buildMeshConfiguredDrive(); // your wiring
|
||||
|
||||
const resolver = createStorageUriResolver([
|
||||
driveUriPlugin({ drive: localDrive, driveId: "primary" }),
|
||||
driveMeshUriPlugin({ drive: meshDrive, defaultSwarmId: "swarmA" }),
|
||||
driveTemplateUriPlugin({ drive: meshDrive, templateProvider }),
|
||||
]);
|
||||
|
||||
// Three different schemes, one resolver, one dispatch flow.
|
||||
await resolver.read("drive://primary/folder/file.bin");
|
||||
await resolver.open("drive+mesh://swarmA/raw/clip.mov");
|
||||
await resolver.read("drive+template://templA/manifest/clip.jpg");
|
||||
```
|
||||
|
||||
The three plugins share zero state by default — but you can pass the same
|
||||
`Drive` to two factories so a hydration triggered by the template plugin
|
||||
populates the file-location index that the mesh plugin then uses to fetch
|
||||
chunks. That composition is the canonical pattern for "open a template,
|
||||
then stream its files".
|
||||
|
||||
## Composition with storage-uri plugins
|
||||
|
||||
`drive-uri` plugins coexist with every other URI plugin. A single
|
||||
`StorageUriResolver` can carry a mix of schemes without any code-path
|
||||
divergence — each URI dispatches to the correct adapter based on its scheme
|
||||
prefix, and no plugin observes another plugin's state.
|
||||
|
||||
```ts
|
||||
const resolver = createStorageUriResolver([
|
||||
fileUriPlugin({ rootDir: "/var/media" }),
|
||||
httpsUriPlugin(httpStorage),
|
||||
s3UriPlugin({ bucket: "uploads", region: "eu-west-1" }),
|
||||
driveUriPlugin({ drive: localDrive, driveId: "primary" }),
|
||||
driveMeshUriPlugin({ drive: meshDrive }),
|
||||
driveTemplateUriPlugin({ drive: meshDrive, templateProvider }),
|
||||
]);
|
||||
|
||||
const bytes = await resolver.read("drive://primary/folder/file.bin");
|
||||
const headers = await resolver.read("https://cdn.example/img.png");
|
||||
```
|
||||
|
||||
## Authoring a `TemplateProvider`
|
||||
|
||||
```ts
|
||||
import type { TemplateProvider } from "@suaveplan/drive-uri";
|
||||
import type { DriveTemplate } from "@suaveplan/drive-template";
|
||||
|
||||
export const httpTemplateProvider: TemplateProvider = {
|
||||
async fetchTemplate(templateId: string, signal?: AbortSignal): Promise<DriveTemplate> {
|
||||
const res = await fetch(`/api/templates/${templateId}`, { signal });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return (await res.json()) as DriveTemplate;
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The provider is invoked exactly once per `templateId` per plugin instance —
|
||||
the hydration cache short-circuits subsequent lookups. On a transient
|
||||
network failure, the cache entry is removed so a retry is possible.
|
||||
|
||||
## Errors
|
||||
|
||||
Every drive-uri failure surfaces as `DriveUriError` with a `DRIVE_URI.*`
|
||||
code:
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| `DRIVE_URI.INVALID_URI` | URI is malformed or has an unexpected scheme |
|
||||
| `DRIVE_URI.DRIVE_ID_MISMATCH` | URI's driveId differs from plugin's configured driveId |
|
||||
| `DRIVE_URI.TEMPLATE_NOT_AVAILABLE` | `templateProvider.fetchTemplate` rejected |
|
||||
| `DRIVE_URI.TEMPLATE_INVALID` | `Drive.hydrateFrom` rejected (template validation failed) |
|
||||
| `DRIVE_URI.WRITE_UNSUPPORTED` | Initial release rejects every write |
|
||||
| `DRIVE_URI.CAPABILITY_MISSING` | Required option (drive, templateProvider) missing |
|
||||
| `DRIVE_URI.ABORTED` | `AbortSignal` was already aborted at call time |
|
||||
|
||||
Wrapping rules are documented in the design doc — `templateProvider`
|
||||
rejection becomes `TEMPLATE_NOT_AVAILABLE`; `Drive.hydrateFrom` rejection
|
||||
(any `DTP_*` code) becomes `TEMPLATE_INVALID`. The original cause is
|
||||
preserved on `err.cause` so operators can drill down to the underlying drive
|
||||
failure.
|
||||
|
||||
## Observability
|
||||
|
||||
The package emits to the `@suaveplan/drive-uri` meter and tracer:
|
||||
|
||||
- `drive_uri.op_duration_ms{op, scheme, outcome}` — duration histogram
|
||||
- `drive_uri.reads_total{scheme, outcome}`,
|
||||
`drive_uri.writes_total{scheme, outcome}`,
|
||||
`drive_uri.deletes_total{scheme, outcome}` — counters
|
||||
- `drive_uri.errors_total{scheme, code}` — error attribution
|
||||
- `drive_uri.template_hydrations_total{outcome}` — increments exactly once
|
||||
per first-use template
|
||||
|
||||
Span attributes: `drive.uri` (query-stripped), `drive.uri.scheme`, and one of
|
||||
`drive.uri.drive_id` / `drive.uri.swarm_id` / `drive.uri.template_id`
|
||||
depending on the scheme.
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/error-codes/error-codes.md`](src/error-codes/error-codes.md) — `DRIVE_URI_CODES` + `DriveUriError`
|
||||
- [`src/adapter/drive-storage-like.md`](src/adapter/drive-storage-like.md) — `DriveStorageLikeAdapter` + `isDriveNotFound`
|
||||
- [`src/plugins/drive/drive.md`](src/plugins/drive/drive.md) — `driveUriPlugin`
|
||||
- [`src/plugins/drive/drive.streams.md`](src/plugins/drive/drive.streams.md) — `InstrumentedDriveAdapter`
|
||||
- [`src/plugins/drive-mesh/drive-mesh.md`](src/plugins/drive-mesh/drive-mesh.md) — `driveMeshUriPlugin`
|
||||
- [`src/plugins/drive-template/drive-template.md`](src/plugins/drive-template/drive-template.md) — `driveTemplateUriPlugin`
|
||||
- [`src/types/plugin-options.types.md`](src/types/plugin-options.types.md) — option-bag interfaces + `TemplateProvider`
|
||||
- [`src/otel/otel.md`](src/otel/otel.md) — OpenTelemetry surface
|
||||
- [`src/conformance/conformance.md`](src/conformance/conformance.md) — re-export of `runUriPluginConformance`
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# `@suaveplan/drive-uri/adapter`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines `DriveStorageLikeAdapter`, the internal shim that exposes a `Drive`
|
||||
instance as a `StorageLike` so it can be registered as the `adapter` of a
|
||||
`UriPlugin` consumed by `@suaveplan/storage-uri`'s `StorageUriResolver`. The
|
||||
shim is the join point between the drive-tier API (which throws on missing
|
||||
files) and the storage-tier contract (which returns `undefined` for missing
|
||||
keys) — every other failure path is preserved unchanged so the resolver can
|
||||
attach its own `STORAGE_URI.ADAPTER_ERROR` wrap.
|
||||
|
||||
A second export, `isDriveNotFound`, walks the `cause` chain of a thrown value
|
||||
looking for any `SuaveplanError` whose `code` ends in `.NOT_FOUND`. This is
|
||||
the only place that has to know about Drive's not-found vocabulary; every
|
||||
plugin defers the translation to the shim.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements every required member of `StorageLike` (`get`, `set`, `delete`,
|
||||
`exists`, `list`, `clear`) plus the optional `createReadStream` extension
|
||||
used by `StorageUriResolver.open()`'s native-stream fast path.
|
||||
- Translates `Drive.read` not-found rejections into `undefined` from `get()`
|
||||
so the resolver's existing `undefined → STORAGE_URI.NOT_FOUND` mapping
|
||||
applies without modification.
|
||||
- Rejects `clear()` with a `DriveUriError(WRITE_UNSUPPORTED)` whose message
|
||||
names `Drive.delete(path)` as the supported alternative.
|
||||
- Forwards `AbortSignal` to `Drive.createReadStream` so consumers can cancel
|
||||
in-flight streams.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { DriveStorageLikeAdapter } from "@suaveplan/drive-uri/adapter";
|
||||
|
||||
const adapter = new DriveStorageLikeAdapter(drive);
|
||||
const bytes = await adapter.get("folder/file.bin");
|
||||
if (bytes === undefined) console.log("not found");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Composing the shim with a custom `UriPlugin` (this is essentially what
|
||||
`driveUriPlugin` does internally):
|
||||
|
||||
```ts
|
||||
import { DriveStorageLikeAdapter } from "@suaveplan/drive-uri/adapter";
|
||||
import type { UriPlugin } from "@suaveplan/storage-uri";
|
||||
|
||||
function myPlugin(drive: Drive): UriPlugin {
|
||||
const adapter = new DriveStorageLikeAdapter(drive);
|
||||
return {
|
||||
scheme: "my",
|
||||
adapter,
|
||||
parse(uri: string) {
|
||||
const path = uri.slice("my://".length);
|
||||
return { key: path };
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class DriveStorageLikeAdapter implements StorageLike` — accepts a `Drive`
|
||||
in its constructor. All `StorageLike` methods delegate to the bound drive.
|
||||
- `DriveStorageLikeAdapter.createReadStream(key, signal?)` — forwards
|
||||
directly to `Drive.createReadStream(key, signal)`.
|
||||
- `function isDriveNotFound(err: unknown): boolean` — returns `true` when
|
||||
`err` (or anything in its `cause` chain, up to 8 levels deep with cycle
|
||||
detection) is a `SuaveplanError` whose `code` ends in `.NOT_FOUND`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The shim does NOT own any chunk codec, manifest fetching, peer discovery,
|
||||
or template validation — that all happens inside `Drive` via its installed
|
||||
middleware (`chunksPlugin`, `drive-mesh`, etc.).
|
||||
- `list()` accepts either of two shapes Drive may return (`{ keys }` vs.
|
||||
`{ entries: [{ path }] }`) and normalises to the `StorageLike` shape,
|
||||
preserving `continuationToken` when present.
|
||||
- `clear()` is intentionally a hard error rather than a soft no-op — wiping a
|
||||
Drive through the URI surface is a footgun, and a silent success would lose
|
||||
data on misuse. The supported path is `resolver.remove(uri)` per file.
|
||||
- `isDriveNotFound` includes cycle detection (depth-limit + self-reference
|
||||
check) so a malicious or accidental self-referential `cause` chain cannot
|
||||
cause an infinite loop.
|
||||
106
docs/networking/drive/drive-uri/src/conformance/conformance.md
Normal file
106
docs/networking/drive/drive-uri/src/conformance/conformance.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# `@suaveplan/drive-uri/conformance`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Re-exports the canonical URI-plugin conformance harness from
|
||||
`@suaveplan/storage-uri` (`runUriPluginConformance`, `UriPluginFixtures`,
|
||||
`UriPluginConformanceFailure`). `drive-uri` does NOT author its own harness
|
||||
— keeping the assertions in one place means every URI-plugin package in the
|
||||
project (storage-file, storage-http, storage-cdn-*, drive-uri, …)
|
||||
runs against an identical set of contract checks.
|
||||
|
||||
Each plugin's fixture set lives in its co-located test file; the harness
|
||||
runs three classes of assertion per fixture: every `validUris` member is
|
||||
accepted by `parse`, every `invalidUris` member throws
|
||||
`STORAGE_URI.INVALID_URI`, and every `buildRoundTrip` entry round-trips
|
||||
through `plugin.build`.
|
||||
|
||||
## Features
|
||||
|
||||
- Zero-runtime re-export — the conformance harness is a value, not a copy.
|
||||
- Type-safe `UriPluginFixtures` shape.
|
||||
- Strong-typed failure surface (`UriPluginConformanceFailure`).
|
||||
- Compatible with both `drive`, `drive+mesh`, and `drive+template` plugins.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
runUriPluginConformance,
|
||||
type UriPluginFixtures,
|
||||
} from "@suaveplan/drive-uri/conformance";
|
||||
import { driveUriPlugin } from "@suaveplan/drive-uri";
|
||||
|
||||
const fixtures: UriPluginFixtures = {
|
||||
validUris: ["drive://primary/a", "drive://primary/nested/file.bin"],
|
||||
invalidUris: ["", "drive:", "drive://primary/has space"],
|
||||
buildRoundTrip: [
|
||||
{
|
||||
key: "a",
|
||||
meta: { driveId: "primary" },
|
||||
uri: "drive://primary/a",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
runUriPluginConformance(
|
||||
driveUriPlugin({ drive, driveId: "primary" }),
|
||||
fixtures,
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Running the same fixture template across multiple driveIds parametrically:
|
||||
|
||||
```ts
|
||||
import { runUriPluginConformance, type UriPluginFixtures } from "@suaveplan/drive-uri";
|
||||
|
||||
function fixturesFor(driveId: string): UriPluginFixtures {
|
||||
return {
|
||||
validUris: [`drive://${driveId}/a`, `drive://${driveId}/folder/b`],
|
||||
invalidUris: ["", "drive:"],
|
||||
buildRoundTrip: [
|
||||
{
|
||||
key: "a",
|
||||
meta: { driveId },
|
||||
uri: `drive://${driveId}/a`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
for (const id of ["primary", "secondary", "backup"]) {
|
||||
runUriPluginConformance(
|
||||
driveUriPlugin({ drive: drives[id], driveId: id }),
|
||||
fixturesFor(id),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `runUriPluginConformance(plugin, fixtures)` — throws
|
||||
`UriPluginConformanceFailure` on the first mismatch. Re-export from
|
||||
`@suaveplan/storage-uri/conformance`.
|
||||
- `interface UriPluginFixtures` — `validUris`, `invalidUris`, optional
|
||||
`buildRoundTrip`.
|
||||
- `class UriPluginConformanceFailure extends Error` — the harness's failure
|
||||
class.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The drive-uri tests assemble per-plugin fixture sets locally (one each for
|
||||
`drive://`, `drive+mesh://`, `drive+template://`) and feed them into the
|
||||
harness inside `src/conformance/conformance.test.ts`.
|
||||
- The `invalidUris` set must produce `STORAGE_URI.INVALID_URI` from
|
||||
`parseUri` — the drive-uri plugins authenticate the scheme themselves but
|
||||
defer URI-hygiene checks (whitespace, NUL, length, scheme regex) to the
|
||||
upstream `parseUri` helper.
|
||||
- Round-trip fixtures only need to be supplied when the plugin implements
|
||||
`build`; the harness skips that section when `buildRoundTrip` is empty
|
||||
or omitted.
|
||||
@@ -0,0 +1,98 @@
|
||||
# `@suaveplan/drive-uri/error-codes`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Declares the seven `DRIVE_URI.*` error codes raised by `@suaveplan/drive-uri`
|
||||
plugins and the `DriveUriError` discriminant class used to surface them. The
|
||||
codes form the public contract by which consumers programmatically distinguish
|
||||
URI-parsing failures (`INVALID_URI`), authority mismatches
|
||||
(`DRIVE_ID_MISMATCH`), template fetch/validation failures
|
||||
(`TEMPLATE_NOT_AVAILABLE`, `TEMPLATE_INVALID`), unsupported writes
|
||||
(`WRITE_UNSUPPORTED`), missing required wiring (`CAPABILITY_MISSING`), and
|
||||
abort short-circuits (`ABORTED`).
|
||||
|
||||
Every code is produced by `@suaveplan/error`'s `defineCodes` so the literal
|
||||
strings (`"DRIVE_URI.INVALID_URI"` etc.) are generated once and consumed via
|
||||
the typed `DRIVE_URI_CODES` constant — never hand-typed. This keeps the
|
||||
namespace honest under refactor and guarantees that `instanceof DriveUriError`
|
||||
narrows the `code` discriminant to a small literal union.
|
||||
|
||||
## Features
|
||||
|
||||
- Seven namespaced codes registered via a single `defineCodes` call.
|
||||
- `DriveUriError` carries optional discriminants (`uri`, `scheme`, `driveId`,
|
||||
`swarmId`, `templateId`, `path`, `expected`, `actual`, `reason`) so
|
||||
operators can correlate failures without parsing message strings.
|
||||
- Original `cause` is preserved across re-wraps via the `SuaveplanError`
|
||||
base class so `err.cause.code` still reads e.g. `DTP.TEMPLATE_EXPIRED`
|
||||
after a `TEMPLATE_INVALID` wrap.
|
||||
- The error class name is `"DriveUriError"` for log aggregation and
|
||||
TypeScript narrowing.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { DRIVE_URI_CODES, DriveUriError } from "@suaveplan/drive-uri/error-codes";
|
||||
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.DRIVE_ID_MISMATCH,
|
||||
"URI driveId 'other' does not match plugin driveId 'primary'",
|
||||
{ uri: "drive://other/x", expected: "primary", actual: "other" },
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Walking the `cause` chain to surface the underlying drive failure code while
|
||||
keeping the public `DRIVE_URI.*` shape:
|
||||
|
||||
```ts
|
||||
import { DRIVE_URI_CODES, DriveUriError } from "@suaveplan/drive-uri";
|
||||
|
||||
try {
|
||||
await drive.hydrateFrom(template);
|
||||
} catch (cause) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.TEMPLATE_INVALID,
|
||||
"hydrateFrom rejected for templateId 'templA'",
|
||||
{ templateId: "templA", cause },
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
A consumer receives a `DriveUriError` whose `code === "DRIVE_URI.TEMPLATE_INVALID"`
|
||||
and whose `cause.code === "DTP.TEMPLATE_EXPIRED"` (or whichever DTP code the
|
||||
drive-template validator surfaced). Both are programmatically actionable.
|
||||
|
||||
## API Reference
|
||||
|
||||
- `DRIVE_URI_CODES` — frozen registry with seven keys: `INVALID_URI`,
|
||||
`DRIVE_ID_MISMATCH`, `TEMPLATE_NOT_AVAILABLE`, `TEMPLATE_INVALID`,
|
||||
`WRITE_UNSUPPORTED`, `CAPABILITY_MISSING`, `ABORTED`. Each value is the
|
||||
namespaced string `DRIVE_URI.<REASON>`.
|
||||
- `DriveUriErrorCode` — string-literal union over the registry values.
|
||||
- `DriveUriErrorOptions` — extends `SuaveplanErrorOptions` with optional
|
||||
`uri`, `scheme`, `driveId`, `swarmId`, `templateId`, `path`, `expected`,
|
||||
`actual`, `reason` discriminants.
|
||||
- `DriveUriError` — extends `SuaveplanError<DriveUriErrorCode>`. Constructor:
|
||||
`(code, message, options?) => DriveUriError`. Exposes the discriminants as
|
||||
readonly fields.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The registry is intentionally narrow. Codes like `STORAGE_URI.NOT_FOUND` are
|
||||
emitted by the resolver, not by drive-uri — duplicating them here would
|
||||
drift on rename. The codes that ARE in this registry are the ones drive-uri
|
||||
raises before reaching the resolver, plus the ones it re-wraps to keep the
|
||||
failure mode visible to dashboards.
|
||||
- `DriveUriError` does NOT include `getMetadata`-style state — it is a plain
|
||||
exception class. OpenTelemetry instrumentation lives in
|
||||
`@suaveplan/drive-uri/otel`.
|
||||
- The side-effect import at `src/index.ts` ensures the registry is registered
|
||||
when consumers reach the package via the root entry; subpath consumers
|
||||
reaching `./error-codes` directly register the codes on import via
|
||||
`defineCodes`'s own side effect.
|
||||
92
docs/networking/drive/drive-uri/src/otel/otel.md
Normal file
92
docs/networking/drive/drive-uri/src/otel/otel.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# `@suaveplan/drive-uri/otel`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
OpenTelemetry instrumentation surface for `@suaveplan/drive-uri`. Exposes a
|
||||
meter named `@suaveplan/drive-uri` carrying the operation-level counters and
|
||||
duration histogram described in the package spec, plus a tracer of the same
|
||||
name for span emission. The `redactUri` helper strips query strings and
|
||||
fragments from URI attributes so peer hints, seeds, or other secret material
|
||||
never reach span backends.
|
||||
|
||||
Counters and histograms are lazily created on first access via
|
||||
`getInstruments()` so the package has zero side effects at import time, and
|
||||
`setDriveUriMeterProvider` lets tests inject a stub provider for assertions.
|
||||
|
||||
## Features
|
||||
|
||||
- Lazy instrument bag keyed by the active `MeterProvider`.
|
||||
- Counters: `drive_uri.reads_total`, `drive_uri.writes_total`,
|
||||
`drive_uri.deletes_total`, `drive_uri.errors_total`,
|
||||
`drive_uri.template_hydrations_total`.
|
||||
- Histogram: `drive_uri.op_duration_ms{op, scheme, outcome}`.
|
||||
- Per-op routing helper `recordOp` that increments the correct success or
|
||||
error counter and the duration histogram in one call. Writes increment
|
||||
`writes_total` even on rejection so operator dashboards see misuse rate.
|
||||
- `redactUri(uri)` strips everything from the first `?` or `#` onwards.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { recordOp, recordTemplateHydration } from "@suaveplan/drive-uri/otel";
|
||||
|
||||
recordOp({ op: "read", scheme: "drive", outcome: "success", durationMs: 12 });
|
||||
recordTemplateHydration("success");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Test harness swapping in a stub `MeterProvider`:
|
||||
|
||||
```ts
|
||||
import {
|
||||
resetInstruments,
|
||||
setDriveUriMeterProvider,
|
||||
} from "@suaveplan/drive-uri/otel";
|
||||
|
||||
class StubProvider implements MeterProvider {
|
||||
public meter = new StubMeter();
|
||||
public getMeter() {
|
||||
return this.meter;
|
||||
}
|
||||
}
|
||||
|
||||
const provider = new StubProvider();
|
||||
setDriveUriMeterProvider(provider);
|
||||
resetInstruments();
|
||||
// ... run code under test, then inspect provider.meter ...
|
||||
setDriveUriMeterProvider(undefined);
|
||||
resetInstruments();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `setDriveUriMeterProvider(provider | undefined)` — override or revert.
|
||||
- `getInstruments(force = false)` — returns the cached bag or rebuilds.
|
||||
- `resetInstruments()` — clears the cache (used between provider swaps).
|
||||
- `recordOp({ op, scheme, outcome, durationMs, code? })` — one-shot
|
||||
per-operation observation. `op ∈ {open|read|write|stat|exists|remove|list}`.
|
||||
- `recordTemplateHydration(outcome)` — increments
|
||||
`drive_uri.template_hydrations_total{outcome}` by one.
|
||||
- `redactUri(uri): string` — strips `?...` and `#...` suffixes.
|
||||
- Types: `DriveUriOp`, `DriveUriOutcome`, `DriveUriInstruments`,
|
||||
`RecordOpInput`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `writes_total` carries the `outcome` attribute (`"success"` or `"error"`),
|
||||
letting dashboards split successful (currently impossible) writes from
|
||||
misuse rejections.
|
||||
- `errors_total` is keyed by `scheme` and `code` only — operators slice it
|
||||
by code to attribute failures to root causes (`DRIVE_URI.TEMPLATE_INVALID`
|
||||
vs. `DRIVE_URI.WRITE_UNSUPPORTED` etc.).
|
||||
- The drive-uri tracer is independent of the resolver's `@suaveplan/storage-uri`
|
||||
tracer — both spans fire on a single dispatch, giving operators a per-tier
|
||||
view of the operation.
|
||||
- `redactUri` is content-agnostic; it does NOT validate that the input is a
|
||||
syntactically correct URI. The plugin layer is responsible for rejecting
|
||||
malformed URIs before they reach the span attribute.
|
||||
@@ -0,0 +1,91 @@
|
||||
# `@suaveplan/drive-uri/plugins/drive-mesh`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`driveMeshUriPlugin(options)` is a factory returning a `UriPlugin` registered
|
||||
against the `drive+mesh://` scheme. It parses `drive+mesh://<swarmId>/<path>`
|
||||
URIs, captures `swarmId` for instrumentation, and forwards `path` to the
|
||||
consumer-supplied mesh-configured `Drive`. The plugin does NOT use `swarmId`
|
||||
to choose a `Drive` or transport — peer routing remains entirely the
|
||||
responsibility of the `Drive`'s installed transports (`drive-transport-https`,
|
||||
`drive-webrtc`) and its `PeerRegistry`.
|
||||
|
||||
The captured `swarmId` surfaces on every emitted span via the
|
||||
`drive.uri.swarm_id` attribute. When a span is active at the time of
|
||||
`parse(uri)`, the attribute is written there (alongside `drive.uri.scheme`
|
||||
and the redacted `drive.uri`); otherwise the parse step is silent.
|
||||
|
||||
## Features
|
||||
|
||||
- `parse(uri)` enforces `scheme === "drive+mesh"`, a non-empty authority, and
|
||||
a non-empty path. All other URI hygiene (whitespace, NUL, scheme regex,
|
||||
4096-char limit) is delegated to `parseUri` from `@suaveplan/storage-uri`.
|
||||
- `build(key, meta?)` round-trips the URI back to its source shape, defaulting
|
||||
the swarmId to `options.defaultSwarmId` (or `"default"`) when no meta is
|
||||
supplied.
|
||||
- Adapter inherits `InstrumentedDriveAdapter` so reads, writes, and errors
|
||||
are recorded under the `drive+mesh` scheme attribute.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { chunksPlugin } from "@suaveplan/drive-plugin-chunks";
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import { driveMeshUriPlugin } from "@suaveplan/drive-uri/plugins/drive-mesh";
|
||||
|
||||
const meshDrive = new Drive(localStorage);
|
||||
await meshDrive.initialize();
|
||||
await meshDrive.usePlugin(chunksPlugin({ /* ... */ }));
|
||||
|
||||
const resolver = createStorageUriResolver([
|
||||
driveMeshUriPlugin({ drive: meshDrive }),
|
||||
]);
|
||||
|
||||
const bytes = await resolver.read("drive+mesh://swarmA/raw/clip.mov");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Mixing mesh and template plugins on the same mesh-configured Drive so a
|
||||
single resolver dispatches both shapes through one drive instance:
|
||||
|
||||
```ts
|
||||
import {
|
||||
driveMeshUriPlugin,
|
||||
driveTemplateUriPlugin,
|
||||
} from "@suaveplan/drive-uri";
|
||||
|
||||
const resolver = createStorageUriResolver([
|
||||
driveMeshUriPlugin({ drive: meshDrive, defaultSwarmId: "primary" }),
|
||||
driveTemplateUriPlugin({ drive: meshDrive, templateProvider }),
|
||||
]);
|
||||
|
||||
await resolver.read("drive+mesh://primary/folder/a.bin");
|
||||
await resolver.read("drive+template://templA/folder/a.bin");
|
||||
```
|
||||
|
||||
The two plugins share the same `Drive`, so a hydration triggered by the
|
||||
template plugin populates the file-location index that the mesh plugin then
|
||||
uses to fetch chunks. This is the canonical composition.
|
||||
|
||||
## API Reference
|
||||
|
||||
- `driveMeshUriPlugin(options: DriveMeshUriPluginOptions): UriPlugin` —
|
||||
`options.drive` is required; `options.defaultSwarmId` is optional and
|
||||
defaults to `"default"` for `build` output.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The plugin's `parse(uri)` attaches the swarmId to the active span via
|
||||
`trace.getActiveSpan()`. When no span is active (e.g. plugin used
|
||||
outside the resolver, in unit tests parsing URIs directly), the
|
||||
attribute write is silently skipped — never throws.
|
||||
- The `drive.uri` attribute is passed through `redactUri` so query strings
|
||||
carrying peer hints or seeds are stripped before reaching span backends.
|
||||
- Like `driveUriPlugin`, writes are rejected with `DRIVE_URI.WRITE_UNSUPPORTED`
|
||||
per the per-release scope.
|
||||
@@ -0,0 +1,112 @@
|
||||
# `@suaveplan/drive-uri/plugins/drive-template`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`driveTemplateUriPlugin(options)` is a factory returning a `UriPlugin`
|
||||
registered against the `drive+template://` scheme. It parses
|
||||
`drive+template://<templateId>/<path>` URIs and, before delegating any read
|
||||
to the wrapped `Drive`, fetches the matching `DriveTemplate` via
|
||||
`options.templateProvider.fetchTemplate(templateId)` and feeds it to
|
||||
`drive.hydrateFrom(template)`. The first read against a given `templateId`
|
||||
performs both steps; concurrent reads await the same in-flight hydration;
|
||||
subsequent reads after a successful hydration skip both calls.
|
||||
|
||||
The hydration cache is a per-plugin `Map<string, Promise<void>>` keyed by
|
||||
`templateId`. After a failed hydration the entry is removed so subsequent
|
||||
reads can retry — important for transient `fetchTemplate` failures.
|
||||
|
||||
## Features
|
||||
|
||||
- Construction-time validation of `options.templateProvider` — absence or a
|
||||
non-function `fetchTemplate` throws `DRIVE_URI.CAPABILITY_MISSING`
|
||||
synchronously, never deferred to first use.
|
||||
- Per-templateId hydration memoisation with concurrent-read coalescing.
|
||||
- Distinct error mappings — `fetchTemplate` rejection becomes
|
||||
`DRIVE_URI.TEMPLATE_NOT_AVAILABLE`; `Drive.hydrateFrom` rejection becomes
|
||||
`DRIVE_URI.TEMPLATE_INVALID`. The two failures are NEVER conflated.
|
||||
- Original cause is preserved on both wrapper errors so operators see e.g.
|
||||
`cause.code === "DTP.TEMPLATE_EXPIRED"` underneath a `TEMPLATE_INVALID`.
|
||||
- `drive_uri.template_hydrations_total{outcome}` increments exactly once
|
||||
per first-use template (success or error). Cache-hit reads do NOT
|
||||
increment the counter.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import { driveTemplateUriPlugin } from "@suaveplan/drive-uri/plugins/drive-template";
|
||||
|
||||
const drive = new Drive(localStorage);
|
||||
await drive.initialize();
|
||||
|
||||
const templateProvider = {
|
||||
async fetchTemplate(id: string) {
|
||||
return downloadTemplate(id);
|
||||
},
|
||||
};
|
||||
|
||||
const resolver = createStorageUriResolver([
|
||||
driveTemplateUriPlugin({ drive, templateProvider }),
|
||||
]);
|
||||
|
||||
const bytes = await resolver.read("drive+template://templA/manifest/clip.jpg");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Sharing the template plugin's hydration cache with a `drive+mesh://` plugin
|
||||
by passing the same `Drive` to both — the file-location index populated by
|
||||
hydration powers the mesh plugin's chunk fetches:
|
||||
|
||||
```ts
|
||||
import {
|
||||
driveMeshUriPlugin,
|
||||
driveTemplateUriPlugin,
|
||||
} from "@suaveplan/drive-uri";
|
||||
|
||||
const sharedDrive = await buildMeshConfiguredDrive();
|
||||
|
||||
const resolver = createStorageUriResolver([
|
||||
driveMeshUriPlugin({ drive: sharedDrive }),
|
||||
driveTemplateUriPlugin({ drive: sharedDrive, templateProvider }),
|
||||
]);
|
||||
|
||||
// First read hydrates the template, populates the index, then reads chunks
|
||||
// through the chunks plugin via the mesh transport.
|
||||
await resolver.read("drive+template://templA/clip.mov");
|
||||
|
||||
// Subsequent reads for the same template skip hydration but still go
|
||||
// through the chunks plugin (because the file-location index is now seeded).
|
||||
await resolver.read("drive+template://templA/cover.jpg");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `driveTemplateUriPlugin(options: DriveTemplateUriPluginOptions): UriPlugin` —
|
||||
both `options.drive` and `options.templateProvider` are required at
|
||||
construction. `templateProvider.fetchTemplate` MUST be a function.
|
||||
- The returned plugin exposes `scheme === "drive+template"`, an instrumented
|
||||
adapter, `parse(uri) → { key, meta: { templateId } }`, and
|
||||
`build(key, meta?) → uri`.
|
||||
- `interface TemplateProvider` — `{ fetchTemplate(templateId, signal?) => Promise<DriveTemplate> }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The hydration cache stores `Promise<void>` rather than the resolved
|
||||
template object — concurrent reads await the same promise, and once it
|
||||
settles the value is irrelevant (the template is already inside the
|
||||
`Drive`'s index via `hydrateFrom`).
|
||||
- Failed hydrations remove themselves from the cache so retries are
|
||||
possible. A re-hydration after a previous failure is idempotent at the
|
||||
`Drive` layer via `TemplateNonceLru`.
|
||||
- `parse(uri)` captures the templateId in a closure-scoped variable so the
|
||||
adapter's `beforeOp` hook (which fires synchronously inside the
|
||||
resolver's dispatch chain) sees the right value before any `get`/`exists`/
|
||||
`delete` call.
|
||||
- Writes are rejected at the adapter tier with `DRIVE_URI.WRITE_UNSUPPORTED`,
|
||||
per the per-release scope.
|
||||
103
docs/networking/drive/drive-uri/src/plugins/drive/drive.md
Normal file
103
docs/networking/drive/drive-uri/src/plugins/drive/drive.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# `@suaveplan/drive-uri/plugins/drive`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`driveUriPlugin(options)` is a factory returning a `UriPlugin` registered
|
||||
against the `drive://` scheme. It wraps a consumer-supplied `Drive` instance
|
||||
in an OTel-instrumented `DriveStorageLikeAdapter` so a single
|
||||
`StorageUriResolver` can dispatch `drive://<driveId>/<path>` URIs through the
|
||||
same code path as `file:///` or `s3://` URIs. The plugin enforces a 1:1 match
|
||||
between the URI's authority component and the `driveId` declared in options;
|
||||
a mismatch surfaces as `DRIVE_URI.DRIVE_ID_MISMATCH` before any `Drive` call.
|
||||
|
||||
The plugin is read-only in this release: every `adapter.set` call rejects
|
||||
with `DRIVE_URI.WRITE_UNSUPPORTED` (which the resolver wraps as
|
||||
`STORAGE_URI.WRITE_UNSUPPORTED` with the drive-uri error preserved on
|
||||
`cause`). Writes still increment `drive_uri.writes_total{outcome:"error"}`
|
||||
so operator dashboards see misuse rates.
|
||||
|
||||
## Features
|
||||
|
||||
- Parses `drive://<driveId>/<path>` URIs and rejects mismatched driveIds.
|
||||
- Returns `{ key, meta: { driveId } }` so callers can re-build the URI
|
||||
through `plugin.build(key, meta)`.
|
||||
- Forwards `Drive.createReadStream` directly when `resolver.open()` is used
|
||||
so the resolver's native streaming fast path applies — no buffered
|
||||
fallback, no `storage_uri.stream_fallback_total` increment.
|
||||
- Pre-aborted signals short-circuit at the adapter shim with
|
||||
`DRIVE_URI.ABORTED` (no `Drive` call is made).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import { driveUriPlugin } from "@suaveplan/drive-uri/plugins/drive";
|
||||
|
||||
const drive = new Drive(localStorage);
|
||||
await drive.initialize();
|
||||
|
||||
const resolver = createStorageUriResolver([
|
||||
driveUriPlugin({ drive, driveId: "primary" }),
|
||||
]);
|
||||
|
||||
const bytes = await resolver.read("drive://primary/folder/file.bin");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Composing with other plugins (file + drive in one resolver) and threading an
|
||||
`AbortSignal` into the streaming open path:
|
||||
|
||||
```ts
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import { driveUriPlugin } from "@suaveplan/drive-uri";
|
||||
|
||||
const resolver = createStorageUriResolver([
|
||||
fileUriPlugin({ rootDir: "/var/cache" }),
|
||||
driveUriPlugin({ drive, driveId: "primary" }),
|
||||
]);
|
||||
|
||||
const controller = new AbortController();
|
||||
const stream = await resolver.open(
|
||||
"drive://primary/large/clip.mov",
|
||||
controller.signal,
|
||||
);
|
||||
const reader = stream.getReader();
|
||||
try {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
process(value);
|
||||
}
|
||||
} finally {
|
||||
controller.abort(); // propagates to Drive.createReadStream
|
||||
reader.releaseLock();
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `driveUriPlugin(options: DriveUriPluginOptions): UriPlugin` — `options.drive`
|
||||
is the consumer-supplied `Drive`, `options.driveId` is the literal authority
|
||||
the plugin will require. Both are validated synchronously; a missing or
|
||||
empty value throws `DRIVE_URI.CAPABILITY_MISSING`.
|
||||
- The returned plugin exposes `scheme === "drive"`, `adapter`,
|
||||
`parse(uri) → { key, meta }`, and `build(key, meta?) → uri`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The plugin's `parse` is purely synchronous. Async work (the actual `Drive`
|
||||
call) happens inside the adapter's `get`/`createReadStream`/etc. methods
|
||||
invoked by `StorageUriResolver`.
|
||||
- `build(key, meta?)` defaults the driveId to the plugin's configured
|
||||
`options.driveId` when no meta is supplied — useful for re-serialising
|
||||
resolver-list results.
|
||||
- Writes are deferred to a follow-up change (see proposal §"Out of scope").
|
||||
The plugin still records the rejection so dashboards capture misuse.
|
||||
- The `DriveStorageLikeAdapter` is created once per plugin instance and
|
||||
shared across every URI dispatched to this plugin.
|
||||
@@ -0,0 +1,84 @@
|
||||
# `@suaveplan/drive-uri/plugins/drive/streams`
|
||||
|
||||
## Purpose
|
||||
|
||||
`InstrumentedDriveAdapter` is the OTel-instrumented `StorageLike` shared by
|
||||
all three URI plugins (`drive://`, `drive+mesh://`, `drive+template://`). It
|
||||
extends `DriveStorageLikeAdapter` to:
|
||||
|
||||
- record `drive_uri.op_duration_ms{op, scheme, outcome}` and the per-op
|
||||
counters (`reads_total`, `writes_total`, `deletes_total`, `errors_total`)
|
||||
via `recordOp`;
|
||||
- reject every `set` call with `DRIVE_URI.WRITE_UNSUPPORTED` (and increment
|
||||
`writes_total{outcome:"error"}`);
|
||||
- short-circuit `createReadStream` when the supplied `AbortSignal` is already
|
||||
aborted, throwing `DRIVE_URI.ABORTED` without invoking the underlying drive;
|
||||
- thread an optional `beforeOp` async hook used by the `drive+template`
|
||||
plugin to interpose template hydration before every operation.
|
||||
|
||||
The class is exported from the package root for advanced consumers building
|
||||
custom plugins; the three first-party plugins use it internally.
|
||||
|
||||
## Features
|
||||
|
||||
- Inherits `StorageLike` conformance from `DriveStorageLikeAdapter` (including
|
||||
`clear() → DRIVE_URI.WRITE_UNSUPPORTED` and `list()`'s normalised shape).
|
||||
- Per-op OTel instrumentation — `read`/`open`/`exists`/`list` route to
|
||||
`reads_total` (on success) and `errors_total` (on failure);
|
||||
`write` always routes to `writes_total`; `delete` routes to `deletes_total`.
|
||||
- `not_found` outcomes are recorded as `errors_total{code:"UNKNOWN"}` for
|
||||
consistency with the storage-uri resolver — operators see a clear "missing
|
||||
resource" signal without the success counters being skewed.
|
||||
- The `beforeOp` hook is awaited before every operation, so the
|
||||
`drive+template` plugin can ensure hydration has completed.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { InstrumentedDriveAdapter } from "@suaveplan/drive-uri";
|
||||
|
||||
const adapter = new InstrumentedDriveAdapter(drive, { scheme: "drive" });
|
||||
await adapter.get("folder/file.bin");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
A custom plugin combining the instrumented adapter with a per-op auth check:
|
||||
|
||||
```ts
|
||||
import { InstrumentedDriveAdapter } from "@suaveplan/drive-uri";
|
||||
|
||||
const adapter = new InstrumentedDriveAdapter(drive, {
|
||||
scheme: "drive+custom",
|
||||
beforeOp: async (op) => {
|
||||
if (op === "delete" && !ctx.hasDeletePermission()) {
|
||||
throw new Error("forbidden");
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The hook fires in lockstep with the resolver's dispatch chain. It's run
|
||||
before the wrapped `Drive` call, so failures inside the hook are recorded
|
||||
under `errors_total` for the relevant op.
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface InstrumentedAdapterOptions` — `scheme: string`,
|
||||
`beforeOp?: (op) => Promise<void>`.
|
||||
- `class InstrumentedDriveAdapter extends DriveStorageLikeAdapter` —
|
||||
overrides `get`, `set`, `delete`, `exists`, and `createReadStream`. Every
|
||||
override records duration and counter samples via `recordOp`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `set` always throws — the per-release scope deliberately excludes writes.
|
||||
- `not_found` outcome on `get` happens when the base class translates a
|
||||
drive not-found error into `undefined`; both the histogram and
|
||||
`errors_total{code:"UNKNOWN"}` (per `recordOp` semantics) increment.
|
||||
- `createReadStream` is the only operation that takes an `AbortSignal` at
|
||||
the storage-uri resolver layer; the pre-aborted check throws `ABORTED`
|
||||
before any Drive call.
|
||||
- The `beforeOp` hook receives a string discriminant (`"get"|"set"|"delete"|
|
||||
"exists"|"list"`) rather than the op-level `DriveUriOp` so it doesn't
|
||||
conflate the resolver's `open` (which maps to `get`) with `read`.
|
||||
@@ -0,0 +1,95 @@
|
||||
# `@suaveplan/drive-uri/types`
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared option-bag interfaces consumed by the three URI plugin factories
|
||||
exported from `@suaveplan/drive-uri`. Each interface follows AGENTS §11's
|
||||
rule that nested object shapes must be named — every option referenced
|
||||
across plugin boundaries (e.g. `TemplateProvider`) is a standalone interface,
|
||||
never an inline object literal.
|
||||
|
||||
The types are intentionally minimal — drive-uri does not own the contract
|
||||
of the `Drive` it wraps, nor the schema of the `DriveTemplate` it passes
|
||||
through. Those types come from `@suaveplan/drive` and `@suaveplan/drive-template`
|
||||
respectively and are referenced by name here.
|
||||
|
||||
## Features
|
||||
|
||||
- `TemplateProvider` — single-method interface that the `drive+template`
|
||||
plugin consumes. Implementations may fetch templates over HTTP, gossip,
|
||||
the upstream mesh, or an in-memory map (used in tests).
|
||||
- `DriveUriPluginOptions` — minimum surface for the `drive` plugin:
|
||||
`drive` + `driveId`.
|
||||
- `DriveMeshUriPluginOptions` — `drive` + optional `defaultSwarmId`.
|
||||
- `DriveTemplateUriPluginOptions` — `drive` + `templateProvider`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import type {
|
||||
DriveUriPluginOptions,
|
||||
TemplateProvider,
|
||||
} from "@suaveplan/drive-uri";
|
||||
|
||||
const options: DriveUriPluginOptions = { drive, driveId: "primary" };
|
||||
|
||||
const provider: TemplateProvider = {
|
||||
async fetchTemplate(templateId, signal) {
|
||||
return downloadTemplate(templateId, signal);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
A `TemplateProvider` backed by an HTTP fetch with caching, retry, and the
|
||||
forwarded `AbortSignal`:
|
||||
|
||||
```ts
|
||||
import type { TemplateProvider } from "@suaveplan/drive-uri";
|
||||
import type { DriveTemplate } from "@suaveplan/drive-template";
|
||||
|
||||
const cache = new Map<string, Promise<DriveTemplate>>();
|
||||
|
||||
const provider: TemplateProvider = {
|
||||
async fetchTemplate(templateId, signal) {
|
||||
const cached = cache.get(templateId);
|
||||
if (cached !== undefined) return cached;
|
||||
const pending = (async () => {
|
||||
const res = await fetch(`/api/templates/${templateId}`, { signal });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return (await res.json()) as DriveTemplate;
|
||||
})();
|
||||
cache.set(templateId, pending);
|
||||
try {
|
||||
return await pending;
|
||||
} catch (err) {
|
||||
cache.delete(templateId);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface TemplateProvider` — `fetchTemplate(templateId: string, signal?: AbortSignal): Promise<DriveTemplate>`.
|
||||
- `interface DriveUriPluginOptions` — `readonly drive: Drive`, `readonly driveId: string`.
|
||||
- `interface DriveMeshUriPluginOptions` — `readonly drive: Drive`,
|
||||
`readonly defaultSwarmId?: string`.
|
||||
- `interface DriveTemplateUriPluginOptions` — `readonly drive: Drive`,
|
||||
`readonly templateProvider: TemplateProvider`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Every interface is `readonly` for ergonomic safety — option bags are
|
||||
one-shot construction inputs, not mutable state.
|
||||
- `defaultSwarmId` on the mesh options is only used by `plugin.build(...)`
|
||||
when no `meta.swarmId` is supplied — it never affects dispatch.
|
||||
- `TemplateProvider.fetchTemplate` returns `DriveTemplate` (the canonical
|
||||
type from `@suaveplan/drive-template`), not a serialized form. The
|
||||
provider is responsible for any decoding/parsing; `drive-uri` passes the
|
||||
return value straight to `Drive.hydrateFrom`.
|
||||
- A future addition could be a `templateCache` option exposing the
|
||||
hydration cache to the consumer, but per AGENTS §11 that requires ≥2
|
||||
consumers — out of scope for the initial release.
|
||||
Reference in New Issue
Block a user