Initial Commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user