# `@suaveplan/drive-uri/plugins/drive-template` _(no submodules)_ ## Purpose `driveTemplateUriPlugin(options)` is a factory returning a `UriPlugin` registered against the `drive+template://` scheme. It parses `drive+template:///` 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>` 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 }`. ## Implementation Notes - The hydration cache stores `Promise` 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.