Initial Commit
This commit is contained in:
159
packages/networking/drive/drive-uri/README.md
Normal file
159
packages/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`
|
||||
|
||||
147
packages/networking/drive/drive-uri/package.json
Normal file
147
packages/networking/drive/drive-uri/package.json
Normal file
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"name": "@suaveplan/drive-uri",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"description": "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.",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"./adapter": {
|
||||
"import": {
|
||||
"types": "./dist/adapter/index.d.ts",
|
||||
"default": "./dist/adapter/drive-storage-like.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/adapter/index.d.ts",
|
||||
"default": "./dist/adapter/drive-storage-like.js"
|
||||
}
|
||||
},
|
||||
"./plugins/drive": {
|
||||
"import": {
|
||||
"types": "./dist/plugins/drive/index.d.ts",
|
||||
"default": "./dist/plugins/drive/drive.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/plugins/drive/index.d.ts",
|
||||
"default": "./dist/plugins/drive/drive.js"
|
||||
}
|
||||
},
|
||||
"./plugins/drive-mesh": {
|
||||
"import": {
|
||||
"types": "./dist/plugins/drive-mesh/index.d.ts",
|
||||
"default": "./dist/plugins/drive-mesh/drive-mesh.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/plugins/drive-mesh/index.d.ts",
|
||||
"default": "./dist/plugins/drive-mesh/drive-mesh.js"
|
||||
}
|
||||
},
|
||||
"./plugins/drive-template": {
|
||||
"import": {
|
||||
"types": "./dist/plugins/drive-template/index.d.ts",
|
||||
"default": "./dist/plugins/drive-template/drive-template.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/plugins/drive-template/index.d.ts",
|
||||
"default": "./dist/plugins/drive-template/drive-template.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/drive": "workspace:^",
|
||||
"@suaveplan/drive-template": "workspace:^",
|
||||
"@suaveplan/error": "workspace:^",
|
||||
"@suaveplan/storage-uri": "workspace:^",
|
||||
"@suaveplan/types": "workspace:^",
|
||||
"tslib": "catalog:build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@suaveplan/storage": "workspace:^",
|
||||
"@suaveplan/storage-file": "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"
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -0,0 +1,245 @@
|
||||
import { SuaveplanError } from "@suaveplan/error";
|
||||
import { beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
asDrive,
|
||||
FakeDrive,
|
||||
FakeDriveError,
|
||||
} from "../../tests/setup/fake-drive.js";
|
||||
import { DRIVE_URI_CODES, DriveUriError } from "../error-codes/error-codes.js";
|
||||
import {
|
||||
DriveStorageLikeAdapter,
|
||||
isDriveNotFound,
|
||||
} from "./drive-storage-like.js";
|
||||
|
||||
class CustomNotInDriveError extends SuaveplanError<string> {}
|
||||
|
||||
describe("isDriveNotFound", () => {
|
||||
it("matches any SuaveplanError whose code ends in .NOT_FOUND", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(
|
||||
isDriveNotFound(new CustomNotInDriveError("DRIVE.NOT_FOUND", "x"))
|
||||
).toBe(true);
|
||||
expect(
|
||||
isDriveNotFound(
|
||||
new CustomNotInDriveError("SOMETHING_ELSE.NOT_FOUND", "x")
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for non-SuaveplanError values", { timeout: 5000 }, () => {
|
||||
expect(isDriveNotFound(undefined)).toBe(false);
|
||||
expect(isDriveNotFound(null)).toBe(false);
|
||||
expect(isDriveNotFound("string")).toBe(false);
|
||||
expect(isDriveNotFound(new Error("plain"))).toBe(false);
|
||||
});
|
||||
|
||||
it("walks the cause chain", { timeout: 5000 }, () => {
|
||||
const inner = new FakeDriveError("DRIVE.NOT_FOUND", "missing");
|
||||
const outer = new Error("wrapped");
|
||||
(outer as Error & { cause?: unknown }).cause = inner;
|
||||
expect(isDriveNotFound(outer)).toBe(true);
|
||||
});
|
||||
|
||||
it("aborts on a self-referential cause cycle", { timeout: 5000 }, () => {
|
||||
const err = new Error("cyclic");
|
||||
(err as Error & { cause?: unknown }).cause = err;
|
||||
expect(isDriveNotFound(err)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no cause member exists", { timeout: 5000 }, () => {
|
||||
expect(isDriveNotFound({ message: "no cause", code: "X" })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DriveStorageLikeAdapter", () => {
|
||||
let fake: FakeDrive;
|
||||
let adapter: DriveStorageLikeAdapter;
|
||||
|
||||
beforeEach(() => {
|
||||
fake = new FakeDrive({
|
||||
contents: new Map([["a.bin", new Uint8Array([1, 2, 3])]]),
|
||||
});
|
||||
adapter = new DriveStorageLikeAdapter(asDrive(fake));
|
||||
});
|
||||
|
||||
it("returns undefined for Drive not-found errors", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const result = await adapter.get("missing.bin");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns the bytes on a hit", { timeout: 5000 }, async () => {
|
||||
const bytes = await adapter.get("a.bin");
|
||||
expect(bytes).toEqual(new Uint8Array([1, 2, 3]));
|
||||
});
|
||||
|
||||
it("re-throws non-not-found Drive errors unchanged", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const failure = new FakeDriveError(
|
||||
"DRIVE.PLUGIN_FAILED",
|
||||
"plugin crashed"
|
||||
);
|
||||
const bad = new FakeDrive({ readError: () => failure });
|
||||
const a = new DriveStorageLikeAdapter(asDrive(bad));
|
||||
await expect(a.get("x")).rejects.toBe(failure);
|
||||
});
|
||||
|
||||
it("forwards set, delete, exists, list", { timeout: 5000 }, async () => {
|
||||
await adapter.set("b.bin", new Uint8Array([9]));
|
||||
expect(fake.calls.write).toHaveLength(1);
|
||||
|
||||
expect(await adapter.exists("a.bin")).toBe(true);
|
||||
expect(await adapter.exists("missing")).toBe(false);
|
||||
|
||||
const removed = await adapter.delete("b.bin");
|
||||
expect(removed).toBe(true);
|
||||
|
||||
const listed = await adapter.list({ prefix: "a" });
|
||||
expect(listed.keys).toContain("a.bin");
|
||||
expect(listed.hasMore).toBe(false);
|
||||
expect(typeof listed.total).toBe("number");
|
||||
});
|
||||
|
||||
it("list with no options defaults prefix/limit to undefined", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const result = await adapter.list();
|
||||
expect(Array.isArray(result.keys)).toBe(true);
|
||||
});
|
||||
|
||||
it("list forwards options.limit to the drive", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const result = await adapter.list({ prefix: "a", limit: 10 });
|
||||
expect(Array.isArray(result.keys)).toBe(true);
|
||||
});
|
||||
|
||||
it("list copes with a drive returning entries-shape instead of keys-shape", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const odd = {
|
||||
async read(): Promise<Uint8Array> {
|
||||
return new Uint8Array();
|
||||
},
|
||||
async write(): Promise<void> {},
|
||||
async delete(): Promise<boolean> {
|
||||
return false;
|
||||
},
|
||||
async stat(): Promise<unknown | null> {
|
||||
return null;
|
||||
},
|
||||
async list(): Promise<unknown> {
|
||||
return {
|
||||
entries: [{ path: "p1" }, { path: "p2" }],
|
||||
};
|
||||
},
|
||||
createReadStream(): ReadableStream<Uint8Array> {
|
||||
return new ReadableStream();
|
||||
},
|
||||
};
|
||||
const a = new DriveStorageLikeAdapter(
|
||||
odd as unknown as Parameters<typeof DriveStorageLikeAdapter>[0]
|
||||
);
|
||||
const r = await a.list();
|
||||
expect(r.keys).toEqual(["p1", "p2"]);
|
||||
expect(r.total).toBe(2);
|
||||
});
|
||||
|
||||
it("list copes with a drive returning neither shape", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const oddler = {
|
||||
async read(): Promise<Uint8Array> {
|
||||
return new Uint8Array();
|
||||
},
|
||||
async write(): Promise<void> {},
|
||||
async delete(): Promise<boolean> {
|
||||
return false;
|
||||
},
|
||||
async stat(): Promise<unknown | null> {
|
||||
return null;
|
||||
},
|
||||
async list(): Promise<unknown> {
|
||||
return undefined;
|
||||
},
|
||||
createReadStream(): ReadableStream<Uint8Array> {
|
||||
return new ReadableStream();
|
||||
},
|
||||
};
|
||||
const a = new DriveStorageLikeAdapter(
|
||||
oddler as unknown as Parameters<typeof DriveStorageLikeAdapter>[0]
|
||||
);
|
||||
const r = await a.list();
|
||||
expect(r.keys).toEqual([]);
|
||||
expect(r.total).toBe(0);
|
||||
expect(r.hasMore).toBe(false);
|
||||
});
|
||||
|
||||
it("list forwards continuationToken when the drive returns one", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const cursored = {
|
||||
async read(): Promise<Uint8Array> {
|
||||
return new Uint8Array();
|
||||
},
|
||||
async write(): Promise<void> {},
|
||||
async delete(): Promise<boolean> {
|
||||
return false;
|
||||
},
|
||||
async stat(): Promise<unknown | null> {
|
||||
return null;
|
||||
},
|
||||
async list(): Promise<unknown> {
|
||||
return {
|
||||
keys: ["a", "b"],
|
||||
total: 99,
|
||||
hasMore: true,
|
||||
continuationToken: "next-page",
|
||||
};
|
||||
},
|
||||
createReadStream(): ReadableStream<Uint8Array> {
|
||||
return new ReadableStream();
|
||||
},
|
||||
};
|
||||
const a = new DriveStorageLikeAdapter(
|
||||
cursored as unknown as Parameters<typeof DriveStorageLikeAdapter>[0]
|
||||
);
|
||||
const r = await a.list();
|
||||
expect(r.continuationToken).toBe("next-page");
|
||||
expect(r.hasMore).toBe(true);
|
||||
expect(r.total).toBe(99);
|
||||
});
|
||||
|
||||
it("clear is unsupported and names the alternative", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
await expect(adapter.clear()).rejects.toBeInstanceOf(SuaveplanError);
|
||||
try {
|
||||
await adapter.clear();
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
expect((err as DriveUriError).code).toBe(
|
||||
DRIVE_URI_CODES.WRITE_UNSUPPORTED
|
||||
);
|
||||
expect((err as DriveUriError).message).toContain(
|
||||
"Drive.delete(path)"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("createReadStream forwards to Drive.createReadStream", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const stream = adapter.createReadStream("a.bin");
|
||||
expect(fake.calls.createReadStream).toHaveLength(1);
|
||||
// Drain it
|
||||
const reader = stream.getReader();
|
||||
const { value } = await reader.read();
|
||||
expect(value).toEqual(new Uint8Array([1, 2, 3]));
|
||||
await reader.read(); // close
|
||||
reader.releaseLock();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { Drive } from "@suaveplan/drive";
|
||||
import { SuaveplanError } from "@suaveplan/error";
|
||||
import type { StorageLike } from "@suaveplan/types";
|
||||
import { DRIVE_URI_CODES, DriveUriError } from "../error-codes/error-codes.js";
|
||||
|
||||
/**
|
||||
* Detect whether `err` (or anything in its `cause` chain) is a "not found"
|
||||
* marker emitted by `Drive` or its installed middleware. Matches any
|
||||
* `SuaveplanError` whose `code` ends in `.NOT_FOUND` (covers `DRIVE.NOT_FOUND`
|
||||
* surfaced by template hydration as well as adapter-level `DRIVE.IO_FAILED`
|
||||
* variants whose underlying cause is itself a not-found marker).
|
||||
*/
|
||||
export function isDriveNotFound(err: unknown): boolean {
|
||||
let cursor: unknown = err;
|
||||
for (
|
||||
let depth = 0;
|
||||
cursor !== undefined && cursor !== null && depth < 8;
|
||||
depth++
|
||||
) {
|
||||
if (cursor instanceof SuaveplanError) {
|
||||
if (cursor.code.endsWith(".NOT_FOUND")) return true;
|
||||
}
|
||||
if (
|
||||
typeof cursor === "object" &&
|
||||
cursor !== null &&
|
||||
"cause" in cursor
|
||||
) {
|
||||
const next: unknown = (cursor as { cause: unknown }).cause;
|
||||
if (next === cursor) return false;
|
||||
cursor = next;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
interface DriveStatLike {
|
||||
readonly size?: number;
|
||||
readonly modifiedAt?: number;
|
||||
readonly createdAt?: number;
|
||||
readonly checksum?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapter that exposes a `Drive` instance as a `StorageLike` so it can be
|
||||
* registered as the `adapter` on a `UriPlugin` consumed by
|
||||
* `StorageUriResolver`.
|
||||
*
|
||||
* The shim translates `Drive.read` rejections whose `cause` chain indicates a
|
||||
* not-found condition into `undefined` (matching `StorageLike.get`'s contract)
|
||||
* — every other failure is re-thrown unchanged so the upstream resolver can
|
||||
* wrap it as `STORAGE_URI.ADAPTER_ERROR`.
|
||||
*
|
||||
* `clear()` rejects with an explicit `SuaveplanError` pointing the caller at
|
||||
* `Drive.delete(path)`. Wiping a Drive through the URI surface is an obvious
|
||||
* footgun; the resolver's `remove(uri)` is the supported path.
|
||||
*
|
||||
* `createReadStream(key, signal)` forwards directly to
|
||||
* `Drive.createReadStream` so `StorageUriResolver.open()` uses the native
|
||||
* streaming path — `storage_uri.stream_fallback_total` is NOT incremented.
|
||||
*/
|
||||
export class DriveStorageLikeAdapter implements StorageLike {
|
||||
public constructor(private readonly drive: Drive) {}
|
||||
|
||||
public async get(key: string): Promise<Uint8Array | undefined> {
|
||||
try {
|
||||
return await this.drive.read(key);
|
||||
} catch (err) {
|
||||
if (isDriveNotFound(err)) return undefined;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async set(key: string, value: Uint8Array): Promise<void> {
|
||||
await this.drive.write(key, value);
|
||||
}
|
||||
|
||||
public async delete(key: string): Promise<boolean> {
|
||||
return this.drive.delete(key);
|
||||
}
|
||||
|
||||
public async exists(key: string): Promise<boolean> {
|
||||
const result = (await this.drive.stat(key)) as DriveStatLike | null;
|
||||
return result !== null && result !== undefined;
|
||||
}
|
||||
|
||||
public async list(options?: { prefix?: string; limit?: number }): Promise<{
|
||||
keys: string[];
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
continuationToken?: string | undefined;
|
||||
}> {
|
||||
const result = (await this.drive.list(options?.prefix, {
|
||||
...(options?.limit !== undefined ? { limit: options.limit } : {}),
|
||||
})) as
|
||||
| {
|
||||
keys?: string[];
|
||||
entries?: { path: string }[];
|
||||
total?: number;
|
||||
hasMore?: boolean;
|
||||
continuationToken?: string;
|
||||
}
|
||||
| undefined;
|
||||
const keys = Array.isArray(result?.keys)
|
||||
? (result?.keys as string[])
|
||||
: Array.isArray(result?.entries)
|
||||
? (result?.entries as { path: string }[]).map((e) => e.path)
|
||||
: [];
|
||||
return {
|
||||
keys,
|
||||
total:
|
||||
typeof result?.total === "number" ? result.total : keys.length,
|
||||
hasMore: result?.hasMore === true,
|
||||
...(typeof result?.continuationToken === "string"
|
||||
? { continuationToken: result.continuationToken }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
public async clear(): Promise<void> {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.WRITE_UNSUPPORTED,
|
||||
"clear() is not supported on Drive — use Drive.delete(path) (or resolver.remove(uri)) for targeted removal"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward to `Drive.createReadStream(key, signal)`. Used by
|
||||
* `StorageUriResolver.open()`'s native-stream fast path so the resolver
|
||||
* does not buffer the entire payload.
|
||||
*/
|
||||
public createReadStream(
|
||||
key: string,
|
||||
signal?: AbortSignal
|
||||
): ReadableStream<Uint8Array> {
|
||||
return this.drive.createReadStream(key, signal);
|
||||
}
|
||||
}
|
||||
4
packages/networking/drive/drive-uri/src/adapter/index.ts
Normal file
4
packages/networking/drive/drive-uri/src/adapter/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
DriveStorageLikeAdapter,
|
||||
isDriveNotFound,
|
||||
} from "./drive-storage-like.js";
|
||||
@@ -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,123 @@
|
||||
import type { UriPluginFixtures } from "@suaveplan/storage-uri";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { asDrive, FakeDrive } from "../../tests/setup/fake-drive.js";
|
||||
import { driveUriPlugin } from "../plugins/drive/drive.js";
|
||||
import { driveMeshUriPlugin } from "../plugins/drive-mesh/drive-mesh.js";
|
||||
import { driveTemplateUriPlugin } from "../plugins/drive-template/drive-template.js";
|
||||
import type { TemplateProvider } from "../types/plugin-options.types.js";
|
||||
import { runUriPluginConformance } from "./conformance.js";
|
||||
|
||||
const fake = new FakeDrive();
|
||||
const fakeProvider: TemplateProvider = {
|
||||
async fetchTemplate(): Promise<never> {
|
||||
return {} as never;
|
||||
},
|
||||
};
|
||||
|
||||
const driveFixtures: UriPluginFixtures = {
|
||||
validUris: [
|
||||
"drive://primary/a",
|
||||
"drive://primary/nested/path/file.bin",
|
||||
"drive://primary/with-dash_and.dot",
|
||||
],
|
||||
invalidUris: ["", "://no-scheme", "drive:", "drive://primary/with space"],
|
||||
buildRoundTrip: [
|
||||
{
|
||||
key: "a",
|
||||
meta: { driveId: "primary" },
|
||||
uri: "drive://primary/a",
|
||||
},
|
||||
{
|
||||
key: "nested/path/file.bin",
|
||||
meta: { driveId: "primary" },
|
||||
uri: "drive://primary/nested/path/file.bin",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const meshFixtures: UriPluginFixtures = {
|
||||
validUris: [
|
||||
"drive+mesh://swarmA/a",
|
||||
"drive+mesh://swarmA/nested/path/file.bin",
|
||||
"drive+mesh://swarmA/with-dash_and.dot",
|
||||
],
|
||||
invalidUris: [
|
||||
"",
|
||||
"drive:",
|
||||
"drive+mesh://swarmA/path with space",
|
||||
"drive+mesh:///x",
|
||||
],
|
||||
buildRoundTrip: [
|
||||
{
|
||||
key: "a",
|
||||
meta: { swarmId: "swarmA" },
|
||||
uri: "drive+mesh://swarmA/a",
|
||||
},
|
||||
{
|
||||
key: "raw/clip.mov",
|
||||
meta: { swarmId: "swarmA" },
|
||||
uri: "drive+mesh://swarmA/raw/clip.mov",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const templateFixtures: UriPluginFixtures = {
|
||||
validUris: [
|
||||
"drive+template://templA/a",
|
||||
"drive+template://templA/nested/path/file.bin",
|
||||
"drive+template://templA/with-dash_and.dot",
|
||||
],
|
||||
invalidUris: [
|
||||
"",
|
||||
"drive:",
|
||||
"drive+template://templA/path with space",
|
||||
"drive+template:///x",
|
||||
],
|
||||
buildRoundTrip: [
|
||||
{
|
||||
key: "a",
|
||||
meta: { templateId: "templA" },
|
||||
uri: "drive+template://templA/a",
|
||||
},
|
||||
{
|
||||
key: "manifest/clip.jpg",
|
||||
meta: { templateId: "templA" },
|
||||
uri: "drive+template://templA/manifest/clip.jpg",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe("runUriPluginConformance", () => {
|
||||
it("drive plugin satisfies runUriPluginConformance", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
expect(() =>
|
||||
runUriPluginConformance(plugin, driveFixtures)
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("drive+mesh plugin satisfies runUriPluginConformance", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveMeshUriPlugin({ drive: asDrive(fake) });
|
||||
expect(() =>
|
||||
runUriPluginConformance(plugin, meshFixtures)
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("drive+template plugin satisfies runUriPluginConformance", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: fakeProvider,
|
||||
});
|
||||
expect(() =>
|
||||
runUriPluginConformance(plugin, templateFixtures)
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Re-export the canonical conformance harness from `@suaveplan/storage-uri`.
|
||||
*
|
||||
* `drive-uri` does not author its own conformance harness — every URI plugin
|
||||
* package in the genesis monorepo runs against the same harness so the
|
||||
* "round-trip parse/build + invalid URI rejection" contract stays in one
|
||||
* place. Each drive-uri plugin's fixture set lives in its co-located test
|
||||
* file and is fed into the harness from there.
|
||||
*/
|
||||
export {
|
||||
runUriPluginConformance,
|
||||
UriPluginConformanceFailure,
|
||||
type UriPluginFixtures,
|
||||
} from "@suaveplan/storage-uri";
|
||||
@@ -0,0 +1,5 @@
|
||||
export type { UriPluginFixtures } from "./conformance.js";
|
||||
export {
|
||||
runUriPluginConformance,
|
||||
UriPluginConformanceFailure,
|
||||
} from "./conformance.js";
|
||||
@@ -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.
|
||||
@@ -0,0 +1,116 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
DRIVE_URI_CODES,
|
||||
DriveUriError,
|
||||
type DriveUriErrorCode,
|
||||
} from "./error-codes.js";
|
||||
|
||||
describe("DRIVE_URI_CODES registry", () => {
|
||||
it("exposes all seven namespaced codes", { timeout: 5000 }, () => {
|
||||
const codes: DriveUriErrorCode[] = [
|
||||
DRIVE_URI_CODES.INVALID_URI,
|
||||
DRIVE_URI_CODES.DRIVE_ID_MISMATCH,
|
||||
DRIVE_URI_CODES.TEMPLATE_NOT_AVAILABLE,
|
||||
DRIVE_URI_CODES.TEMPLATE_INVALID,
|
||||
DRIVE_URI_CODES.WRITE_UNSUPPORTED,
|
||||
DRIVE_URI_CODES.CAPABILITY_MISSING,
|
||||
DRIVE_URI_CODES.ABORTED,
|
||||
];
|
||||
expect(codes).toHaveLength(7);
|
||||
for (const code of codes) {
|
||||
expect(code.startsWith("DRIVE_URI.")).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("DriveUriError", () => {
|
||||
it("populates every discriminant from the options bag", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const cause = new Error("underlying");
|
||||
const err = new DriveUriError(
|
||||
DRIVE_URI_CODES.DRIVE_ID_MISMATCH,
|
||||
"mismatch",
|
||||
{
|
||||
uri: "drive://other/x",
|
||||
scheme: "drive",
|
||||
driveId: "primary",
|
||||
swarmId: "swarmA",
|
||||
templateId: "templA",
|
||||
path: "x",
|
||||
expected: "primary",
|
||||
actual: "other",
|
||||
reason: "test",
|
||||
cause,
|
||||
}
|
||||
);
|
||||
expect(err.code).toBe(DRIVE_URI_CODES.DRIVE_ID_MISMATCH);
|
||||
expect(err.uri).toBe("drive://other/x");
|
||||
expect(err.scheme).toBe("drive");
|
||||
expect(err.driveId).toBe("primary");
|
||||
expect(err.swarmId).toBe("swarmA");
|
||||
expect(err.templateId).toBe("templA");
|
||||
expect(err.path).toBe("x");
|
||||
expect(err.expected).toBe("primary");
|
||||
expect(err.actual).toBe("other");
|
||||
expect(err.reason).toBe("test");
|
||||
expect(err.cause).toBe(cause);
|
||||
expect(err.name).toBe("DriveUriError");
|
||||
});
|
||||
|
||||
it("leaves unspecified discriminants undefined", { timeout: 5000 }, () => {
|
||||
const err = new DriveUriError(
|
||||
DRIVE_URI_CODES.INVALID_URI,
|
||||
"no options at all"
|
||||
);
|
||||
expect(err.uri).toBeUndefined();
|
||||
expect(err.scheme).toBeUndefined();
|
||||
expect(err.driveId).toBeUndefined();
|
||||
expect(err.swarmId).toBeUndefined();
|
||||
expect(err.templateId).toBeUndefined();
|
||||
expect(err.path).toBeUndefined();
|
||||
expect(err.expected).toBeUndefined();
|
||||
expect(err.actual).toBeUndefined();
|
||||
expect(err.reason).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves cause across re-wraps", { timeout: 5000 }, () => {
|
||||
const inner = new Error("inner failure");
|
||||
const middle = new DriveUriError(
|
||||
DRIVE_URI_CODES.TEMPLATE_NOT_AVAILABLE,
|
||||
"fetch failed",
|
||||
{ cause: inner }
|
||||
);
|
||||
const outer = new DriveUriError(
|
||||
DRIVE_URI_CODES.TEMPLATE_INVALID,
|
||||
"rewrapped",
|
||||
{ cause: middle }
|
||||
);
|
||||
expect(outer.cause).toBe(middle);
|
||||
expect((outer.cause as DriveUriError).cause).toBe(inner);
|
||||
});
|
||||
|
||||
it("is reachable for every registered code via a real throw", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const allCodes: DriveUriErrorCode[] = [
|
||||
DRIVE_URI_CODES.INVALID_URI,
|
||||
DRIVE_URI_CODES.DRIVE_ID_MISMATCH,
|
||||
DRIVE_URI_CODES.TEMPLATE_NOT_AVAILABLE,
|
||||
DRIVE_URI_CODES.TEMPLATE_INVALID,
|
||||
DRIVE_URI_CODES.WRITE_UNSUPPORTED,
|
||||
DRIVE_URI_CODES.CAPABILITY_MISSING,
|
||||
DRIVE_URI_CODES.ABORTED,
|
||||
];
|
||||
for (const code of allCodes) {
|
||||
let caught: DriveUriError | undefined;
|
||||
try {
|
||||
throw new DriveUriError(code, `test for ${code}`);
|
||||
} catch (err) {
|
||||
caught = err as DriveUriError;
|
||||
}
|
||||
expect(caught).toBeInstanceOf(DriveUriError);
|
||||
expect(caught?.code).toBe(code);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
defineCodes,
|
||||
SuaveplanError,
|
||||
type SuaveplanErrorOptions,
|
||||
} from "@suaveplan/error";
|
||||
|
||||
/**
|
||||
* Registry of every error code raised by `@suaveplan/drive-uri`.
|
||||
*
|
||||
* Every member is the fully-qualified string `DRIVE_URI.<REASON>` produced by
|
||||
* `defineCodes`. Seven codes total — `INVALID_URI`, `DRIVE_ID_MISMATCH`,
|
||||
* `TEMPLATE_NOT_AVAILABLE`, `TEMPLATE_INVALID`, `WRITE_UNSUPPORTED`,
|
||||
* `CAPABILITY_MISSING`, `ABORTED` — all reachable from a real call site
|
||||
* exercised by the test suite.
|
||||
*/
|
||||
export const DRIVE_URI_CODES = defineCodes("DRIVE_URI", [
|
||||
"INVALID_URI",
|
||||
"DRIVE_ID_MISMATCH",
|
||||
"TEMPLATE_NOT_AVAILABLE",
|
||||
"TEMPLATE_INVALID",
|
||||
"WRITE_UNSUPPORTED",
|
||||
"CAPABILITY_MISSING",
|
||||
"ABORTED",
|
||||
] as const);
|
||||
|
||||
export type DriveUriErrorCode =
|
||||
(typeof DRIVE_URI_CODES)[keyof typeof DRIVE_URI_CODES];
|
||||
|
||||
export interface DriveUriErrorOptions extends SuaveplanErrorOptions {
|
||||
uri?: string;
|
||||
scheme?: string;
|
||||
driveId?: string;
|
||||
swarmId?: string;
|
||||
templateId?: string;
|
||||
path?: string;
|
||||
expected?: string;
|
||||
actual?: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Structured error for drive-URI plugin operations.
|
||||
*
|
||||
* Carries optional discriminants (`uri`, `scheme`, `driveId`, `swarmId`,
|
||||
* `templateId`, `path`, `expected`/`actual` for mismatches, `reason` for
|
||||
* capability-missing failures) so operators can correlate a failure with the
|
||||
* offending request without parsing message text. The original underlying
|
||||
* cause (when re-wrapping a `Drive`, `DTP_*`, or `templateProvider` throwable)
|
||||
* is preserved on `cause` via the base class.
|
||||
*/
|
||||
export class DriveUriError extends SuaveplanError<DriveUriErrorCode> {
|
||||
public readonly uri: string | undefined;
|
||||
public readonly scheme: string | undefined;
|
||||
public readonly driveId: string | undefined;
|
||||
public readonly swarmId: string | undefined;
|
||||
public readonly templateId: string | undefined;
|
||||
public readonly path: string | undefined;
|
||||
public readonly expected: string | undefined;
|
||||
public readonly actual: string | undefined;
|
||||
public readonly reason: string | undefined;
|
||||
|
||||
public constructor(
|
||||
code: DriveUriErrorCode,
|
||||
message: string,
|
||||
options?: DriveUriErrorOptions
|
||||
) {
|
||||
super(code, message, options);
|
||||
this.uri = options?.uri;
|
||||
this.scheme = options?.scheme;
|
||||
this.driveId = options?.driveId;
|
||||
this.swarmId = options?.swarmId;
|
||||
this.templateId = options?.templateId;
|
||||
this.path = options?.path;
|
||||
this.expected = options?.expected;
|
||||
this.actual = options?.actual;
|
||||
this.reason = options?.reason;
|
||||
this.name = "DriveUriError";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export type {
|
||||
DriveUriErrorCode,
|
||||
DriveUriErrorOptions,
|
||||
} from "./error-codes.js";
|
||||
export {
|
||||
DRIVE_URI_CODES,
|
||||
DriveUriError,
|
||||
} from "./error-codes.js";
|
||||
44
packages/networking/drive/drive-uri/src/index.ts
Normal file
44
packages/networking/drive/drive-uri/src/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// Side-effect import: ensures the DRIVE_URI.* code registry registers even
|
||||
// when subpath consumers never reach this barrel.
|
||||
import "./error-codes/error-codes.js";
|
||||
|
||||
export {
|
||||
DriveStorageLikeAdapter,
|
||||
isDriveNotFound,
|
||||
} from "./adapter/drive-storage-like.js";
|
||||
export {
|
||||
runUriPluginConformance,
|
||||
UriPluginConformanceFailure,
|
||||
type UriPluginFixtures,
|
||||
} from "./conformance/conformance.js";
|
||||
export {
|
||||
DRIVE_URI_CODES,
|
||||
DriveUriError,
|
||||
type DriveUriErrorCode,
|
||||
type DriveUriErrorOptions,
|
||||
} from "./error-codes/error-codes.js";
|
||||
export {
|
||||
type DriveUriInstruments,
|
||||
type DriveUriOp,
|
||||
type DriveUriOutcome,
|
||||
getInstruments,
|
||||
type RecordOpInput,
|
||||
recordOp,
|
||||
recordTemplateHydration,
|
||||
redactUri,
|
||||
resetInstruments,
|
||||
setDriveUriMeterProvider,
|
||||
} from "./otel/otel.js";
|
||||
export { driveUriPlugin } from "./plugins/drive/drive.js";
|
||||
export {
|
||||
type InstrumentedAdapterOptions,
|
||||
InstrumentedDriveAdapter,
|
||||
} from "./plugins/drive/drive.streams.js";
|
||||
export { driveMeshUriPlugin } from "./plugins/drive-mesh/drive-mesh.js";
|
||||
export { driveTemplateUriPlugin } from "./plugins/drive-template/drive-template.js";
|
||||
export type {
|
||||
DriveMeshUriPluginOptions,
|
||||
DriveTemplateUriPluginOptions,
|
||||
DriveUriPluginOptions,
|
||||
TemplateProvider,
|
||||
} from "./types/plugin-options.types.js";
|
||||
14
packages/networking/drive/drive-uri/src/otel/index.ts
Normal file
14
packages/networking/drive/drive-uri/src/otel/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type {
|
||||
DriveUriInstruments,
|
||||
DriveUriOp,
|
||||
DriveUriOutcome,
|
||||
RecordOpInput,
|
||||
} from "./otel.js";
|
||||
export {
|
||||
getInstruments,
|
||||
recordOp,
|
||||
recordTemplateHydration,
|
||||
redactUri,
|
||||
resetInstruments,
|
||||
setDriveUriMeterProvider,
|
||||
} from "./otel.js";
|
||||
92
packages/networking/drive/drive-uri/src/otel/otel.md
Normal file
92
packages/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.
|
||||
294
packages/networking/drive/drive-uri/src/otel/otel.test.ts
Normal file
294
packages/networking/drive/drive-uri/src/otel/otel.test.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import type {
|
||||
Counter,
|
||||
Histogram,
|
||||
Meter,
|
||||
MeterProvider,
|
||||
} from "@opentelemetry/api";
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import { asDrive, FakeDrive } from "../../tests/setup/fake-drive.js";
|
||||
import { driveUriPlugin } from "../plugins/drive/drive.js";
|
||||
import { driveMeshUriPlugin } from "../plugins/drive-mesh/drive-mesh.js";
|
||||
import { driveTemplateUriPlugin } from "../plugins/drive-template/drive-template.js";
|
||||
import {
|
||||
recordOp,
|
||||
recordTemplateHydration,
|
||||
redactUri,
|
||||
resetInstruments,
|
||||
setDriveUriMeterProvider,
|
||||
} 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 = new Map<string, StubCounter>();
|
||||
public histograms = new Map<string, StubHistogram>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
describe("drive-uri otel", () => {
|
||||
let provider: StubMeterProvider;
|
||||
beforeEach(() => {
|
||||
provider = new StubMeterProvider();
|
||||
setDriveUriMeterProvider(provider);
|
||||
resetInstruments();
|
||||
});
|
||||
afterEach(() => {
|
||||
setDriveUriMeterProvider(undefined);
|
||||
resetInstruments();
|
||||
});
|
||||
|
||||
it("records duration histogram and reads counter on success", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fake = new FakeDrive({
|
||||
contents: new Map([["x", new Uint8Array([1])]]),
|
||||
});
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
await resolver.read("drive://primary/x");
|
||||
const dur = provider.meter.histograms.get("drive_uri.op_duration_ms");
|
||||
expect(dur?.samples.length).toBeGreaterThan(0);
|
||||
const sample = dur?.samples.find(
|
||||
(s) => s.attrs?.["outcome"] === "success"
|
||||
);
|
||||
expect(sample?.attrs).toMatchObject({
|
||||
op: "read",
|
||||
scheme: "drive",
|
||||
outcome: "success",
|
||||
});
|
||||
const reads = provider.meter.counters.get("drive_uri.reads_total");
|
||||
expect(reads?.adds[0]?.attrs).toMatchObject({
|
||||
scheme: "drive",
|
||||
outcome: "success",
|
||||
});
|
||||
});
|
||||
|
||||
it("increments errors_total with the resolved DRIVE_URI code", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fake = new FakeDrive();
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: {
|
||||
async fetchTemplate(): Promise<never> {
|
||||
throw new Error("404");
|
||||
},
|
||||
},
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
await expect(
|
||||
resolver.read("drive+template://templA/x")
|
||||
).rejects.toBeDefined();
|
||||
const errs = provider.meter.counters.get("drive_uri.errors_total");
|
||||
const match = errs?.adds.find(
|
||||
(a) =>
|
||||
a.attrs?.["code"] === "DRIVE_URI.TEMPLATE_NOT_AVAILABLE" &&
|
||||
a.attrs?.["scheme"] === "drive+template"
|
||||
);
|
||||
expect(match).toBeDefined();
|
||||
});
|
||||
|
||||
it("strips query strings from the drive.uri span attribute", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(redactUri("drive+mesh://swarmA/path?peers=abcdef")).toBe(
|
||||
"drive+mesh://swarmA/path"
|
||||
);
|
||||
expect(redactUri("drive://primary/clip#frag")).toBe(
|
||||
"drive://primary/clip"
|
||||
);
|
||||
expect(redactUri("drive://primary/clip")).toBe("drive://primary/clip");
|
||||
});
|
||||
|
||||
it("increments template_hydrations_total exactly once per first-use template", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fake = new FakeDrive({
|
||||
contents: new Map([
|
||||
["x", new Uint8Array([1])],
|
||||
["y", new Uint8Array([2])],
|
||||
]),
|
||||
});
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: {
|
||||
async fetchTemplate(): Promise<unknown> {
|
||||
return {};
|
||||
},
|
||||
} as unknown as Parameters<
|
||||
typeof driveTemplateUriPlugin
|
||||
>[0]["templateProvider"],
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
await resolver.read("drive+template://templA/x");
|
||||
await resolver.read("drive+template://templA/y");
|
||||
const hyd = provider.meter.counters.get(
|
||||
"drive_uri.template_hydrations_total"
|
||||
);
|
||||
expect(hyd?.adds).toHaveLength(1);
|
||||
expect(hyd?.adds[0]?.attrs).toMatchObject({ outcome: "success" });
|
||||
});
|
||||
|
||||
it("increments writes_total on rejected writes", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fake = new FakeDrive();
|
||||
const plugin = driveMeshUriPlugin({ drive: asDrive(fake) });
|
||||
const adapter = plugin.adapter as {
|
||||
set(key: string, value: Uint8Array): Promise<void>;
|
||||
};
|
||||
await expect(
|
||||
adapter.set("x", new Uint8Array([1]))
|
||||
).rejects.toBeDefined();
|
||||
const writes = provider.meter.counters.get("drive_uri.writes_total");
|
||||
expect(writes?.adds[0]?.attrs).toMatchObject({
|
||||
op: "write",
|
||||
scheme: "drive+mesh",
|
||||
outcome: "error",
|
||||
});
|
||||
});
|
||||
|
||||
it("recordOp routes read-class ops to reads_total only on success", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
for (const op of ["read", "open", "stat", "exists", "list"] as const) {
|
||||
recordOp({
|
||||
op,
|
||||
scheme: "drive",
|
||||
outcome: "success",
|
||||
durationMs: 1,
|
||||
});
|
||||
}
|
||||
expect(
|
||||
provider.meter.counters.get("drive_uri.reads_total")?.adds
|
||||
).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("recordOp records error outcome separately into errors_total", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
recordOp({
|
||||
op: "read",
|
||||
scheme: "drive",
|
||||
outcome: "error",
|
||||
durationMs: 1,
|
||||
});
|
||||
const errs = provider.meter.counters.get("drive_uri.errors_total");
|
||||
expect(errs?.adds[0]?.attrs).toMatchObject({ code: "UNKNOWN" });
|
||||
});
|
||||
|
||||
it("recordOp handles not_found read outcomes without incrementing reads_total", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
recordOp({
|
||||
op: "read",
|
||||
scheme: "drive",
|
||||
outcome: "not_found",
|
||||
durationMs: 1,
|
||||
});
|
||||
expect(
|
||||
provider.meter.counters.get("drive_uri.reads_total")?.adds.length ??
|
||||
0
|
||||
).toBe(0);
|
||||
expect(
|
||||
provider.meter.counters.get("drive_uri.errors_total")?.adds.length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("recordTemplateHydration records the supplied outcome attribute", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
recordTemplateHydration("success");
|
||||
recordTemplateHydration("error");
|
||||
const hyd = provider.meter.counters.get(
|
||||
"drive_uri.template_hydrations_total"
|
||||
);
|
||||
expect(hyd?.adds).toHaveLength(2);
|
||||
expect(hyd?.adds[0]?.attrs).toMatchObject({ outcome: "success" });
|
||||
expect(hyd?.adds[1]?.attrs).toMatchObject({ outcome: "error" });
|
||||
});
|
||||
|
||||
it("getInstruments caches between calls and rebuilds on provider swap", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
recordOp({
|
||||
op: "read",
|
||||
scheme: "x",
|
||||
outcome: "success",
|
||||
durationMs: 1,
|
||||
});
|
||||
const reads1 = provider.meter.counters.get("drive_uri.reads_total");
|
||||
expect(reads1?.adds.length).toBe(1);
|
||||
const newProvider = new StubMeterProvider();
|
||||
setDriveUriMeterProvider(newProvider);
|
||||
recordOp({
|
||||
op: "read",
|
||||
scheme: "x",
|
||||
outcome: "success",
|
||||
durationMs: 1,
|
||||
});
|
||||
expect(
|
||||
newProvider.meter.counters.get("drive_uri.reads_total")?.adds.length
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
||||
156
packages/networking/drive/drive-uri/src/otel/otel.ts
Normal file
156
packages/networking/drive/drive-uri/src/otel/otel.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import {
|
||||
type Counter,
|
||||
type Histogram,
|
||||
type Meter,
|
||||
type MeterProvider,
|
||||
metrics,
|
||||
type Tracer,
|
||||
trace,
|
||||
} from "@opentelemetry/api";
|
||||
|
||||
const METER_NAME = "@suaveplan/drive-uri";
|
||||
const TRACER_NAME = "@suaveplan/drive-uri";
|
||||
|
||||
let overrideMeterProvider: MeterProvider | undefined;
|
||||
|
||||
/**
|
||||
* Override the `MeterProvider` used to acquire the drive-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 setDriveUriMeterProvider(
|
||||
provider: MeterProvider | undefined
|
||||
): void {
|
||||
overrideMeterProvider = provider;
|
||||
cachedInstruments = undefined;
|
||||
}
|
||||
|
||||
export type DriveUriOp =
|
||||
| "open"
|
||||
| "read"
|
||||
| "write"
|
||||
| "stat"
|
||||
| "exists"
|
||||
| "remove"
|
||||
| "list";
|
||||
|
||||
export type DriveUriOutcome = "success" | "not_found" | "error" | "aborted";
|
||||
|
||||
export interface DriveUriInstruments {
|
||||
readonly tracer: Tracer;
|
||||
readonly meter: Meter;
|
||||
readonly readsTotal: Counter;
|
||||
readonly writesTotal: Counter;
|
||||
readonly deletesTotal: Counter;
|
||||
readonly errorsTotal: Counter;
|
||||
readonly templateHydrationsTotal: Counter;
|
||||
readonly opDurationMs: Histogram;
|
||||
}
|
||||
|
||||
let cachedInstruments: DriveUriInstruments | undefined;
|
||||
|
||||
/** Lazily build (or return cached) instruments bound to the active provider. */
|
||||
export function getInstruments(force = false): DriveUriInstruments {
|
||||
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("drive_uri.reads_total", {
|
||||
description:
|
||||
"Read operations dispatched via drive-uri, partitioned by scheme and outcome",
|
||||
}),
|
||||
writesTotal: meter.createCounter("drive_uri.writes_total", {
|
||||
description:
|
||||
"Write operations attempted via drive-uri (initial release rejects every write)",
|
||||
}),
|
||||
deletesTotal: meter.createCounter("drive_uri.deletes_total", {
|
||||
description: "Delete operations attempted via drive-uri",
|
||||
}),
|
||||
errorsTotal: meter.createCounter("drive_uri.errors_total", {
|
||||
description:
|
||||
"Operations that ended in error, attributed by DRIVE_URI.* code",
|
||||
}),
|
||||
templateHydrationsTotal: meter.createCounter(
|
||||
"drive_uri.template_hydrations_total",
|
||||
{
|
||||
description:
|
||||
"Number of first-use template hydrations performed by drive+template plugin instances (cache hits do not increment)",
|
||||
}
|
||||
),
|
||||
opDurationMs: meter.createHistogram("drive_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<DriveUriOp, "reads" | "writes" | "deletes"> = {
|
||||
read: "reads",
|
||||
open: "reads",
|
||||
stat: "reads",
|
||||
exists: "reads",
|
||||
list: "reads",
|
||||
write: "writes",
|
||||
remove: "deletes",
|
||||
};
|
||||
|
||||
export interface RecordOpInput {
|
||||
readonly op: DriveUriOp;
|
||||
readonly scheme: string;
|
||||
readonly outcome: DriveUriOutcome;
|
||||
readonly durationMs: number;
|
||||
/** Resolved `DRIVE_URI.*` code when `outcome === "error"`. */
|
||||
readonly code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record one full observability bundle for a completed operation.
|
||||
*
|
||||
* Per spec, `drive_uri.writes_total{scheme, outcome: "error"}` increments
|
||||
* even on rejection (so operators can see misuse rate); the success vs.
|
||||
* error counter routing is handled below.
|
||||
*/
|
||||
export function recordOp(input: RecordOpInput): void {
|
||||
const inst = getInstruments();
|
||||
const { op, scheme, outcome, durationMs, code } = input;
|
||||
inst.opDurationMs.record(durationMs, { op, scheme, outcome });
|
||||
const counter = SUCCESS_COUNTER_OF[op];
|
||||
const attrs = { op, scheme, outcome };
|
||||
if (counter === "writes") {
|
||||
inst.writesTotal.add(1, attrs);
|
||||
} else if (counter === "deletes") {
|
||||
inst.deletesTotal.add(1, attrs);
|
||||
} else if (outcome === "success") {
|
||||
inst.readsTotal.add(1, attrs);
|
||||
}
|
||||
if (outcome !== "success") {
|
||||
inst.errorsTotal.add(1, { scheme, code: code ?? "UNKNOWN" });
|
||||
}
|
||||
}
|
||||
|
||||
/** Increment `drive_uri.template_hydrations_total{outcome}` by one. */
|
||||
export function recordTemplateHydration(outcome: "success" | "error"): void {
|
||||
getInstruments().templateHydrationsTotal.add(1, { outcome });
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip the query string and fragment from a URI so span attributes never
|
||||
* leak peer hints, seeds, or other sensitive material. Returns the input
|
||||
* verbatim when no `?` or `#` is present.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
@@ -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,157 @@
|
||||
import { trace } from "@opentelemetry/api";
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import { beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { asDrive, FakeDrive } from "../../../tests/setup/fake-drive.js";
|
||||
import {
|
||||
DRIVE_URI_CODES,
|
||||
DriveUriError,
|
||||
} from "../../error-codes/error-codes.js";
|
||||
import { driveMeshUriPlugin } from "./drive-mesh.js";
|
||||
|
||||
interface SpanLike {
|
||||
setAttribute(name: string, value: unknown): SpanLike;
|
||||
end(): void;
|
||||
readonly attributes: Map<string, unknown>;
|
||||
}
|
||||
|
||||
function makeRecordingSpan(): SpanLike {
|
||||
const attrs = new Map<string, unknown>();
|
||||
return {
|
||||
attributes: attrs,
|
||||
setAttribute(name: string, value: unknown): SpanLike {
|
||||
attrs.set(name, value);
|
||||
return this;
|
||||
},
|
||||
end(): void {},
|
||||
};
|
||||
}
|
||||
|
||||
describe("driveMeshUriPlugin", () => {
|
||||
let fake: FakeDrive;
|
||||
|
||||
beforeEach(() => {
|
||||
fake = new FakeDrive({
|
||||
contents: new Map([["raw/clip.mov", new Uint8Array([1, 2, 3])]]),
|
||||
});
|
||||
});
|
||||
|
||||
it("requires options.drive at construction", { timeout: 5000 }, () => {
|
||||
expect(() =>
|
||||
driveMeshUriPlugin({
|
||||
drive: undefined as unknown as Parameters<
|
||||
typeof driveMeshUriPlugin
|
||||
>[0]["drive"],
|
||||
})
|
||||
).toThrow(DriveUriError);
|
||||
});
|
||||
|
||||
it("rejects non-mesh schemes", { timeout: 5000 }, () => {
|
||||
const plugin = driveMeshUriPlugin({ drive: asDrive(fake) });
|
||||
try {
|
||||
plugin.parse("drive://primary/x");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
expect((err as DriveUriError).code).toBe(
|
||||
DRIVE_URI_CODES.INVALID_URI
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects URIs without an authority", { timeout: 5000 }, () => {
|
||||
const plugin = driveMeshUriPlugin({ drive: asDrive(fake) });
|
||||
expect(() => plugin.parse("drive+mesh:///x")).toThrow(DriveUriError);
|
||||
});
|
||||
|
||||
it("rejects URIs without a path", { timeout: 5000 }, () => {
|
||||
const plugin = driveMeshUriPlugin({ drive: asDrive(fake) });
|
||||
expect(() => plugin.parse("drive+mesh://swarmA/")).toThrow(
|
||||
DriveUriError
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects URIs with no authority component (no //)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveMeshUriPlugin({ drive: asDrive(fake) });
|
||||
// `drive+mesh:x` has scheme + path but no authority — covers the
|
||||
// `parts.authority ?? ""` fallback branch.
|
||||
expect(() => plugin.parse("drive+mesh:x")).toThrow(DriveUriError);
|
||||
});
|
||||
|
||||
it("dispatches a drive+mesh:// URI to the configured Drive", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const plugin = driveMeshUriPlugin({ drive: asDrive(fake) });
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
const bytes = await resolver.read("drive+mesh://swarmA/raw/clip.mov");
|
||||
expect(bytes).toEqual(new Uint8Array([1, 2, 3]));
|
||||
expect(fake.calls.read).toHaveLength(1);
|
||||
expect(fake.calls.read[0]?.path).toBe("raw/clip.mov");
|
||||
});
|
||||
|
||||
it("surfaces swarmId on the emitted span", { timeout: 5000 }, () => {
|
||||
const span = makeRecordingSpan();
|
||||
const plugin = driveMeshUriPlugin({ drive: asDrive(fake) });
|
||||
// Patch the OTel API's getActiveSpan to return our recording span
|
||||
// for the duration of this test. Without an SDK-installed context
|
||||
// manager, `trace.setSpan(context, span)` is a no-op (the noop
|
||||
// context manager doesn't propagate); patching at the API layer is
|
||||
// the lightest-weight test seam compatible with `@opentelemetry/api`
|
||||
// alone (no `@opentelemetry/sdk-trace-base` dev dependency needed).
|
||||
const originalGetActiveSpan = trace.getActiveSpan.bind(trace);
|
||||
trace.getActiveSpan = (() =>
|
||||
span as unknown as ReturnType<
|
||||
typeof trace.getActiveSpan
|
||||
>) as typeof trace.getActiveSpan;
|
||||
try {
|
||||
plugin.parse("drive+mesh://swarmA/raw/clip.mov?peers=secret");
|
||||
} finally {
|
||||
trace.getActiveSpan = originalGetActiveSpan;
|
||||
}
|
||||
expect(span.attributes.get("drive.uri.swarm_id")).toBe("swarmA");
|
||||
expect(span.attributes.get("drive.uri.scheme")).toBe("drive+mesh");
|
||||
expect(span.attributes.get("drive.uri")).toBe(
|
||||
"drive+mesh://swarmA/raw/clip.mov"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not attempt span attribute writes when no span is active", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveMeshUriPlugin({ drive: asDrive(fake) });
|
||||
// No active span — must not throw.
|
||||
expect(() =>
|
||||
plugin.parse("drive+mesh://swarmA/raw/clip.mov")
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("build round-trips swarmId and key into a drive+mesh:// URI", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveMeshUriPlugin({ drive: asDrive(fake) });
|
||||
expect(plugin.build?.("raw/clip.mov", { swarmId: "swarmA" })).toBe(
|
||||
"drive+mesh://swarmA/raw/clip.mov"
|
||||
);
|
||||
});
|
||||
|
||||
it("build defaults swarmId to options.defaultSwarmId when meta is omitted", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveMeshUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
defaultSwarmId: "fallback",
|
||||
});
|
||||
expect(plugin.build?.("raw/clip.mov")).toBe(
|
||||
"drive+mesh://fallback/raw/clip.mov"
|
||||
);
|
||||
});
|
||||
|
||||
it("build uses 'default' when neither meta nor defaultSwarmId is provided", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveMeshUriPlugin({ drive: asDrive(fake) });
|
||||
expect(plugin.build?.("clip.mov")).toBe(
|
||||
"drive+mesh://default/clip.mov"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { trace } from "@opentelemetry/api";
|
||||
import type { Drive } from "@suaveplan/drive";
|
||||
import {
|
||||
type ParsedUri,
|
||||
parseUri,
|
||||
type UriPlugin,
|
||||
} from "@suaveplan/storage-uri";
|
||||
import {
|
||||
DRIVE_URI_CODES,
|
||||
DriveUriError,
|
||||
} from "../../error-codes/error-codes.js";
|
||||
import { redactUri } from "../../otel/otel.js";
|
||||
import type { DriveMeshUriPluginOptions } from "../../types/plugin-options.types.js";
|
||||
import { InstrumentedDriveAdapter } from "../drive/drive.streams.js";
|
||||
|
||||
const SCHEME = "drive+mesh";
|
||||
|
||||
/**
|
||||
* `drive+mesh://<swarmId>/<path>` URI plugin factory.
|
||||
*
|
||||
* Identical in shape to `driveUriPlugin` but captures the URI authority as
|
||||
* `swarmId` rather than enforcing a `driveId` match. The plugin does NOT use
|
||||
* `swarmId` to choose a `Drive` or transport — peer routing is the
|
||||
* caller-supplied `Drive`'s job. The swarmId surfaces on every emitted span
|
||||
* via the `drive.uri.swarm_id` attribute (set on the active span when one
|
||||
* exists; otherwise a new short-lived span is opened so the attribute is
|
||||
* not lost).
|
||||
*/
|
||||
export function driveMeshUriPlugin(
|
||||
options: DriveMeshUriPluginOptions
|
||||
): UriPlugin {
|
||||
if (options.drive === undefined || options.drive === null) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.CAPABILITY_MISSING,
|
||||
"driveMeshUriPlugin requires options.drive",
|
||||
{ scheme: SCHEME, reason: "drive" }
|
||||
);
|
||||
}
|
||||
const adapter = new MeshAdapter(options.drive);
|
||||
const defaultSwarmId = options.defaultSwarmId ?? "default";
|
||||
return {
|
||||
scheme: SCHEME,
|
||||
adapter,
|
||||
parse(uri: string): ParsedUri {
|
||||
const parts = parseUri(uri);
|
||||
if (parts.scheme !== SCHEME) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.INVALID_URI,
|
||||
`expected scheme '${SCHEME}', got '${parts.scheme}'`,
|
||||
{ uri, scheme: SCHEME }
|
||||
);
|
||||
}
|
||||
const swarmId = parts.authority ?? "";
|
||||
if (swarmId.length === 0) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.INVALID_URI,
|
||||
"drive+mesh:// URI is missing the swarmId authority",
|
||||
{ uri, scheme: SCHEME }
|
||||
);
|
||||
}
|
||||
const key = parts.path.replace(/^\//, "");
|
||||
if (key.length === 0) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.INVALID_URI,
|
||||
"drive+mesh:// URI must include a non-empty path component",
|
||||
{ uri, scheme: SCHEME, swarmId }
|
||||
);
|
||||
}
|
||||
adapter.recordSwarm(swarmId, uri);
|
||||
return { key, meta: { swarmId } };
|
||||
},
|
||||
build(key: string, meta?: Record<string, string>): string {
|
||||
const swarmId = meta?.["swarmId"] ?? defaultSwarmId;
|
||||
return `${SCHEME}://${swarmId}/${key.replace(/^\//, "")}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mesh-tier adapter — extends `InstrumentedDriveAdapter` and writes the
|
||||
* `drive.uri.swarm_id` and `drive.uri` attributes onto the currently active
|
||||
* span so they appear alongside the resolver's own `storage.uri.*` span.
|
||||
*
|
||||
* `recordSwarm` is invoked from the plugin's `parse` step (which the
|
||||
* resolver runs synchronously before delegating to the adapter), so by the
|
||||
* time `get`/`set`/etc. fires, the attributes are already on the active
|
||||
* span. Tests that exercise `parse(uri)` directly cover the path; tests
|
||||
* routed through `StorageUriResolver` exercise it via the dispatch flow.
|
||||
*/
|
||||
class MeshAdapter extends InstrumentedDriveAdapter {
|
||||
public constructor(drive: Drive) {
|
||||
super(drive, { scheme: SCHEME });
|
||||
}
|
||||
|
||||
public recordSwarm(swarmId: string, uri: string): void {
|
||||
const span = trace.getActiveSpan();
|
||||
if (span !== undefined) {
|
||||
span.setAttribute("drive.uri.scheme", SCHEME);
|
||||
span.setAttribute("drive.uri.swarm_id", swarmId);
|
||||
span.setAttribute("drive.uri", redactUri(uri));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { driveMeshUriPlugin } from "./drive-mesh.js";
|
||||
@@ -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.
|
||||
@@ -0,0 +1,341 @@
|
||||
import { SuaveplanError } from "@suaveplan/error";
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import { beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { asDrive, FakeDrive } from "../../../tests/setup/fake-drive.js";
|
||||
import {
|
||||
DRIVE_URI_CODES,
|
||||
DriveUriError,
|
||||
} from "../../error-codes/error-codes.js";
|
||||
import type { TemplateProvider } from "../../types/plugin-options.types.js";
|
||||
import { driveTemplateUriPlugin } from "./drive-template.js";
|
||||
|
||||
interface Calls {
|
||||
fetch: string[];
|
||||
}
|
||||
|
||||
function tracingProvider(
|
||||
calls: Calls,
|
||||
behaviour?: () => unknown
|
||||
): TemplateProvider {
|
||||
return {
|
||||
async fetchTemplate(templateId: string): Promise<never> {
|
||||
calls.fetch.push(templateId);
|
||||
const result = behaviour?.();
|
||||
if (result instanceof Error) throw result;
|
||||
return result as never;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
class FakeDtpError extends SuaveplanError<string> {}
|
||||
|
||||
describe("driveTemplateUriPlugin", () => {
|
||||
let fake: FakeDrive;
|
||||
let calls: Calls;
|
||||
|
||||
beforeEach(() => {
|
||||
fake = new FakeDrive({
|
||||
contents: new Map([
|
||||
["folder/file.bin", new Uint8Array([7])],
|
||||
["x", new Uint8Array([1])],
|
||||
["y", new Uint8Array([2])],
|
||||
]),
|
||||
});
|
||||
calls = { fetch: [] };
|
||||
});
|
||||
|
||||
it("throws CAPABILITY_MISSING when templateProvider is absent", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
try {
|
||||
driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: undefined as unknown as TemplateProvider,
|
||||
});
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
const e = err as DriveUriError;
|
||||
expect(e.code).toBe(DRIVE_URI_CODES.CAPABILITY_MISSING);
|
||||
expect(e.reason).toBe("templateProvider");
|
||||
}
|
||||
});
|
||||
|
||||
it("throws CAPABILITY_MISSING when templateProvider.fetchTemplate is not a function", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
try {
|
||||
driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: {
|
||||
fetchTemplate:
|
||||
"not a function" as unknown as TemplateProvider["fetchTemplate"],
|
||||
},
|
||||
});
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
expect((err as DriveUriError).code).toBe(
|
||||
DRIVE_URI_CODES.CAPABILITY_MISSING
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws CAPABILITY_MISSING when drive is absent", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() =>
|
||||
driveTemplateUriPlugin({
|
||||
drive: undefined as unknown as Parameters<
|
||||
typeof driveTemplateUriPlugin
|
||||
>[0]["drive"],
|
||||
templateProvider: tracingProvider(calls, () => ({})),
|
||||
})
|
||||
).toThrow(DriveUriError);
|
||||
});
|
||||
|
||||
it("rejects URIs with wrong scheme/missing authority/missing path", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: tracingProvider(calls, () => ({})),
|
||||
});
|
||||
expect(() => plugin.parse("file:///x")).toThrow(DriveUriError);
|
||||
expect(() => plugin.parse("drive+template:///x")).toThrow(
|
||||
DriveUriError
|
||||
);
|
||||
expect(() => plugin.parse("drive+template://templA/")).toThrow(
|
||||
DriveUriError
|
||||
);
|
||||
// No `//` — covers `parts.authority ?? ""` fallback branch.
|
||||
expect(() => plugin.parse("drive+template:x")).toThrow(DriveUriError);
|
||||
});
|
||||
|
||||
it("adapter.get on an un-parsed URI short-circuits hydration", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
// Construct the plugin but never call its parse() — exercise the
|
||||
// `currentTemplateId === undefined` branch in beforeOp.
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: tracingProvider(calls, () => ({})),
|
||||
});
|
||||
const adapter = plugin.adapter as {
|
||||
get(key: string): Promise<Uint8Array | undefined>;
|
||||
};
|
||||
const result = await adapter.get("x");
|
||||
expect(result).toEqual(new Uint8Array([1]));
|
||||
// Hydration must not have fired (no template id captured yet).
|
||||
expect(calls.fetch).toEqual([]);
|
||||
expect(fake.calls.hydrateFrom).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("wraps a non-Error fetchTemplate rejection without setting cause", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
// Throwing `undefined` exercises the `cause !== undefined` false
|
||||
// branch in the TEMPLATE_NOT_AVAILABLE wrapper. Biome cannot
|
||||
// statically reject a thrown non-Error variable, so we route the
|
||||
// value through an `unknown` binding instead of a suppression.
|
||||
const nonError: unknown = undefined;
|
||||
const provider: TemplateProvider = {
|
||||
async fetchTemplate(): Promise<never> {
|
||||
throw nonError;
|
||||
},
|
||||
};
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: provider,
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
try {
|
||||
await resolver.read("drive+template://templA/x");
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
const e = err as DriveUriError;
|
||||
expect(e.code).toBe(DRIVE_URI_CODES.TEMPLATE_NOT_AVAILABLE);
|
||||
expect(e.cause).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("hydrates exactly once on the first read", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: tracingProvider(calls, () => ({})),
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
const bytes = await resolver.read(
|
||||
"drive+template://templA/folder/file.bin"
|
||||
);
|
||||
expect(bytes).toEqual(new Uint8Array([7]));
|
||||
expect(calls.fetch).toEqual(["templA"]);
|
||||
expect(fake.calls.hydrateFrom).toHaveLength(1);
|
||||
expect(fake.calls.read).toHaveLength(1);
|
||||
expect(fake.calls.read[0]?.path).toBe("folder/file.bin");
|
||||
});
|
||||
|
||||
it("shares a single hydration across concurrent reads against the same templateId", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
let resolveFetch: (value: unknown) => void = () => {};
|
||||
const provider: TemplateProvider = {
|
||||
async fetchTemplate(templateId: string): Promise<never> {
|
||||
calls.fetch.push(templateId);
|
||||
return new Promise((resolve) => {
|
||||
resolveFetch = resolve;
|
||||
}) as Promise<never>;
|
||||
},
|
||||
};
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: provider,
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
const p1 = resolver.read("drive+template://templA/x");
|
||||
const p2 = resolver.read("drive+template://templA/y");
|
||||
// Both reads are queued behind the pending hydration.
|
||||
await Promise.resolve();
|
||||
expect(calls.fetch).toEqual(["templA"]);
|
||||
resolveFetch({});
|
||||
await Promise.all([p1, p2]);
|
||||
expect(calls.fetch).toEqual(["templA"]);
|
||||
expect(fake.calls.hydrateFrom).toHaveLength(1);
|
||||
expect(fake.calls.read).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("skips fetch and hydrate on subsequent reads against the same templateId", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: tracingProvider(calls, () => ({})),
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
await resolver.read("drive+template://templA/x");
|
||||
await resolver.read("drive+template://templA/y");
|
||||
expect(calls.fetch).toEqual(["templA"]);
|
||||
expect(fake.calls.hydrateFrom).toHaveLength(1);
|
||||
expect(fake.calls.read).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("wraps fetchTemplate rejection as TEMPLATE_NOT_AVAILABLE", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fetchErr = new Error("404");
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: {
|
||||
async fetchTemplate(): Promise<never> {
|
||||
throw fetchErr;
|
||||
},
|
||||
},
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
try {
|
||||
await resolver.read("drive+template://templA/x");
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
const e = err as DriveUriError;
|
||||
expect(e.code).toBe(DRIVE_URI_CODES.TEMPLATE_NOT_AVAILABLE);
|
||||
expect(e.cause).toBe(fetchErr);
|
||||
expect((e.cause as Error).message).toBe("404");
|
||||
}
|
||||
});
|
||||
|
||||
it("wraps hydrateFrom rejection as TEMPLATE_INVALID", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const hydrateErr = new FakeDtpError(
|
||||
"DTP.TEMPLATE_EXPIRED",
|
||||
"template expired"
|
||||
);
|
||||
const badDrive = new FakeDrive({ hydrateError: () => hydrateErr });
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(badDrive),
|
||||
templateProvider: tracingProvider(calls, () => ({})),
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
try {
|
||||
await resolver.read("drive+template://templA/x");
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
const e = err as DriveUriError;
|
||||
expect(e.code).toBe(DRIVE_URI_CODES.TEMPLATE_INVALID);
|
||||
expect((e.cause as SuaveplanError).code).toBe(
|
||||
"DTP.TEMPLATE_EXPIRED"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("retries hydration after a previous failure", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
let attempts = 0;
|
||||
const provider: TemplateProvider = {
|
||||
async fetchTemplate(): Promise<never> {
|
||||
attempts += 1;
|
||||
if (attempts === 1) throw new Error("first attempt fails");
|
||||
return {} as never;
|
||||
},
|
||||
};
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: provider,
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
await expect(
|
||||
resolver.read("drive+template://templA/x")
|
||||
).rejects.toBeDefined();
|
||||
const bytes = await resolver.read("drive+template://templA/x");
|
||||
expect(bytes).toEqual(new Uint8Array([1]));
|
||||
expect(attempts).toBe(2);
|
||||
});
|
||||
|
||||
it("wraps a non-Error hydrate rejection without setting cause", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const badDrive = new FakeDrive({ hydrateError: () => "literal" });
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(badDrive),
|
||||
templateProvider: tracingProvider(calls, () => ({})),
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
try {
|
||||
await resolver.read("drive+template://templA/x");
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
const e = err as DriveUriError;
|
||||
expect(e.code).toBe(DRIVE_URI_CODES.TEMPLATE_INVALID);
|
||||
expect(e.cause).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("build round-trips templateId and key into a drive+template:// URI", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: tracingProvider(calls, () => ({})),
|
||||
});
|
||||
expect(
|
||||
plugin.build?.("folder/file.bin", { templateId: "templA" })
|
||||
).toBe("drive+template://templA/folder/file.bin");
|
||||
});
|
||||
|
||||
it("build with no meta yields an empty authority", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
templateProvider: tracingProvider(calls, () => ({})),
|
||||
});
|
||||
expect(plugin.build?.("clip.mov")).toBe("drive+template:///clip.mov");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import type { Drive } from "@suaveplan/drive";
|
||||
import { SuaveplanError } from "@suaveplan/error";
|
||||
import {
|
||||
type ParsedUri,
|
||||
parseUri,
|
||||
type UriPlugin,
|
||||
} from "@suaveplan/storage-uri";
|
||||
import {
|
||||
DRIVE_URI_CODES,
|
||||
DriveUriError,
|
||||
} from "../../error-codes/error-codes.js";
|
||||
import { recordTemplateHydration } from "../../otel/otel.js";
|
||||
import type {
|
||||
DriveTemplateUriPluginOptions,
|
||||
TemplateProvider,
|
||||
} from "../../types/plugin-options.types.js";
|
||||
import { InstrumentedDriveAdapter } from "../drive/drive.streams.js";
|
||||
|
||||
const SCHEME = "drive+template";
|
||||
|
||||
/**
|
||||
* `drive+template://<templateId>/<path>` URI plugin factory.
|
||||
*
|
||||
* Before delegating any read to the wrapped `Drive`, the plugin fetches the
|
||||
* `DriveTemplate` for `templateId` via `options.templateProvider.fetchTemplate`
|
||||
* and feeds it to `drive.hydrateFrom`. The first read against a given
|
||||
* `templateId` performs both steps; concurrent reads await a single in-flight
|
||||
* hydration; subsequent reads after a successful hydration short-circuit
|
||||
* directly to `drive.read`. The cache is a per-plugin
|
||||
* `Map<string, Promise<void>>` — re-hydration after a previous *failure*
|
||||
* retries by deleting the rejected entry.
|
||||
*
|
||||
* Construction-time validates `options.templateProvider` — absence or a
|
||||
* non-function `fetchTemplate` throws `DRIVE_URI.CAPABILITY_MISSING`
|
||||
* synchronously, never deferring the failure to first use.
|
||||
*/
|
||||
export function driveTemplateUriPlugin(
|
||||
options: DriveTemplateUriPluginOptions
|
||||
): UriPlugin {
|
||||
validateOptions(options);
|
||||
const hydrated = new Map<string, Promise<void>>();
|
||||
let currentTemplateId: string | undefined;
|
||||
const adapter = new InstrumentedDriveAdapter(options.drive, {
|
||||
scheme: SCHEME,
|
||||
beforeOp: async () => {
|
||||
if (currentTemplateId === undefined) return;
|
||||
await ensureHydrated(
|
||||
options.drive,
|
||||
options.templateProvider,
|
||||
hydrated,
|
||||
currentTemplateId
|
||||
);
|
||||
},
|
||||
});
|
||||
return {
|
||||
scheme: SCHEME,
|
||||
adapter,
|
||||
parse(uri: string): ParsedUri {
|
||||
const parts = parseUri(uri);
|
||||
if (parts.scheme !== SCHEME) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.INVALID_URI,
|
||||
`expected scheme '${SCHEME}', got '${parts.scheme}'`,
|
||||
{ uri, scheme: SCHEME }
|
||||
);
|
||||
}
|
||||
const templateId = parts.authority ?? "";
|
||||
if (templateId.length === 0) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.INVALID_URI,
|
||||
"drive+template:// URI is missing the templateId authority",
|
||||
{ uri, scheme: SCHEME }
|
||||
);
|
||||
}
|
||||
const key = parts.path.replace(/^\//, "");
|
||||
if (key.length === 0) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.INVALID_URI,
|
||||
"drive+template:// URI must include a non-empty path component",
|
||||
{ uri, scheme: SCHEME, templateId }
|
||||
);
|
||||
}
|
||||
// Capture the templateId so the adapter's `beforeOp` hook
|
||||
// (which fires inside the resolver's synchronous dispatch
|
||||
// chain) sees the right value.
|
||||
currentTemplateId = templateId;
|
||||
return { key, meta: { templateId } };
|
||||
},
|
||||
build(key: string, meta?: Record<string, string>): string {
|
||||
const templateId = meta?.["templateId"] ?? "";
|
||||
return `${SCHEME}://${templateId}/${key.replace(/^\//, "")}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function validateOptions(options: DriveTemplateUriPluginOptions): void {
|
||||
if (options.drive === undefined || options.drive === null) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.CAPABILITY_MISSING,
|
||||
"driveTemplateUriPlugin requires options.drive",
|
||||
{ scheme: SCHEME, reason: "drive" }
|
||||
);
|
||||
}
|
||||
const provider = options.templateProvider as TemplateProvider | undefined;
|
||||
if (
|
||||
provider === undefined ||
|
||||
provider === null ||
|
||||
typeof provider.fetchTemplate !== "function"
|
||||
) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.CAPABILITY_MISSING,
|
||||
"driveTemplateUriPlugin requires options.templateProvider with a function-typed fetchTemplate",
|
||||
{ scheme: SCHEME, reason: "templateProvider" }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureHydrated(
|
||||
drive: Drive,
|
||||
provider: TemplateProvider,
|
||||
cache: Map<string, Promise<void>>,
|
||||
templateId: string
|
||||
): Promise<void> {
|
||||
let pending = cache.get(templateId);
|
||||
if (pending === undefined) {
|
||||
pending = hydrate(drive, provider, templateId);
|
||||
cache.set(templateId, pending);
|
||||
// Remove failed hydrations so retries are possible.
|
||||
pending.catch(() => {
|
||||
cache.delete(templateId);
|
||||
});
|
||||
}
|
||||
await pending;
|
||||
}
|
||||
|
||||
async function hydrate(
|
||||
drive: Drive,
|
||||
provider: TemplateProvider,
|
||||
templateId: string
|
||||
): Promise<void> {
|
||||
let template: Awaited<ReturnType<TemplateProvider["fetchTemplate"]>>;
|
||||
try {
|
||||
template = await provider.fetchTemplate(templateId);
|
||||
} catch (cause) {
|
||||
recordTemplateHydration("error");
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.TEMPLATE_NOT_AVAILABLE,
|
||||
`templateProvider.fetchTemplate('${templateId}') rejected`,
|
||||
{
|
||||
scheme: SCHEME,
|
||||
templateId,
|
||||
...(cause !== undefined ? { cause } : {}),
|
||||
}
|
||||
);
|
||||
}
|
||||
try {
|
||||
await drive.hydrateFrom(template);
|
||||
} catch (cause) {
|
||||
recordTemplateHydration("error");
|
||||
// `Drive.hydrateFrom` rejects with SuaveplanError carrying a DTP_*
|
||||
// (or DRIVE.*) code; preserve it on `cause` so operators can
|
||||
// distinguish the underlying failure.
|
||||
const wrapped = new DriveUriError(
|
||||
DRIVE_URI_CODES.TEMPLATE_INVALID,
|
||||
`Drive.hydrateFrom rejected for templateId '${templateId}'`,
|
||||
{
|
||||
scheme: SCHEME,
|
||||
templateId,
|
||||
...(cause instanceof SuaveplanError || cause instanceof Error
|
||||
? { cause }
|
||||
: {}),
|
||||
}
|
||||
);
|
||||
throw wrapped;
|
||||
}
|
||||
recordTemplateHydration("success");
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { driveTemplateUriPlugin } from "./drive-template.js";
|
||||
103
packages/networking/drive/drive-uri/src/plugins/drive/drive.md
Normal file
103
packages/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,284 @@
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import { beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
asDrive,
|
||||
FakeDrive,
|
||||
FakeDriveError,
|
||||
} from "../../../tests/setup/fake-drive.js";
|
||||
import { driveUriPlugin } from "./drive.js";
|
||||
import { InstrumentedDriveAdapter } from "./drive.streams.js";
|
||||
|
||||
describe("driveUriPlugin streams", () => {
|
||||
let fake: FakeDrive;
|
||||
|
||||
beforeEach(() => {
|
||||
fake = new FakeDrive({
|
||||
contents: new Map([["clip.mov", new Uint8Array([1, 2, 3, 4, 5])]]),
|
||||
});
|
||||
});
|
||||
|
||||
it("open forwards Drive.createReadStream without buffered fallback", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
const stream = await resolver.open("drive://primary/clip.mov");
|
||||
expect(fake.calls.createReadStream).toHaveLength(1);
|
||||
expect(fake.calls.read).toHaveLength(0);
|
||||
const reader = stream.getReader();
|
||||
const { value } = await reader.read();
|
||||
expect(value).toEqual(new Uint8Array([1, 2, 3, 4, 5]));
|
||||
reader.releaseLock();
|
||||
});
|
||||
|
||||
it("mid-stream abort propagates to Drive.createReadStream", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
// Fake whose createReadStream observes the signal we hand it.
|
||||
const observed: AbortSignal[] = [];
|
||||
const stub = {
|
||||
async read(): Promise<Uint8Array> {
|
||||
return new Uint8Array();
|
||||
},
|
||||
async write(): Promise<void> {},
|
||||
async delete(): Promise<boolean> {
|
||||
return false;
|
||||
},
|
||||
async stat(): Promise<unknown | null> {
|
||||
return null;
|
||||
},
|
||||
async list(): Promise<unknown> {
|
||||
return { keys: [], total: 0, hasMore: false };
|
||||
},
|
||||
createReadStream(
|
||||
_: string,
|
||||
signal?: AbortSignal
|
||||
): ReadableStream<Uint8Array> {
|
||||
if (signal !== undefined) observed.push(signal);
|
||||
return new ReadableStream<Uint8Array>({
|
||||
start(ctrl) {
|
||||
ctrl.enqueue(new Uint8Array([0xff]));
|
||||
// Stay open so the consumer can abort.
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
const plugin = driveUriPlugin({
|
||||
drive: stub as unknown as Parameters<
|
||||
typeof driveUriPlugin
|
||||
>[0]["drive"],
|
||||
driveId: "primary",
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
const controller = new AbortController();
|
||||
const stream = await resolver.open(
|
||||
"drive://primary/clip.mov",
|
||||
controller.signal
|
||||
);
|
||||
expect(observed).toHaveLength(1);
|
||||
expect(observed[0]).toBe(controller.signal);
|
||||
|
||||
const reader = stream.getReader();
|
||||
const first = await reader.read();
|
||||
expect(first.value).toEqual(new Uint8Array([0xff]));
|
||||
controller.abort();
|
||||
expect(controller.signal.aborted).toBe(true);
|
||||
reader.releaseLock();
|
||||
// The observed signal is the controller's — abort is visible there.
|
||||
expect(observed[0]?.aborted).toBe(true);
|
||||
});
|
||||
|
||||
it("records error outcome when delete throws a DriveUriError", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
// Use an adapter wired with a beforeOp hook that throws DriveUriError —
|
||||
// exercises the error branch with `err instanceof DriveUriError` true.
|
||||
const adapter = new InstrumentedDriveAdapter(asDrive(fake), {
|
||||
scheme: "drive",
|
||||
async beforeOp(op) {
|
||||
if (op === "delete") {
|
||||
const { DRIVE_URI_CODES, DriveUriError } = await import(
|
||||
"../../error-codes/error-codes.js"
|
||||
);
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.ABORTED,
|
||||
"test forced abort"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
await expect(adapter.delete("clip.mov")).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it("records error outcome when exists throws an arbitrary failure", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
// Use a drive that throws a non-not-found from stat — exercises the
|
||||
// ADAPTER_ERROR branch with `err instanceof DriveUriError` false.
|
||||
const stub = {
|
||||
async read() {
|
||||
return new Uint8Array();
|
||||
},
|
||||
async write(): Promise<void> {},
|
||||
async delete(): Promise<boolean> {
|
||||
return false;
|
||||
},
|
||||
async stat(): Promise<unknown | null> {
|
||||
throw new FakeDriveError("DRIVE.PLUGIN_FAILED", "boom");
|
||||
},
|
||||
async list(): Promise<unknown> {
|
||||
return { keys: [], total: 0, hasMore: false };
|
||||
},
|
||||
createReadStream(): ReadableStream<Uint8Array> {
|
||||
return new ReadableStream();
|
||||
},
|
||||
};
|
||||
const adapter = new InstrumentedDriveAdapter(
|
||||
stub as unknown as Parameters<typeof InstrumentedDriveAdapter>[0],
|
||||
{ scheme: "drive" }
|
||||
);
|
||||
await expect(adapter.exists("x")).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it("records not_found outcome when exists returns false", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const adapter = new InstrumentedDriveAdapter(asDrive(fake), {
|
||||
scheme: "drive",
|
||||
});
|
||||
const result = await adapter.exists("missing-key");
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("delete records success when the file exists", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const adapter = new InstrumentedDriveAdapter(asDrive(fake), {
|
||||
scheme: "drive",
|
||||
});
|
||||
const removed = await adapter.delete("clip.mov");
|
||||
expect(removed).toBe(true);
|
||||
});
|
||||
|
||||
it("delete records error with ADAPTER_ERROR code for non-DriveUri failures", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
// A drive that throws a non-DriveUriError from delete — exercises
|
||||
// the `err instanceof DriveUriError ? err.code : "ADAPTER_ERROR"`
|
||||
// false branch.
|
||||
const stub = {
|
||||
async read() {
|
||||
return new Uint8Array();
|
||||
},
|
||||
async write(): Promise<void> {},
|
||||
async delete(): Promise<boolean> {
|
||||
throw new FakeDriveError("DRIVE.IO_FAILED", "io-fail");
|
||||
},
|
||||
async stat(): Promise<unknown | null> {
|
||||
return null;
|
||||
},
|
||||
async list(): Promise<unknown> {
|
||||
return { keys: [], total: 0, hasMore: false };
|
||||
},
|
||||
createReadStream(): ReadableStream<Uint8Array> {
|
||||
return new ReadableStream();
|
||||
},
|
||||
};
|
||||
const adapter = new InstrumentedDriveAdapter(
|
||||
stub as unknown as Parameters<typeof InstrumentedDriveAdapter>[0],
|
||||
{ scheme: "drive" }
|
||||
);
|
||||
await expect(adapter.delete("clip.mov")).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it("exists records error with the DriveUriError code when beforeOp throws", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const adapter = new InstrumentedDriveAdapter(asDrive(fake), {
|
||||
scheme: "drive",
|
||||
async beforeOp(op) {
|
||||
if (op === "exists") {
|
||||
const { DRIVE_URI_CODES, DriveUriError } = await import(
|
||||
"../../error-codes/error-codes.js"
|
||||
);
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.ABORTED,
|
||||
"test forced abort on exists"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
await expect(adapter.exists("clip.mov")).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it("get records error with ADAPTER_ERROR code for non-DriveUri failures", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const stub = {
|
||||
async read(): Promise<Uint8Array> {
|
||||
throw new FakeDriveError("DRIVE.PLUGIN_FAILED", "plugin");
|
||||
},
|
||||
async write(): Promise<void> {},
|
||||
async delete(): Promise<boolean> {
|
||||
return false;
|
||||
},
|
||||
async stat(): Promise<unknown | null> {
|
||||
return null;
|
||||
},
|
||||
async list(): Promise<unknown> {
|
||||
return { keys: [], total: 0, hasMore: false };
|
||||
},
|
||||
createReadStream(): ReadableStream<Uint8Array> {
|
||||
return new ReadableStream();
|
||||
},
|
||||
};
|
||||
const adapter = new InstrumentedDriveAdapter(
|
||||
stub as unknown as Parameters<typeof InstrumentedDriveAdapter>[0],
|
||||
{ scheme: "drive" }
|
||||
);
|
||||
await expect(adapter.get("x")).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it("get records not_found outcome when the drive returns undefined", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const adapter = new InstrumentedDriveAdapter(asDrive(fake), {
|
||||
scheme: "drive",
|
||||
});
|
||||
const result = await adapter.get("absent-key");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("exists records success outcome when the file is present", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const adapter = new InstrumentedDriveAdapter(asDrive(fake), {
|
||||
scheme: "drive",
|
||||
});
|
||||
const present = await adapter.exists("clip.mov");
|
||||
expect(present).toBe(true);
|
||||
});
|
||||
|
||||
it("get records error with the DriveUriError code when beforeOp throws", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const adapter = new InstrumentedDriveAdapter(asDrive(fake), {
|
||||
scheme: "drive",
|
||||
async beforeOp(op) {
|
||||
if (op === "get") {
|
||||
const { DRIVE_URI_CODES, DriveUriError } = await import(
|
||||
"../../error-codes/error-codes.js"
|
||||
);
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.ABORTED,
|
||||
"test forced abort on get"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
await expect(adapter.get("clip.mov")).rejects.toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
import type { Drive } from "@suaveplan/drive";
|
||||
import { DriveStorageLikeAdapter } from "../../adapter/drive-storage-like.js";
|
||||
import {
|
||||
DRIVE_URI_CODES,
|
||||
DriveUriError,
|
||||
} from "../../error-codes/error-codes.js";
|
||||
import { recordOp } from "../../otel/otel.js";
|
||||
|
||||
/**
|
||||
* Adapter shape used by all three plugins. Wraps a `Drive` in an
|
||||
* OTel-instrumented `StorageLike` that:
|
||||
*
|
||||
* - records `drive_uri.op_duration_ms{op, scheme, outcome}` per operation
|
||||
* - increments the correct success/error counters via `recordOp`
|
||||
* - rejects every `set` call with `DRIVE_URI.WRITE_UNSUPPORTED` (and still
|
||||
* records the rejection so operators see misuse rate)
|
||||
* - exposes `createReadStream(key, signal)` with pre-abort short-circuit
|
||||
* so the resolver's native streaming fast path is wired without losing
|
||||
* the `DRIVE_URI.ABORTED` semantic
|
||||
*/
|
||||
export interface InstrumentedAdapterOptions {
|
||||
readonly scheme: string;
|
||||
/**
|
||||
* Optional hook the adapter calls before each operation. Used by the
|
||||
* `drive+template` plugin to ensure the Drive is hydrated before any
|
||||
* delegation reaches it.
|
||||
*/
|
||||
readonly beforeOp?: (
|
||||
op: "get" | "set" | "delete" | "exists" | "list"
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export class InstrumentedDriveAdapter extends DriveStorageLikeAdapter {
|
||||
readonly #scheme: string;
|
||||
readonly #beforeOp?: InstrumentedAdapterOptions["beforeOp"];
|
||||
readonly #drive: Drive;
|
||||
|
||||
public constructor(drive: Drive, options: InstrumentedAdapterOptions) {
|
||||
super(drive);
|
||||
this.#drive = drive;
|
||||
this.#scheme = options.scheme;
|
||||
if (options.beforeOp !== undefined) this.#beforeOp = options.beforeOp;
|
||||
}
|
||||
|
||||
public override async get(key: string): Promise<Uint8Array | undefined> {
|
||||
const start = Date.now();
|
||||
try {
|
||||
if (this.#beforeOp !== undefined) await this.#beforeOp("get");
|
||||
const bytes = await super.get(key);
|
||||
recordOp({
|
||||
op: "read",
|
||||
scheme: this.#scheme,
|
||||
outcome: bytes === undefined ? "not_found" : "success",
|
||||
durationMs: Date.now() - start,
|
||||
});
|
||||
return bytes;
|
||||
} catch (err) {
|
||||
recordOp({
|
||||
op: "read",
|
||||
scheme: this.#scheme,
|
||||
outcome: "error",
|
||||
durationMs: Date.now() - start,
|
||||
code: err instanceof DriveUriError ? err.code : "ADAPTER_ERROR",
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public override async set(key: string, _value: Uint8Array): Promise<void> {
|
||||
const start = Date.now();
|
||||
const err = new DriveUriError(
|
||||
DRIVE_URI_CODES.WRITE_UNSUPPORTED,
|
||||
"writes via drive*:// URIs are not supported in this release",
|
||||
{ scheme: this.#scheme, path: key }
|
||||
);
|
||||
recordOp({
|
||||
op: "write",
|
||||
scheme: this.#scheme,
|
||||
outcome: "error",
|
||||
durationMs: Date.now() - start,
|
||||
code: err.code,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
|
||||
public override async delete(key: string): Promise<boolean> {
|
||||
const start = Date.now();
|
||||
try {
|
||||
if (this.#beforeOp !== undefined) await this.#beforeOp("delete");
|
||||
const removed = await super.delete(key);
|
||||
recordOp({
|
||||
op: "remove",
|
||||
scheme: this.#scheme,
|
||||
outcome: "success",
|
||||
durationMs: Date.now() - start,
|
||||
});
|
||||
return removed;
|
||||
} catch (err) {
|
||||
recordOp({
|
||||
op: "remove",
|
||||
scheme: this.#scheme,
|
||||
outcome: "error",
|
||||
durationMs: Date.now() - start,
|
||||
code: err instanceof DriveUriError ? err.code : "ADAPTER_ERROR",
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public override async exists(key: string): Promise<boolean> {
|
||||
const start = Date.now();
|
||||
try {
|
||||
if (this.#beforeOp !== undefined) await this.#beforeOp("exists");
|
||||
const present = await super.exists(key);
|
||||
recordOp({
|
||||
op: "exists",
|
||||
scheme: this.#scheme,
|
||||
outcome: present ? "success" : "not_found",
|
||||
durationMs: Date.now() - start,
|
||||
});
|
||||
return present;
|
||||
} catch (err) {
|
||||
recordOp({
|
||||
op: "exists",
|
||||
scheme: this.#scheme,
|
||||
outcome: "error",
|
||||
durationMs: Date.now() - start,
|
||||
code: err instanceof DriveUriError ? err.code : "ADAPTER_ERROR",
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public override createReadStream(
|
||||
key: string,
|
||||
signal?: AbortSignal
|
||||
): ReadableStream<Uint8Array> {
|
||||
if (signal?.aborted === true) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.ABORTED,
|
||||
"signal aborted before createReadStream",
|
||||
{ scheme: this.#scheme, path: key }
|
||||
);
|
||||
}
|
||||
return this.#drive.createReadStream(key, signal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import { beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { asDrive, FakeDrive } from "../../../tests/setup/fake-drive.js";
|
||||
import {
|
||||
DRIVE_URI_CODES,
|
||||
DriveUriError,
|
||||
} from "../../error-codes/error-codes.js";
|
||||
import { driveUriPlugin } from "./drive.js";
|
||||
|
||||
describe("driveUriPlugin", () => {
|
||||
let fake: FakeDrive;
|
||||
|
||||
beforeEach(() => {
|
||||
fake = new FakeDrive({
|
||||
contents: new Map([
|
||||
["folder/file.bin", new Uint8Array([7, 8, 9])],
|
||||
["clip.mov", new Uint8Array([1, 2])],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it("validates options.driveId at construction", { timeout: 5000 }, () => {
|
||||
expect(() =>
|
||||
driveUriPlugin({ drive: asDrive(fake), driveId: "" })
|
||||
).toThrow(DriveUriError);
|
||||
});
|
||||
|
||||
it("validates options.drive at construction", { timeout: 5000 }, () => {
|
||||
expect(() =>
|
||||
driveUriPlugin({
|
||||
drive: undefined as unknown as Parameters<
|
||||
typeof driveUriPlugin
|
||||
>[0]["drive"],
|
||||
driveId: "primary",
|
||||
})
|
||||
).toThrow(DriveUriError);
|
||||
});
|
||||
|
||||
it("rejects URIs whose scheme is not drive://", { timeout: 5000 }, () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
expect(() => plugin.parse("file:///x")).toThrow(DriveUriError);
|
||||
});
|
||||
|
||||
it("rejects URIs missing the driveId authority", { timeout: 5000 }, () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
try {
|
||||
plugin.parse("drive:///x");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
expect((err as DriveUriError).code).toBe(
|
||||
DRIVE_URI_CODES.INVALID_URI
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects URIs with no authority component (no //)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
// `drive:x` has scheme + path but no authority — covers the
|
||||
// `parts.authority ?? ""` fallback branch.
|
||||
try {
|
||||
plugin.parse("drive:x");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
expect((err as DriveUriError).code).toBe(
|
||||
DRIVE_URI_CODES.INVALID_URI
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects URIs with an empty path", { timeout: 5000 }, () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
try {
|
||||
plugin.parse("drive://primary/");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
expect((err as DriveUriError).code).toBe(
|
||||
DRIVE_URI_CODES.INVALID_URI
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("dispatches a drive:// URI to Drive.read", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
const bytes = await resolver.read("drive://primary/folder/file.bin");
|
||||
expect(bytes).toEqual(new Uint8Array([7, 8, 9]));
|
||||
expect(fake.calls.read).toHaveLength(1);
|
||||
expect(fake.calls.read[0]?.path).toBe("folder/file.bin");
|
||||
});
|
||||
|
||||
it("throws DRIVE_ID_MISMATCH when URI driveId differs from configured driveId", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
try {
|
||||
await resolver.read("drive://other/x");
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
// Per fix-storage-uri-error-passthrough: SuaveplanError
|
||||
// subclasses thrown by a plugin's adapter pass through the
|
||||
// resolver unchanged, so the caller sees the DriveUriError
|
||||
// directly with its original code.
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
const e = err as DriveUriError;
|
||||
expect(e.code).toBe(DRIVE_URI_CODES.DRIVE_ID_MISMATCH);
|
||||
expect(e.uri).toBe("drive://other/x");
|
||||
expect(e.expected).toBe("primary");
|
||||
expect(e.actual).toBe("other");
|
||||
}
|
||||
});
|
||||
|
||||
it("build round-trips driveId and key into a drive:// URI", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
expect(plugin.build?.("folder/file.bin", { driveId: "primary" })).toBe(
|
||||
"drive://primary/folder/file.bin"
|
||||
);
|
||||
});
|
||||
|
||||
it("build defaults to the configured driveId when meta is omitted", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
expect(plugin.build?.("clip.mov")).toBe("drive://primary/clip.mov");
|
||||
});
|
||||
|
||||
it("build strips a leading slash from key", { timeout: 5000 }, () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
expect(plugin.build?.("/clip.mov", { driveId: "primary" })).toBe(
|
||||
"drive://primary/clip.mov"
|
||||
);
|
||||
});
|
||||
|
||||
it("short-circuits with DRIVE_URI.ABORTED on a pre-aborted signal", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
const adapter = plugin.adapter as {
|
||||
createReadStream(
|
||||
key: string,
|
||||
signal?: AbortSignal
|
||||
): ReadableStream<Uint8Array>;
|
||||
};
|
||||
const signal = AbortSignal.abort();
|
||||
try {
|
||||
adapter.createReadStream("x", signal);
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
expect((err as DriveUriError).code).toBe(DRIVE_URI_CODES.ABORTED);
|
||||
}
|
||||
// Drive.createReadStream MUST NOT have been invoked.
|
||||
expect(fake.calls.createReadStream).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("forwards a live AbortSignal unchanged to Drive.createReadStream", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
const adapter = plugin.adapter as {
|
||||
createReadStream(
|
||||
key: string,
|
||||
signal?: AbortSignal
|
||||
): ReadableStream<Uint8Array>;
|
||||
};
|
||||
const controller = new AbortController();
|
||||
adapter.createReadStream("clip.mov", controller.signal);
|
||||
expect(fake.calls.createReadStream).toHaveLength(1);
|
||||
expect(fake.calls.createReadStream[0]?.signal).toBe(controller.signal);
|
||||
});
|
||||
|
||||
it("rejects writes with DRIVE_URI.WRITE_UNSUPPORTED", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
try {
|
||||
await resolver.write(
|
||||
"drive://primary/x",
|
||||
new Uint8Array([1, 2, 3])
|
||||
);
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
// Per fix-storage-uri-error-passthrough: the DriveUriError
|
||||
// passes through the resolver's write path unchanged.
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
const e = err as DriveUriError;
|
||||
expect(e.code).toBe(DRIVE_URI_CODES.WRITE_UNSUPPORTED);
|
||||
expect(e.scheme).toBe("drive");
|
||||
}
|
||||
});
|
||||
|
||||
it("delete forwards through the adapter to Drive.delete", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const plugin = driveUriPlugin({
|
||||
drive: asDrive(fake),
|
||||
driveId: "primary",
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
const removed = await resolver.remove("drive://primary/clip.mov");
|
||||
expect(removed).toBe(true);
|
||||
expect(fake.calls.delete).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
type ParsedUri,
|
||||
parseUri,
|
||||
type UriPlugin,
|
||||
} from "@suaveplan/storage-uri";
|
||||
import {
|
||||
DRIVE_URI_CODES,
|
||||
DriveUriError,
|
||||
} from "../../error-codes/error-codes.js";
|
||||
import type { DriveUriPluginOptions } from "../../types/plugin-options.types.js";
|
||||
import { InstrumentedDriveAdapter } from "./drive.streams.js";
|
||||
|
||||
const SCHEME = "drive";
|
||||
|
||||
/**
|
||||
* `drive://<driveId>/<path>` URI plugin factory.
|
||||
*
|
||||
* Wraps the caller-supplied `Drive` in a `DriveStorageLikeAdapter` (with OTel
|
||||
* instrumentation) and returns a `UriPlugin` registerable with
|
||||
* `StorageUriResolver`. The plugin's `parse` enforces that the URI's authority
|
||||
* equals `options.driveId` — mismatches throw `DRIVE_URI.DRIVE_ID_MISMATCH`
|
||||
* synchronously, before any `Drive` call.
|
||||
*
|
||||
* The plugin is read-only. Every `adapter.set` rejects with
|
||||
* `DRIVE_URI.WRITE_UNSUPPORTED` and increments
|
||||
* `drive_uri.writes_total{scheme:"drive", outcome:"error"}` regardless of
|
||||
* cause (so operator dashboards see the misuse rate).
|
||||
*/
|
||||
export function driveUriPlugin(options: DriveUriPluginOptions): UriPlugin {
|
||||
validateOptions(options);
|
||||
const adapter = new InstrumentedDriveAdapter(options.drive, {
|
||||
scheme: SCHEME,
|
||||
});
|
||||
return {
|
||||
scheme: SCHEME,
|
||||
adapter,
|
||||
parse(uri: string): ParsedUri {
|
||||
const parts = parseUri(uri);
|
||||
if (parts.scheme !== SCHEME) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.INVALID_URI,
|
||||
`expected scheme '${SCHEME}', got '${parts.scheme}'`,
|
||||
{ uri, scheme: SCHEME }
|
||||
);
|
||||
}
|
||||
const driveId = parts.authority ?? "";
|
||||
if (driveId.length === 0) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.INVALID_URI,
|
||||
"drive:// URI is missing the driveId authority",
|
||||
{ uri, scheme: SCHEME }
|
||||
);
|
||||
}
|
||||
if (driveId !== options.driveId) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.DRIVE_ID_MISMATCH,
|
||||
`URI driveId '${driveId}' does not match plugin driveId '${options.driveId}'`,
|
||||
{
|
||||
uri,
|
||||
scheme: SCHEME,
|
||||
driveId: options.driveId,
|
||||
expected: options.driveId,
|
||||
actual: driveId,
|
||||
}
|
||||
);
|
||||
}
|
||||
const key = parts.path.replace(/^\//, "");
|
||||
if (key.length === 0) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.INVALID_URI,
|
||||
"drive:// URI must include a non-empty path component",
|
||||
{ uri, scheme: SCHEME, driveId }
|
||||
);
|
||||
}
|
||||
return { key, meta: { driveId } };
|
||||
},
|
||||
build(key: string, meta?: Record<string, string>): string {
|
||||
const driveId = meta?.["driveId"] ?? options.driveId;
|
||||
return `${SCHEME}://${driveId}/${key.replace(/^\//, "")}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function validateOptions(options: DriveUriPluginOptions): void {
|
||||
if (typeof options.driveId !== "string" || options.driveId.length === 0) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.CAPABILITY_MISSING,
|
||||
"driveUriPlugin requires a non-empty options.driveId",
|
||||
{ scheme: SCHEME, reason: "driveId" }
|
||||
);
|
||||
}
|
||||
if (options.drive === undefined || options.drive === null) {
|
||||
throw new DriveUriError(
|
||||
DRIVE_URI_CODES.CAPABILITY_MISSING,
|
||||
"driveUriPlugin requires options.drive",
|
||||
{ scheme: SCHEME, reason: "drive" }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { driveUriPlugin } from "./drive.js";
|
||||
export type { InstrumentedAdapterOptions } from "./drive.streams.js";
|
||||
export { InstrumentedDriveAdapter } from "./drive.streams.js";
|
||||
6
packages/networking/drive/drive-uri/src/types/index.ts
Normal file
6
packages/networking/drive/drive-uri/src/types/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type {
|
||||
DriveMeshUriPluginOptions,
|
||||
DriveTemplateUriPluginOptions,
|
||||
DriveUriPluginOptions,
|
||||
TemplateProvider,
|
||||
} from "./plugin-options.types.js";
|
||||
@@ -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.
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { Drive } from "@suaveplan/drive";
|
||||
import type { DriveTemplate } from "@suaveplan/drive-template";
|
||||
|
||||
/**
|
||||
* Provider injected into the `drive+template` plugin so the URI resolver can
|
||||
* obtain a signed `DriveTemplate` for a given `templateId` without being
|
||||
* coupled to a particular transport (HTTP, gossip, mesh, etc.). The provider
|
||||
* is invoked at most once per templateId per plugin instance — the hydration
|
||||
* cache short-circuits subsequent lookups.
|
||||
*/
|
||||
export interface TemplateProvider {
|
||||
fetchTemplate(
|
||||
templateId: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<DriveTemplate>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options accepted by `driveUriPlugin`. The `Drive` instance is supplied by
|
||||
* the consumer — `drive-uri` never constructs one. `driveId` is the literal
|
||||
* authority component the plugin will require in every URI; mismatches throw
|
||||
* `DRIVE_URI.DRIVE_ID_MISMATCH` synchronously from `parse`.
|
||||
*/
|
||||
export interface DriveUriPluginOptions {
|
||||
readonly drive: Drive;
|
||||
readonly driveId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options accepted by `driveMeshUriPlugin`. Unlike `DriveUriPluginOptions`,
|
||||
* the mesh variant does NOT enforce a configured swarmId — the URI's
|
||||
* authority is captured for instrumentation only and is forwarded to the
|
||||
* `Drive`'s mesh transport via the consumer's wiring. `defaultSwarmId` is
|
||||
* the value used by `build(key)` when no `meta.swarmId` is supplied.
|
||||
*/
|
||||
export interface DriveMeshUriPluginOptions {
|
||||
readonly drive: Drive;
|
||||
readonly defaultSwarmId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options accepted by `driveTemplateUriPlugin`. Both `drive` and
|
||||
* `templateProvider` are required at construction; absence of either
|
||||
* throws `DRIVE_URI.CAPABILITY_MISSING`.
|
||||
*/
|
||||
export interface DriveTemplateUriPluginOptions {
|
||||
readonly drive: Drive;
|
||||
readonly templateProvider: TemplateProvider;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { createNodeFsStorage } from "@suaveplan/storage-file";
|
||||
import {
|
||||
createStorageUriResolver,
|
||||
type UriPlugin,
|
||||
} from "@suaveplan/storage-uri";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import { driveUriPlugin } from "../../src/plugins/drive/drive.js";
|
||||
|
||||
interface FileUriOptions {
|
||||
readonly rootDir: string;
|
||||
}
|
||||
|
||||
function fileUriPlugin(options: FileUriOptions): UriPlugin {
|
||||
const adapter = createNodeFsStorage({
|
||||
rootDir: options.rootDir,
|
||||
mkdirp: true,
|
||||
});
|
||||
return {
|
||||
scheme: "file",
|
||||
adapter,
|
||||
parse(uri: string) {
|
||||
// file:///some/path -> key = "some/path"
|
||||
const idx = uri.indexOf("://");
|
||||
const after = uri.slice(idx + 3);
|
||||
const pathStart = after.indexOf("/");
|
||||
const path = pathStart >= 0 ? after.slice(pathStart + 1) : after;
|
||||
return { key: path };
|
||||
},
|
||||
build(key: string) {
|
||||
return `file:///${key.replace(/^\//, "")}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("compose-with-storage-uri integration", () => {
|
||||
let rootDir: string;
|
||||
let driveRoot: string;
|
||||
let drive: Drive;
|
||||
|
||||
beforeEach(async () => {
|
||||
rootDir = mkdtempSync(join(tmpdir(), "drive-uri-file-"));
|
||||
driveRoot = mkdtempSync(join(tmpdir(), "drive-uri-drive-"));
|
||||
const driveStorage = Object.assign(
|
||||
createNodeFsStorage({ rootDir: driveRoot, mkdirp: true }),
|
||||
{ id: "default" }
|
||||
);
|
||||
drive = new Drive(driveStorage);
|
||||
await drive.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await drive.dispose();
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
rmSync(driveRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("file:// and drive:// resolve through one StorageUriResolver", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const resolver = createStorageUriResolver([
|
||||
fileUriPlugin({ rootDir }),
|
||||
driveUriPlugin({ drive, driveId: "primary" }),
|
||||
]);
|
||||
|
||||
// Seed both backends with distinct bytes.
|
||||
await drive.write("a.bin", new Uint8Array([0xaa]));
|
||||
// Use the file plugin directly to write — the resolver rejects
|
||||
// writes for drive:// (by design) so we exercise the file side via
|
||||
// resolver.write to confirm it works.
|
||||
await resolver.write("file:///b.bin", new Uint8Array([0xbb]));
|
||||
|
||||
const fileBytes = await resolver.read("file:///b.bin");
|
||||
const driveBytes = await resolver.read("drive://primary/a.bin");
|
||||
expect(Array.from(fileBytes)).toEqual([0xbb]);
|
||||
expect(Array.from(driveBytes)).toEqual([0xaa]);
|
||||
|
||||
// Neither plugin sees the other's state.
|
||||
expect(resolver.hasScheme("file")).toBe(true);
|
||||
expect(resolver.hasScheme("drive")).toBe(true);
|
||||
expect(resolver.schemeCount).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { createNodeFsStorage } from "@suaveplan/storage-file";
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import { driveUriPlugin } from "../../src/plugins/drive/drive.js";
|
||||
|
||||
describe("drive-roundtrip integration", () => {
|
||||
let rootDir: string;
|
||||
let drive: Drive;
|
||||
|
||||
beforeEach(async () => {
|
||||
rootDir = mkdtempSync(join(tmpdir(), "drive-uri-roundtrip-"));
|
||||
const fileStorage = createNodeFsStorage({ rootDir, mkdirp: true });
|
||||
const storage = Object.assign(fileStorage, { id: "default" });
|
||||
drive = new Drive(storage);
|
||||
await drive.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await drive.dispose();
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("writes a file via Drive.write then reads it via resolver.read('drive://')", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const plugin = driveUriPlugin({ drive, driveId: "test-drive" });
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
const payload = new Uint8Array([0x4d, 0x4f, 0x56, 0x21]);
|
||||
await drive.write("folder/clip.mov", payload);
|
||||
const bytes = await resolver.read("drive://test-drive/folder/clip.mov");
|
||||
expect(Array.from(bytes)).toEqual(Array.from(payload));
|
||||
});
|
||||
|
||||
it("resolver.open streams the file via Drive.createReadStream", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const plugin = driveUriPlugin({ drive, driveId: "test-drive" });
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
const payload = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
await drive.write("clip.dat", payload);
|
||||
const stream = await resolver.open("drive://test-drive/clip.dat");
|
||||
const reader = stream.getReader();
|
||||
const chunks: number[] = [];
|
||||
for (;;) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
if (value !== undefined) chunks.push(...value);
|
||||
}
|
||||
reader.releaseLock();
|
||||
expect(chunks).toEqual(Array.from(payload));
|
||||
});
|
||||
|
||||
it("resolver.remove deletes the file", { timeout: 30000 }, async () => {
|
||||
const plugin = driveUriPlugin({ drive, driveId: "test-drive" });
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
await drive.write("doomed.bin", new Uint8Array([42]));
|
||||
const removed = await resolver.remove("drive://test-drive/doomed.bin");
|
||||
expect(removed).toBe(true);
|
||||
// A subsequent read should yield STORAGE_URI.NOT_FOUND (the
|
||||
// adapter translates Drive's not-found into undefined, and the
|
||||
// resolver maps undefined to NOT_FOUND).
|
||||
await expect(
|
||||
resolver.read("drive://test-drive/doomed.bin")
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { createNodeFsStorage } from "@suaveplan/storage-file";
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import {
|
||||
DRIVE_URI_CODES,
|
||||
DriveUriError,
|
||||
} from "../../src/error-codes/error-codes.js";
|
||||
import { driveTemplateUriPlugin } from "../../src/plugins/drive-template/drive-template.js";
|
||||
|
||||
describe("template-roundtrip integration", () => {
|
||||
let rootDir: string;
|
||||
let drive: Drive;
|
||||
|
||||
beforeEach(async () => {
|
||||
rootDir = mkdtempSync(join(tmpdir(), "drive-uri-template-"));
|
||||
const fileStorage = createNodeFsStorage({ rootDir, mkdirp: true });
|
||||
const storage = Object.assign(fileStorage, { id: "default" });
|
||||
drive = new Drive(storage);
|
||||
await drive.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await drive.dispose();
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("real Drive.hydrateFrom rejects unsigned template as TEMPLATE_INVALID", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const provider = {
|
||||
async fetchTemplate(): Promise<unknown> {
|
||||
// Returning an obviously-malformed template forces
|
||||
// Drive.hydrateFrom's validation pipeline to reject.
|
||||
return {};
|
||||
},
|
||||
};
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive,
|
||||
templateProvider: provider as unknown as Parameters<
|
||||
typeof driveTemplateUriPlugin
|
||||
>[0]["templateProvider"],
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
try {
|
||||
await resolver.read("drive+template://templA/x");
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
// SuaveplanError subclasses pass through the resolver
|
||||
// unchanged, so the caller sees the DriveUriError directly.
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
const e = err as DriveUriError;
|
||||
expect(e.code).toBe(DRIVE_URI_CODES.TEMPLATE_INVALID);
|
||||
// The inner cause is the real DTP_*/DRIVE error surfaced by
|
||||
// Drive's hydrateFrom — confirming the wrapping path is wired
|
||||
// through real package code, not a stub.
|
||||
expect(e.cause).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("provider rejection becomes TEMPLATE_NOT_AVAILABLE via real Drive", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const original = new Error("network down");
|
||||
const provider = {
|
||||
async fetchTemplate(): Promise<never> {
|
||||
throw original;
|
||||
},
|
||||
};
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive,
|
||||
templateProvider: provider as unknown as Parameters<
|
||||
typeof driveTemplateUriPlugin
|
||||
>[0]["templateProvider"],
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
try {
|
||||
await resolver.read("drive+template://templA/x");
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
const e = err as DriveUriError;
|
||||
expect(e.code).toBe(DRIVE_URI_CODES.TEMPLATE_NOT_AVAILABLE);
|
||||
expect(e.cause).toBe(original);
|
||||
}
|
||||
});
|
||||
});
|
||||
151
packages/networking/drive/drive-uri/tests/setup/fake-drive.ts
Normal file
151
packages/networking/drive/drive-uri/tests/setup/fake-drive.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import type { Drive } from "@suaveplan/drive";
|
||||
import type { DriveTemplate } from "@suaveplan/drive-template";
|
||||
import { SuaveplanError } from "@suaveplan/error";
|
||||
|
||||
/**
|
||||
* Real, hand-written `Drive`-shaped test double. NOT a mock — it is a real
|
||||
* class implementing every method consumed by `@suaveplan/drive-uri`. Used
|
||||
* for unit-tier tests where spinning up a full `Drive + storage-file +
|
||||
* chunksPlugin` would be excessive; integration tests use the real `Drive`.
|
||||
*
|
||||
* Per CLAUDE.md rule 2 this is acceptable because:
|
||||
* - the class is concrete, with deterministic behaviour
|
||||
* - no `vi.mock`/`jest.mock` is involved
|
||||
* - it implements the structural contract drive-uri consumes
|
||||
*/
|
||||
export class FakeDriveError extends SuaveplanError<string> {
|
||||
public constructor(
|
||||
code: string,
|
||||
message: string,
|
||||
options?: { cause?: unknown }
|
||||
) {
|
||||
super(code, message, options);
|
||||
this.name = "FakeDriveError";
|
||||
}
|
||||
}
|
||||
|
||||
export interface FakeDriveCalls {
|
||||
read: Array<{ path: string; options?: { signal?: AbortSignal } }>;
|
||||
write: Array<{ path: string; data: Uint8Array }>;
|
||||
delete: Array<{ path: string }>;
|
||||
stat: Array<{ path: string }>;
|
||||
list: Array<{ prefix?: string }>;
|
||||
createReadStream: Array<{ path: string; signal?: AbortSignal }>;
|
||||
hydrateFrom: Array<{
|
||||
template: DriveTemplate;
|
||||
options?: { signal?: AbortSignal };
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface FakeDriveOptions {
|
||||
readonly contents?: Map<string, Uint8Array>;
|
||||
readonly readError?: () => unknown;
|
||||
readonly hydrateError?: () => unknown;
|
||||
}
|
||||
|
||||
export class FakeDrive {
|
||||
public readonly clock = undefined;
|
||||
public readonly rng = undefined;
|
||||
public readonly logger = undefined;
|
||||
public readonly events = undefined;
|
||||
public readonly calls: FakeDriveCalls = {
|
||||
read: [],
|
||||
write: [],
|
||||
delete: [],
|
||||
stat: [],
|
||||
list: [],
|
||||
createReadStream: [],
|
||||
hydrateFrom: [],
|
||||
};
|
||||
readonly #contents: Map<string, Uint8Array>;
|
||||
readonly #options: FakeDriveOptions;
|
||||
|
||||
public constructor(options: FakeDriveOptions = {}) {
|
||||
this.#contents = options.contents ?? new Map();
|
||||
this.#options = options;
|
||||
}
|
||||
|
||||
public async read(
|
||||
path: string,
|
||||
options?: { signal?: AbortSignal }
|
||||
): Promise<Uint8Array> {
|
||||
const entry: { path: string; options?: { signal?: AbortSignal } } = {
|
||||
path,
|
||||
};
|
||||
if (options !== undefined) entry.options = options;
|
||||
this.calls.read.push(entry);
|
||||
if (this.#options.readError !== undefined) {
|
||||
throw this.#options.readError();
|
||||
}
|
||||
const bytes = this.#contents.get(path);
|
||||
if (bytes === undefined) {
|
||||
throw new FakeDriveError(
|
||||
"DRIVE.NOT_FOUND",
|
||||
`no entry for '${path}'`
|
||||
);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public async write(path: string, data: Uint8Array): Promise<void> {
|
||||
this.calls.write.push({ path, data });
|
||||
this.#contents.set(path, data);
|
||||
}
|
||||
|
||||
public async delete(path: string): Promise<boolean> {
|
||||
this.calls.delete.push({ path });
|
||||
return this.#contents.delete(path);
|
||||
}
|
||||
|
||||
public async stat(path: string): Promise<unknown | null> {
|
||||
this.calls.stat.push({ path });
|
||||
const bytes = this.#contents.get(path);
|
||||
if (bytes === undefined) return null;
|
||||
return { path, size: bytes.byteLength };
|
||||
}
|
||||
|
||||
public async list(prefix?: string): Promise<unknown> {
|
||||
this.calls.list.push({ ...(prefix !== undefined ? { prefix } : {}) });
|
||||
const keys = Array.from(this.#contents.keys()).filter((k) =>
|
||||
prefix === undefined || prefix === "" ? true : k.startsWith(prefix)
|
||||
);
|
||||
return { keys, total: keys.length, hasMore: false };
|
||||
}
|
||||
|
||||
public createReadStream(
|
||||
path: string,
|
||||
signal?: AbortSignal
|
||||
): ReadableStream<Uint8Array> {
|
||||
const entry: { path: string; signal?: AbortSignal } = { path };
|
||||
if (signal !== undefined) entry.signal = signal;
|
||||
this.calls.createReadStream.push(entry);
|
||||
const bytes = this.#contents.get(path);
|
||||
return new ReadableStream<Uint8Array>({
|
||||
start(ctrl) {
|
||||
if (bytes !== undefined) ctrl.enqueue(bytes);
|
||||
ctrl.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async hydrateFrom(
|
||||
template: DriveTemplate,
|
||||
options?: { signal?: AbortSignal }
|
||||
): Promise<{ filesAdded: number }> {
|
||||
const entry: {
|
||||
template: DriveTemplate;
|
||||
options?: { signal?: AbortSignal };
|
||||
} = { template };
|
||||
if (options !== undefined) entry.options = options;
|
||||
this.calls.hydrateFrom.push(entry);
|
||||
if (this.#options.hydrateError !== undefined) {
|
||||
throw this.#options.hydrateError();
|
||||
}
|
||||
return { filesAdded: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a `Drive`-typed reference to the fake — narrows for ergonomic use. */
|
||||
export function asDrive(fake: FakeDrive): Drive {
|
||||
return fake as unknown as Drive;
|
||||
}
|
||||
@@ -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
|
||||
// drive packages.
|
||||
export {};
|
||||
12
packages/networking/drive/drive-uri/tsconfig.json
Normal file
12
packages/networking/drive/drive-uri/tsconfig.json
Normal 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"]
|
||||
}
|
||||
12
packages/networking/drive/drive-uri/tsconfig.lib.json
Normal file
12
packages/networking/drive/drive-uri/tsconfig.lib.json
Normal 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"]
|
||||
}
|
||||
38
packages/networking/drive/drive-uri/vite.config.ts
Normal file
38
packages/networking/drive/drive-uri/vite.config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
cacheDir: "../../../node_modules/.vite/packages/drive-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/drive",
|
||||
"@suaveplan/drive-template",
|
||||
"@suaveplan/error",
|
||||
"@suaveplan/storage-uri",
|
||||
"@suaveplan/types",
|
||||
"tslib",
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
32
packages/networking/drive/drive-uri/vitest.config.ts
Normal file
32
packages/networking/drive/drive-uri/vitest.config.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
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/**/*.types.ts",
|
||||
"src/**/index.ts",
|
||||
"src/**/*.bench.ts",
|
||||
"src/__benchmarks__/**",
|
||||
"tests/**",
|
||||
],
|
||||
thresholds: {
|
||||
statements: 100,
|
||||
branches: 100,
|
||||
functions: 100,
|
||||
lines: 100,
|
||||
},
|
||||
},
|
||||
testTimeout: 30000,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user