first commit
This commit is contained in:
67
docs/networking/drive/drive/README.md
Normal file
67
docs/networking/drive/drive/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# @suaveplan/drive - Modular virtual filesystem core for Suaveplan. File-level operations over AbstractStorage with a plugin slot for middleware, chunking, encryption, and P2P mesh.
|
||||
|
||||
## Summary
|
||||
|
||||
Modular virtual filesystem core for Suaveplan. File-level operations over `AbstractStorage` with a plugin slot for middleware, chunking, encryption, and P2P mesh.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [drive](./src/drive/drive.md) — Core `Drive` class — a virtual filesystem over `AbstractStorage` with plugin and middleware support.
|
||||
- [drive](./src/drive/drive.md) — Core `Drive` class — a virtual filesystem over `AbstractStorage` with plugin and middleware support.
|
||||
- [file-location-index](./src/drive/file-location-index.md) — Implements `FileLocationIndex`, the in-memory map populated by `Drive.hydrateFrom` and consulted by `Drive.getKeyStatus…
|
||||
- [get-key-status](./src/drive/get-key-status.md) — Implements `Drive.getKeyStatus(path)`, the synchronous three-state lock-icon lookup (DTP-22). Returns `'unencrypted'`,…
|
||||
- [get-presence](./src/drive/get-presence.md) — Implements `Drive.getPresence(path)`, the chunk-level progress lookup (DTP-23). Returns the `ChunkPresence` shape (`chu…
|
||||
- [hydrate-from](./src/drive/hydrate-from.md) — Implements `Drive.hydrateFrom(template, options)`, the orchestrator that turns a server-signed `DriveTemplate` into a p…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Defines the structured error registry for `@suaveplan/drive`. Every failure mode the package can produce is registered…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Defines the structured error registry for `@suaveplan/drive`. Every failure mode the package can produce is registered…
|
||||
- [delete](./src/file-operations/delete.md) — Implements the deletion helper that powers `Drive.delete(path, signal?)`. Delegates to the underlying `DriveStorage.del…
|
||||
- [list](./src/file-operations/list.md) — Implements the listing helper that powers `Drive.list(prefix?, options?)`. Delegates to the underlying `DriveStorage.li…
|
||||
- [read](./src/file-operations/read.md) — Implements the byte-reading helper that powers `Drive.read(path, options?)`. Delegates to the underlying `DriveStorage.…
|
||||
- [stat](./src/file-operations/stat.md) — Implements the metadata helper that powers `Drive.stat(path, signal?)`. Delegates to the underlying `DriveStorage.getMe…
|
||||
- [streams](./src/file-operations/streams.md) — Implements `createReadStream` and `createWriteStream` for `Drive`. Each function inspects the storage's `capabilities.n…
|
||||
- [write](./src/file-operations/write.md) — Implements the byte-writing helper that powers `Drive.write(path, data, options?)`. Delegates to the underlying `DriveS…
|
||||
- [plugins](./src/plugins/registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [registry](./src/plugins/registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [schemas](./src/schemas/drive-options.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [drive-options.schema](./src/schemas/drive-options.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs --> - [registry](./src/plugins/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/drive-options.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-options.schema](./src/schemas/drive-options.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the structured error registry for `@suaveplan/drive`. Every failure mode the package can produce is registered…
|
||||
- [registry](./src/plugins/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/drive-options.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-options.schema](./src/schemas/drive-options.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the structured error registry for `@suaveplan/drive`. Every failure mode the package can produce is registered…
|
||||
- [registry](./src/plugins/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/drive-options.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-options.schema](./src/schemas/drive-options.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the structured error registry for `@suaveplan/drive`. Every failure mode the package can produce is registered…
|
||||
- [registry](./src/plugins/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/drive-options.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ - [drive-options.schema](./src/schemas/drive-options.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the structured error registry for `@suaveplan/drive`. Every failure mode the package can produce is registered…
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/drive
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { MemoryStorage } from "@suaveplan/storage/memory";
|
||||
|
||||
const storage = Object.assign(new MemoryStorage(), { id: "local" });
|
||||
const drive = new Drive(storage);
|
||||
await drive.initialize();
|
||||
await drive.write("hello.txt", new TextEncoder().encode("world"));
|
||||
const data = await drive.read("hello.txt");
|
||||
console.log(new TextDecoder().decode(data)); // "world"
|
||||
await drive.dispose();
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [src/drive/drive.md](src/drive/drive.md) — `Drive` class, lifecycle, and file operations
|
||||
- [src/error-codes.md](src/error-codes.md) — `DriveError` and `DRIVE_CODES` registry
|
||||
- [src/types/drive.types.md](src/types/drive.types.md) — public TypeScript interfaces
|
||||
- [src/schemas/drive-options.schema.md](src/schemas/drive-options.schema.md) — Zod boundary schemas
|
||||
- [src/file-operations/read.md](src/file-operations/read.md) — read operation
|
||||
- [src/file-operations/write.md](src/file-operations/write.md) — write operation
|
||||
- [src/file-operations/delete.md](src/file-operations/delete.md) — delete operation
|
||||
- [src/file-operations/stat.md](src/file-operations/stat.md) — metadata retrieval
|
||||
- [src/file-operations/list.md](src/file-operations/list.md) — list operation
|
||||
- [src/file-operations/streams.md](src/file-operations/streams.md) — stream creation
|
||||
- [src/plugins/registry.md](src/plugins/registry.md) — plugin and middleware registry
|
||||
|
||||
115
docs/networking/drive/drive/src/drive/drive.md
Normal file
115
docs/networking/drive/drive/src/drive/drive.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# drive.ts
|
||||
|
||||
Core `Drive` class — a virtual filesystem over `AbstractStorage` with plugin and middleware support.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Drive exposes POSIX-like file operations (`read`, `write`, `delete`, `stat`, `list`, `createReadStream`, `createWriteStream`) backed by one or more `DriveStorage` instances. It is deliberately file-level only; chunking, encryption, P2P mesh, and transports live in separate plugin packages that consume the `DrivePlugin` interface.
|
||||
|
||||
## Features
|
||||
|
||||
- Four constructor overloads: `()`, `(storage)`, `(storage, options)`, `(options)`
|
||||
- Generic storage map: `drive.use(id, storage)` narrows `TStorageMap`
|
||||
- Lifecycle ownership tracking: `initialize()` connects only unconnected storages; `dispose()` closes only those it opened
|
||||
- Middleware chain: outer-first on request, outer-last on response
|
||||
- Plugin slot: `drive.usePlugin(plugin)` returns a typed handle
|
||||
|
||||
## API Reference
|
||||
|
||||
### `Drive<TStorageMap>`
|
||||
|
||||
#### Constructor
|
||||
|
||||
```ts
|
||||
new Drive() // empty
|
||||
new Drive(storage) // bare storage, id "default"
|
||||
new Drive(storage, options) // storage + options
|
||||
new Drive(options) // options only
|
||||
```
|
||||
|
||||
#### `initialize(): Promise<void>`
|
||||
|
||||
Idempotent. Connects storages that are not yet connected and records them in `connectedByMe`. Throws `DRIVE.STORAGE_CONNECT_FAILED` on connection errors.
|
||||
|
||||
#### `dispose(): Promise<void>`
|
||||
|
||||
Idempotent. Disposes plugins in reverse registration order, then closes only storages in `connectedByMe`.
|
||||
|
||||
#### `use<K, S>(id: K, storage: S): Drive<TStorageMap & Record<K, S>>`
|
||||
|
||||
Registers a storage and returns a narrowed drive instance.
|
||||
|
||||
#### `getStorage<K>(id: K): TStorageMap[K]`
|
||||
|
||||
Typed lookup. Throws `DRIVE.STORAGE_NOT_FOUND` if absent.
|
||||
|
||||
#### `usePlugin<THandle>(plugin: DrivePlugin<THandle>): Promise<THandle>`
|
||||
|
||||
Installs a plugin and returns its handle.
|
||||
|
||||
#### `useMiddleware(middleware: DriveMiddleware): void`
|
||||
|
||||
Registers middleware on the drive's operation chain.
|
||||
|
||||
#### File operations
|
||||
|
||||
- `read(path, options?): Promise<Uint8Array>`
|
||||
- `write(path, data, options?): Promise<void>`
|
||||
- `delete(path, signal?): Promise<boolean>`
|
||||
- `stat(path, signal?): Promise<unknown | null>`
|
||||
- `list(prefix?, options?): Promise<unknown>`
|
||||
- `createReadStream(path, signal?): ReadableStream<Uint8Array>`
|
||||
- `createWriteStream(path, signal?): WritableStream<Uint8Array>`
|
||||
|
||||
#### `static fromConfig(config): Drive`
|
||||
|
||||
Resolves storage via `StorageFactory` from `@suaveplan/storage`.
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { MemoryStorage } from "@suaveplan/storage/memory";
|
||||
|
||||
const storage = Object.assign(new MemoryStorage(), { id: "local" });
|
||||
const drive = new Drive(storage);
|
||||
await drive.initialize();
|
||||
await drive.write("hello.txt", new TextEncoder().encode("world"));
|
||||
const bytes = await drive.read("hello.txt");
|
||||
console.log(new TextDecoder().decode(bytes)); // "world"
|
||||
await drive.dispose();
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
```ts
|
||||
const drive = new Drive()
|
||||
.use("local", localStorage)
|
||||
.use("remote", remoteStorage);
|
||||
|
||||
await drive.initialize();
|
||||
|
||||
// Middleware: log every read
|
||||
let intercepted = false;
|
||||
drive.useMiddleware({
|
||||
read: async (ctx, next) => {
|
||||
intercepted = true;
|
||||
return next();
|
||||
},
|
||||
});
|
||||
|
||||
await drive.read("file.txt");
|
||||
console.log(intercepted); // true
|
||||
await drive.dispose();
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `#resolveStorage` currently returns the default storage or the first registered one. Future versions may support path-based routing.
|
||||
- `Drive` does not maintain a separate storage adapter registry; it relies on `StorageFactory` for config-based construction.
|
||||
- Every operation validates drive state (`fresh` → `ready` → `disposed`) before proceeding.
|
||||
- Path validation rejects empty strings, absolute paths (`/...`), and path-traversal segments (`..`) with `DRIVE.INVALID_PATH`.
|
||||
58
docs/networking/drive/drive/src/drive/file-location-index.md
Normal file
58
docs/networking/drive/drive/src/drive/file-location-index.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# file-location-index
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements `FileLocationIndex`, the in-memory map populated by `Drive.hydrateFrom` and consulted by `Drive.getKeyStatus`, `Drive.getPresence`, and the `list()` enrichment path. Each entry tracks the path, file id, manifest key, total chunks, encryption mode, and the runtime "are we currently holding the key / how many chunks have we materialised" state.
|
||||
|
||||
## Features
|
||||
|
||||
- Dual-key lookup: `getByPath(path)` for filesystem operations, `getByFileId(fileId)` for bus-borne `key:grant` envelopes that carry the file id rather than the path.
|
||||
- `set(entry)` writes both indexes in one call; insertion order preserved for `[Symbol.iterator]`.
|
||||
- `deleteByPath(path)` returns `boolean` so the rollback path in `hydrate-from.ts` can short-circuit when the entry was never added.
|
||||
- `clear()` empties both indexes — used by tests and (eventually) by `Drive.dispose()` for fast teardown.
|
||||
- `emptyPresence()` returns the frozen empty `ChunkPresence` shape so freshly-hydrated entries don't allocate a per-file presence object until the chunks plugin actually delivers bytes.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { FileLocationIndex, emptyPresence } from "@suaveplan/drive";
|
||||
|
||||
const index = new FileLocationIndex();
|
||||
index.set({
|
||||
path: "photos/wedding.jpg",
|
||||
fileId: "abc123",
|
||||
manifestKey: "manifest:abc123",
|
||||
totalChunks: 1,
|
||||
chunkSize: 1024,
|
||||
size: 100,
|
||||
encryptionConfigured: false,
|
||||
keyStatus: "unencrypted",
|
||||
presence: emptyPresence(),
|
||||
autoDownload: false,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Drive, FileLocationIndex } from "@suaveplan/drive";
|
||||
|
||||
function snapshotIndex(drive: Drive) {
|
||||
return Array.from(drive.fileLocationIndex, (e) => ({
|
||||
path: e.path,
|
||||
fileId: e.fileId,
|
||||
keyStatus: e.keyStatus,
|
||||
progress: `${e.presence.chunksPresent}/${e.presence.chunksTotal}`,
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class FileLocationIndex` — `set` / `getByPath` / `getByFileId` / `hasPath` / `deleteByPath` / `clear` / `size` / `[Symbol.iterator]`.
|
||||
- `interface FileLocationEntry` — `{ path, fileId, manifestKey, totalChunks, chunkSize, size, encryptionConfigured, keyStatus, presence, autoDownload }`.
|
||||
- `emptyPresence(): ChunkPresence` — the frozen zero-progress sentinel.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The index is intentionally separate from the chunk-level metadata owned by `@suaveplan/drive-plugin-chunks`: drive proper holds the file-shaped projection so it can answer the "lock icon" / "progress bar" queries even before the chunks plugin has loaded a manifest. The `keyStatus` and `presence` fields are mutable so the bus listener (`hydrate-from.ts`) and the chunks plugin can update them in place without forcing the index to issue a fresh entry per change. `emptyPresence()` returns a `Object.freeze`'d object so a stray `Object.assign` on the result throws in strict mode rather than silently mutating the shared sentinel.
|
||||
53
docs/networking/drive/drive/src/drive/get-key-status.md
Normal file
53
docs/networking/drive/drive/src/drive/get-key-status.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# get-key-status
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements `Drive.getKeyStatus(path)`, the synchronous three-state lock-icon lookup (DTP-22). Returns `'unencrypted'`, `'encrypted-locked'`, or `'encrypted-unlocked'` for a hydrated path. Throws `DriveError(DRIVE.NOT_FOUND)` for paths not in the `FileLocationIndex` — the spec is explicit that "not found" is NOT a valid status return.
|
||||
|
||||
## Features
|
||||
|
||||
- Pure synchronous lookup against the `FileLocationIndex`.
|
||||
- Distinct error code (`DRIVE.NOT_FOUND`) rather than returning a fourth status, so UIs can render "file does not exist" differently from "file exists but the key is locked".
|
||||
- The returned `KeyStatus` value matches what `Drive.list()` enriches `DirEntry.keyStatus` with at the same instant.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
const status = drive.getKeyStatus("photos/wedding.jpg");
|
||||
console.log(status); // "encrypted-locked"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
DRIVE_CODES,
|
||||
DriveError,
|
||||
Drive,
|
||||
} from "@suaveplan/drive";
|
||||
|
||||
function lockIconFor(drive: Drive, path: string): "🔒" | "🔓" | "📄" | "❓" {
|
||||
try {
|
||||
const s = drive.getKeyStatus(path);
|
||||
if (s === "unencrypted") return "📄";
|
||||
if (s === "encrypted-locked") return "🔒";
|
||||
return "🔓";
|
||||
} catch (err) {
|
||||
if (err instanceof DriveError && err.code === DRIVE_CODES.NOT_FOUND) {
|
||||
return "❓";
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `getKeyStatus(index: FileLocationIndex, path: string): KeyStatus` — bare function; `Drive.getKeyStatus` is the consumer-facing entry.
|
||||
- Throws `DriveError(DRIVE.NOT_FOUND)` for unknown paths.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The synchronous contract is load-bearing: the lock-icon lookup happens at every render cycle, and any async overhead would hammer the React reconciler. The function is a single `Map.get` plus a status-field read — all `O(1)`. Encrypted-unlocked transitions happen via the bus listener registered in `Drive.hydrateFrom`; `get-key-status` itself never mutates the entry. The path-validation rules from `Drive.read` apply here too — empty paths, `..` traversal, and absolute paths are rejected with `DRIVE.INVALID_PATH` before the index is even consulted.
|
||||
56
docs/networking/drive/drive/src/drive/get-presence.md
Normal file
56
docs/networking/drive/drive/src/drive/get-presence.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# get-presence
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements `Drive.getPresence(path)`, the chunk-level progress lookup (DTP-23). Returns the `ChunkPresence` shape (`chunksPresent` / `chunksTotal` / `sourceHints` / `stuckChunks`) for a hydrated path. Throws `DriveError(DRIVE.NOT_FOUND)` for paths not in the `FileLocationIndex`.
|
||||
|
||||
## Features
|
||||
|
||||
- Pure synchronous lookup against the `FileLocationIndex`.
|
||||
- Mirrors the freshness contract from `Drive.list` — the value is at most 1 s stale (the chunks plugin updates the entry as bytes arrive).
|
||||
- `stuckChunks` reflects chunks that failed retrieval from every configured source within the last 60 s; resets on retry-success or peer-population.
|
||||
- `sourceHints` lists which transports actually delivered bytes for this file (`'local' | 'mesh' | 'cdn'`).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
const p = drive.getPresence("photos/wedding.jpg");
|
||||
console.log(`${p.chunksPresent}/${p.chunksTotal} chunks`);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
DRIVE_CODES,
|
||||
DriveError,
|
||||
Drive,
|
||||
} from "@suaveplan/drive";
|
||||
|
||||
function progressBar(drive: Drive, path: string): string | null {
|
||||
try {
|
||||
const p = drive.getPresence(path);
|
||||
if (p.chunksTotal === 0) return null;
|
||||
const pct = Math.round(100 * p.chunksPresent / p.chunksTotal);
|
||||
return `${"█".repeat(pct / 5)}${" ".repeat(20 - pct / 5)} ${pct}%${
|
||||
p.stuckChunks > 0 ? ` (${p.stuckChunks} stuck)` : ""
|
||||
}`;
|
||||
} catch (err) {
|
||||
if (err instanceof DriveError && err.code === DRIVE_CODES.NOT_FOUND) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `getPresence(index: FileLocationIndex, path: string): ChunkPresence` — bare function; `Drive.getPresence` is the consumer-facing entry.
|
||||
- Throws `DriveError(DRIVE.NOT_FOUND)` for unknown paths.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function is a single `Map.get` plus a presence-field read — all `O(1)`. The `ChunkPresence` value is `readonly` so consumers can't accidentally mutate it; the chunks plugin owns the per-chunk arrival path and writes through `entry.presence = { ... }` on each commit. Freshness is at most 1 s; consumers polling more often will see the same value back-to-back. Path validation runs before the index consultation so the lookup short-circuits with `DRIVE.INVALID_PATH` for empty / traversal / absolute paths.
|
||||
46
docs/networking/drive/drive/src/drive/hydrate-from.md
Normal file
46
docs/networking/drive/drive/src/drive/hydrate-from.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# hydrate-from
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements `Drive.hydrateFrom(template, options)`, the orchestrator that turns a server-signed `DriveTemplate` into a populated `FileLocationIndex` and (optionally) a queue of byte fetches. Validates the template through the 8-step pipeline in `@suaveplan/drive-template`, projects the recursive folder/file tree into one entry per file, and registers the bus listener that lets retroactive `key:grant` envelopes flip per-file lock icons from `'encrypted-locked'` to `'encrypted-unlocked'`.
|
||||
|
||||
## Features
|
||||
|
||||
- Full DTP-05 validation pipeline reused from `@suaveplan/drive-template` — schema-version, expiry, issuance window, replay LRU, trusted issuer, server signature, folder depth, per-file invariants.
|
||||
- Path collection from the template's recursive folder tree with `/`-prefixed root collapse so the resulting paths match `Drive.read` expectations.
|
||||
- `AbortSignal` support: the loop yields between iterations so a caller-side `queueMicrotask`-scheduled abort can land mid-iteration; `rollback` undoes any entries already added before re-throwing `DRIVE.HYDRATE_ABORTED`.
|
||||
- Three `preFetch` modes: `'none'` populates the index only; `'metadata-only'` records manifest keys without scheduling fetches; `'auto-download-files'` schedules byte fetches for files where `flags.autoDownload === true`.
|
||||
- Bus-listener registration via the injected `EventBusLike` — gracefully no-ops when either the bus or the local `ourPubkey` is absent.
|
||||
- Progress events fire at `validating` / `indexing` / `scheduling` / `complete` stages.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
await drive.hydrateFrom(template);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
const ctrl = new AbortController();
|
||||
const result = await drive.hydrateFrom(template, {
|
||||
signal: ctrl.signal,
|
||||
preFetch: "auto-download-files",
|
||||
onProgress: (event) => console.log(event.stage, event.filesAdded),
|
||||
});
|
||||
console.log(`hydrated ${result.filesAdded}, ${result.bytesScheduled} bytes scheduled`);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `hydrateFrom(template, options, ctx): Promise<HydrateResult>` — bare function consumed by `Drive.hydrateFrom`. Throws `DriveError(DRIVE.ALREADY_HYDRATED)` for re-hydration of the same path; `DriveError(DRIVE.HYDRATE_ABORTED)` for cancelled hydrations; otherwise propagates the underlying `DriveTemplateError` from the validation pipeline.
|
||||
- `KEY_GRANT_TOPIC` — the bus topic the listener subscribes to.
|
||||
- `interface HydrateContext` — `{ index, nonceLru, clock, events?, ourPubkey? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `await Promise.resolve()` between loop iterations is intentional. Without it, the loop runs in a single tick and abort signals scheduled via `queueMicrotask` only land after the loop completes — the rollback path becomes unreachable and the spec's "no partial state" guarantee silently degrades to "no partial state if abort fires before validation". The microtask yield costs ~10 µs per iteration on commodity hardware and gives the abort signal a chance to actually do its job.
|
||||
59
docs/networking/drive/drive/src/error-codes/error-codes.md
Normal file
59
docs/networking/drive/drive/src/error-codes/error-codes.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# error-codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the structured error registry for `@suaveplan/drive`. Every failure mode the package can produce is registered through `defineCodes("DRIVE", [...])` so downstream consumers can branch programmatically on stable string codes (`"DRIVE.NOT_INITIALIZED"`, etc.) without parsing English-language error messages. The companion `DriveError` class extends the project-wide `SuaveplanError` base so drive errors compose with cause-chain traversal, redaction, and OTel span-event adapters defined in `@suaveplan/error`.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `DRIVE_CODES` registry covering the eleven failure classes that can occur during drive lifecycle, file operations, plugin management, and template hydration.
|
||||
- `DriveErrorCode` literal union derived from `DRIVE_CODES` so call sites stay type-safe.
|
||||
- `DriveError` class with the `SuaveplanError` constructor signature widened to public, accepting `(code, message, options?)` plus the standard `cause` / `context` fields.
|
||||
- Side-effect-safe registration via `defineCodes` so subpath consumers automatically see codes registered too.
|
||||
- Hydration-surface codes (`NOT_FOUND`, `UNAVAILABLE`, `ALREADY_HYDRATED`, `HYDRATE_ABORTED`) shipped alongside the lifecycle / IO codes from the original drive surface.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { DRIVE_CODES, DriveError } from "@suaveplan/drive";
|
||||
|
||||
throw new DriveError(DRIVE_CODES.NOT_INITIALIZED, "Drive not initialized");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { DRIVE_CODES, DriveError } from "@suaveplan/drive";
|
||||
|
||||
try {
|
||||
await drive.read("missing.txt");
|
||||
} catch (err) {
|
||||
if (err instanceof DriveError) {
|
||||
switch (err.code) {
|
||||
case DRIVE_CODES.NOT_INITIALIZED:
|
||||
return await initAndRetry();
|
||||
case DRIVE_CODES.STORAGE_NOT_FOUND:
|
||||
return await fallbackToRemote();
|
||||
case DRIVE_CODES.INVALID_PATH:
|
||||
return reportBadPath(err);
|
||||
default:
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `DRIVE_CODES` — `Readonly<Record<string, string>>` of `"DRIVE.<NAME>"` strings. Lifecycle: `NOT_INITIALIZED`, `DISPOSED`. Storage: `STORAGE_CONNECT_FAILED`, `STORAGE_NOT_FOUND`. I/O: `IO_FAILED`, `INVALID_PATH`. Plugins: `PLUGIN_FAILED`. Hydration: `NOT_FOUND`, `UNAVAILABLE`, `ALREADY_HYDRATED`, `HYDRATE_ABORTED`.
|
||||
- `DriveErrorCode` — string-literal union of every value in `DRIVE_CODES`.
|
||||
- `DriveError` — `class DriveError extends SuaveplanError<DriveErrorCode>` with constructor `(code, message, options?)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The hydration codes shipped with `drive-template-drive-surface` and are namespaced under `DRIVE.` rather than `DTP.` because the failure happens inside `Drive.hydrateFrom`. The template-package errors (`DTP.TEMPLATE_*`) flow through unchanged when the validation pipeline rejects — drive doesn't re-wrap them, so consumers can still differentiate between drive-side and template-side failures by inspecting the `code` prefix.
|
||||
44
docs/networking/drive/drive/src/file-operations/delete.md
Normal file
44
docs/networking/drive/drive/src/file-operations/delete.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# delete
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements the deletion helper that powers `Drive.delete(path, signal?)`. Delegates to the underlying `DriveStorage.delete(path, signal)` and reports whether the key actually existed. The drive layer's only job is to thread the path and abort signal through the middleware chain in the right order so plugins can clean up side-band state (chunk records, manifest entries, FileLocationIndex projections) before the storage entry disappears.
|
||||
|
||||
## Features
|
||||
|
||||
- Direct delegation to `storage.delete(path, signal)` — no extra round-trips at the drive level.
|
||||
- Composable with the drive middleware chain so plugins can intercept the delete (chunk-cleanup, FileLocationIndex eviction, audit logging).
|
||||
- Honours the `AbortSignal` so callers can cancel slow deletes (e.g. against networked storage adapters).
|
||||
- Returns `true` if the key existed and was removed; `false` if the key didn't exist — neither case throws.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
const removed = await drive.delete("photos/wedding.jpg");
|
||||
console.log(removed ? "gone" : "wasn't there");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
async function purgeFolder(drive: Drive, prefix: string): Promise<number> {
|
||||
const list = (await drive.list(prefix)) as { keys: string[] };
|
||||
let removed = 0;
|
||||
for (const key of list.keys) {
|
||||
if (await drive.delete(key)) removed++;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `deleteFile(storage: DriveStorage, path: string, signal?: AbortSignal): Promise<boolean>` — bare helper. Returns `true` on actual removal, `false` when the key was absent.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function is intentionally minimal — it exists so the drive's middleware chain has a stable handler to wrap rather than calling through to storage directly. The chunks plugin's delete-side hook iterates the file's chunk inventory and removes each one before this handler runs; in that case the original-path delete is still called so the manifest record disappears too. Returning `boolean` rather than throwing on missing-key keeps `purgeFolder`-style helpers idempotent without try/catch overhead.
|
||||
39
docs/networking/drive/drive/src/file-operations/list.md
Normal file
39
docs/networking/drive/drive/src/file-operations/list.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# list
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements the listing helper that powers `Drive.list(prefix?, options?)`. Delegates to the underlying `DriveStorage.list({ prefix }, signal)` so each storage adapter (memory, S3, OPFS, etc.) controls how it enumerates keys; the drive layer's only job is to thread the prefix and abort signal through the middleware chain in the right order.
|
||||
|
||||
## Features
|
||||
|
||||
- Delegates directly to `storage.list({ prefix }, signal)` — no extra round-trips, no filtering.
|
||||
- Composable with the drive middleware chain so plugins can intercept the listing (e.g. inject `keyStatus` / `presence` enrichment from the hydration index).
|
||||
- Honours the `AbortSignal` so callers can cancel pagination mid-scan.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
const result = await drive.list("photos/");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
const ctrl = new AbortController();
|
||||
setTimeout(() => ctrl.abort(), 5_000);
|
||||
const result = await drive.list("photos/2026/", { signal: ctrl.signal });
|
||||
// Result is the underlying storage's list shape — typically
|
||||
// `{ keys: string[]; nextCursor?: string }`.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `listFiles(storage: DriveStorage, prefix?: string, signal?: AbortSignal): Promise<unknown>` — bare helper. The return type is `unknown` because each storage adapter publishes its own `ListResult` shape; consumers narrow at the call site.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function is intentionally minimal — it exists so the drive's middleware chain has a stable handler to wrap rather than calling through to storage directly. Future work (`drive-template-drive-surface` follow-up) will enrich the result with `keyStatus` / `presence` projections from the `FileLocationIndex` so consumers can render lock icons + progress bars in the same shape they already use. Pagination cursors flow through unchanged so adapters that paginate (S3, Azure Blob) keep their cursor semantics intact.
|
||||
47
docs/networking/drive/drive/src/file-operations/read.md
Normal file
47
docs/networking/drive/drive/src/file-operations/read.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# read
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements the byte-reading helper that powers `Drive.read(path, options?)`. Delegates to the underlying `DriveStorage.get(path, signal)` and returns the raw byte buffer; downstream plugins (chunks, encryption) intercept via the middleware chain to reassemble or decrypt as needed. The drive layer's only job is to thread the path and abort signal through the middleware chain in the right order.
|
||||
|
||||
## Features
|
||||
|
||||
- Direct delegation to `storage.get(path, signal)` — no copy, no transformation at the drive level.
|
||||
- Composable with the drive middleware chain so plugins can intercept the read (chunk reassembly, AEAD decryption, response caching).
|
||||
- Honours the `AbortSignal` so callers can cancel slow reads mid-flight.
|
||||
- Throws `DriveError(DRIVE.IO_FAILED)` when the signal is already aborted or the key doesn't exist in storage.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
const bytes = await drive.read("photos/wedding.jpg");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
const ctrl = new AbortController();
|
||||
setTimeout(() => ctrl.abort(), 5_000);
|
||||
try {
|
||||
const bytes = await drive.read("video.mp4", { signal: ctrl.signal });
|
||||
console.log(`read ${bytes.byteLength} bytes`);
|
||||
} catch (err) {
|
||||
if ((err as Error).name === "AbortError") {
|
||||
console.warn("read timed out");
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `readFile(storage: DriveStorage, path: string, signal?: AbortSignal): Promise<Uint8Array>` — bare helper. Returns the raw bytes from the storage adapter.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function is intentionally minimal — it exists so the drive's middleware chain has a stable handler to wrap rather than calling through to storage directly. The chunks plugin's read middleware intercepts BEFORE this handler runs and returns a reassembled plaintext buffer; in that case `readFile` is never reached. Callers who want raw chunk bytes (e.g. mesh forwarders) bypass the middleware chain by calling `drive.getStorage(id).get(path)` directly.
|
||||
47
docs/networking/drive/drive/src/file-operations/stat.md
Normal file
47
docs/networking/drive/drive/src/file-operations/stat.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# stat
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements the metadata helper that powers `Drive.stat(path, signal?)`. Delegates to the underlying `DriveStorage.getMetadata(path, signal)` and returns the storage-shaped metadata record (size, content type, ETag, custom headers, etc.) or `null` when the key does not exist. The drive layer's only job is to thread the path and abort signal through the middleware chain so plugins can enrich the result before the caller sees it.
|
||||
|
||||
## Features
|
||||
|
||||
- Direct delegation to `storage.getMetadata(path, signal)` — no copy, no transformation at the drive level.
|
||||
- Composable with the drive middleware chain so plugins can intercept the stat (chunk-aware size reporting, encrypted-field decryption, content-type detection).
|
||||
- Honours the `AbortSignal` so callers can cancel slow stat calls.
|
||||
- Returns `null` when the key does not exist; this lets `if (await drive.stat(path))` work as a presence check without try/catch.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
const meta = await drive.stat("photos/wedding.jpg");
|
||||
if (meta) console.log(meta);
|
||||
else console.log("not found");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
async function isLargerThan(
|
||||
drive: Drive,
|
||||
path: string,
|
||||
bytes: number
|
||||
): Promise<boolean> {
|
||||
const meta = (await drive.stat(path)) as
|
||||
| { size?: number }
|
||||
| null;
|
||||
return meta?.size !== undefined && meta.size > bytes;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `statFile(storage: DriveStorage, path: string, signal?: AbortSignal): Promise<unknown | null>` — bare helper. The return type is `unknown` because each storage adapter publishes its own `Metadata` shape; consumers narrow at the call site.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function is intentionally minimal — it exists so the drive's middleware chain has a stable handler to wrap rather than calling through to storage directly. The chunks plugin's stat middleware intercepts to report the plaintext size from the manifest rather than the chunk-bytes size from the storage; in that case `statFile` is never reached. Callers who want the raw chunk-store metadata bypass the middleware chain by calling `drive.getStorage(id).getMetadata(path)` directly.
|
||||
52
docs/networking/drive/drive/src/file-operations/streams.md
Normal file
52
docs/networking/drive/drive/src/file-operations/streams.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# streams
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements `createReadStream` and `createWriteStream` for `Drive`. Each function inspects the storage's `capabilities.nativeStreams` flag and either delegates to the storage's own stream implementation (zero-copy on adapters that support it) or falls back to a one-chunk-at-a-time buffered implementation that uses the storage's `get` / `set` primitives. The fallback is correct on every storage but pays a memory cost equal to the file size for large files; consumers should prefer storages with `nativeStreams: true` for streaming large objects.
|
||||
|
||||
## Features
|
||||
|
||||
- Capability-aware dispatch — uses native streams when the storage advertises them, falls back gracefully otherwise.
|
||||
- `AbortSignal` propagation through both native and fallback paths.
|
||||
- Read fallback handles missing keys gracefully (closes the stream without enqueuing rather than throwing).
|
||||
- Write fallback clears its chunk buffer on abort so partial writes don't leak.
|
||||
- Fallback implementations never materialise more than one chunk at a time in memory beyond the inevitable single-buffer cost on the write path.
|
||||
- Composable with the drive middleware chain — every read / write goes through the chain regardless of which path is taken.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
const reader = drive.createReadStream("video.mp4");
|
||||
for await (const chunk of reader) {
|
||||
console.log(`got ${chunk.byteLength} bytes`);
|
||||
}
|
||||
|
||||
const writer = drive.createWriteStream("upload.bin");
|
||||
const w = writer.getWriter();
|
||||
await w.write(new Uint8Array([1, 2, 3]));
|
||||
await w.close();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
const ctrl = new AbortController();
|
||||
setTimeout(() => ctrl.abort(), 30_000);
|
||||
const reader = drive.createReadStream("video.mp4", ctrl.signal);
|
||||
const decoder = new TextDecoderStream();
|
||||
const writer = drive.createWriteStream("decoded.txt", ctrl.signal);
|
||||
await reader.pipeThrough(decoder).pipeTo(writer);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createReadStream(storage: DriveStorage, path: string, signal?: AbortSignal): ReadableStream<Uint8Array>`.
|
||||
- `createWriteStream(storage: DriveStorage, path: string, signal?: AbortSignal): WritableStream<Uint8Array>`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The fallback path uses standard WHATWG streams so it composes with `pipeThrough` / `pipeTo` and TransformStream pipelines. The choice between native and fallback happens at stream-construction time; once a stream is constructed it commits to one path. If a storage's `nativeStreams` capability flips after the stream is constructed (e.g. through dynamic-feature negotiation), existing streams keep using their original path until they're recreated.
|
||||
47
docs/networking/drive/drive/src/file-operations/write.md
Normal file
47
docs/networking/drive/drive/src/file-operations/write.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# write
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements the byte-writing helper that powers `Drive.write(path, data, options?)`. Delegates to the underlying `DriveStorage.set(path, data, signal)` and persists the bytes verbatim; downstream plugins (chunks, encryption) intercept via the middleware chain to chunk + encrypt before the bytes ever reach the storage. The drive layer's only job is to thread the path / payload / abort signal through the middleware chain in the right order.
|
||||
|
||||
## Features
|
||||
|
||||
- Direct delegation to `storage.set(path, data, signal)` — no copy, no transformation at the drive level.
|
||||
- Composable with the drive middleware chain so plugins can intercept the write (chunked encryption, integrity-tag insertion, write-through caching).
|
||||
- Honours the `AbortSignal` so callers can cancel slow writes mid-flight.
|
||||
- Throws `DriveError(DRIVE.IO_FAILED)` when the signal is already aborted before the underlying storage's `set` is reached.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
const data = new TextEncoder().encode("hello");
|
||||
await drive.write("notes/hello.txt", data);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
const ctrl = new AbortController();
|
||||
setTimeout(() => ctrl.abort(), 30_000);
|
||||
try {
|
||||
await drive.write("video.mp4", largeBuffer, { signal: ctrl.signal });
|
||||
} catch (err) {
|
||||
if ((err as Error).name === "AbortError") {
|
||||
console.warn("write cancelled by deadline");
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `writeFile(storage: DriveStorage, path: string, data: Uint8Array, signal?: AbortSignal): Promise<void>` — bare helper. Persists the raw bytes via the storage adapter.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function is intentionally minimal — it exists so the drive's middleware chain has a stable handler to wrap rather than calling through to storage directly. The chunks plugin's write-side ingest path runs `drive.write(...)` to record the original plaintext at the path, then ingests via `chunks.ingest(path)` to produce the per-chunk records. Skipping the middleware chain (e.g. for chunk-bytes-only writes from a mesh forwarder) requires `drive.getStorage(id).set(path, data)` with explicit storage selection.
|
||||
60
docs/networking/drive/drive/src/plugins/registry.md
Normal file
60
docs/networking/drive/drive/src/plugins/registry.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# registry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements `PluginRegistry`, the install / dispose / middleware bookkeeper that backs `Drive.use(plugin)` / `Drive.useMiddleware(middleware)`. Plugins are installed in registration order with their typed handle returned to the caller; the registry disposes them in reverse order on `Drive.dispose()` so a plugin's teardown sees its dependencies still alive. Middleware chains run outer-first on request, outer-last on response, matching the suaveplan-wide middleware convention.
|
||||
|
||||
## Features
|
||||
|
||||
- Plugin install with typed handle propagation (`drive.use(plugin)` returns the handle the plugin's `install` produced).
|
||||
- Middleware registration independent of plugins — plugins typically register their own middleware during install but consumers can also register cross-cutting middleware (logging, tracing, retry) without authoring a plugin.
|
||||
- Middleware chains for every file-operation method (`read`, `write`, `delete`, `stat`, `list`).
|
||||
- Reverse-order disposal so a plugin's `dispose` can call into the plugins registered before it without finding them already torn down.
|
||||
- Coded errors: `DRIVE.PLUGIN_FAILED` on install or dispose failure, with the underlying cause threaded through `SuaveplanError.cause`.
|
||||
- Lazy chain construction: only middlewares that define a handler for the current operation participate in the chain, so packages that ship `read`-only middleware don't pay the cost of being skipped on every `write`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
|
||||
const drive = new Drive(storage);
|
||||
await drive.initialize();
|
||||
const handle = await drive.usePlugin(somePlugin);
|
||||
drive.useMiddleware({
|
||||
async read(ctx, next) {
|
||||
console.log("reading", ctx.path);
|
||||
return next();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { DriveMiddleware } from "@suaveplan/drive";
|
||||
|
||||
const tracer: DriveMiddleware = {
|
||||
async read(ctx, next) {
|
||||
const start = performance.now();
|
||||
try {
|
||||
return await next();
|
||||
} finally {
|
||||
console.log(`read ${ctx.path}: ${performance.now() - start} ms`);
|
||||
}
|
||||
},
|
||||
};
|
||||
drive.useMiddleware(tracer);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class PluginRegistry` — `install(drive, plugin)`, `useMiddleware(middleware)`, `dispose()`, `runMiddleware<T>(operation, path, drive, impl)`.
|
||||
- Throws `DriveError(DRIVE.PLUGIN_FAILED)` on plugin install / dispose failures.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Middleware is stored in a flat array and the chain is built lazily per operation; calling `next()` advances an internal index, and not calling it short-circuits the chain. Plugin disposal runs sequentially rather than in parallel so a plugin's `dispose` can rely on previously-registered plugins still being live — parallel disposal would surface as race conditions where a plugin's teardown observes a half-disposed neighbour.
|
||||
@@ -0,0 +1,48 @@
|
||||
# drive-options.schema
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the Zod boundary schema that gates `Drive` constructor inputs (`DriveOptions`). The schema rejects malformed top-level shapes (missing keys, wrong-kind values) before any drive-side logic runs so consumers see a structured `ZodError` rather than a downstream `TypeError` from a misaligned plugin install or middleware registration.
|
||||
|
||||
## Features
|
||||
|
||||
- `DriveOptionsSchema` — strict-enough object schema accepting the full `DriveOptions` surface (`defaultStorage`, `plugins`, `middleware`, `clock`, `rng`, `logger`, `events`, `ourPubkey`).
|
||||
- Permissive nested validation — deep validation of plugins, storages, and middleware is the responsibility of their owning packages, so the schema uses `z.custom<unknown>()` for nested objects.
|
||||
- `// schema: boundary-only` annotation so the conformance script doesn't flag the consumer-facing schema for being unused outside `src/schemas/`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { DriveOptionsSchema } from "@suaveplan/drive";
|
||||
|
||||
const result = DriveOptionsSchema.safeParse(input);
|
||||
if (!result.success) throw new Error(result.error.message);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Drive, DriveOptionsSchema } from "@suaveplan/drive";
|
||||
|
||||
function buildDriveSafely(input: unknown) {
|
||||
const parsed = DriveOptionsSchema.safeParse(input);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`bad drive config: ${parsed.error.message}`);
|
||||
}
|
||||
return new Drive(parsed.data);
|
||||
}
|
||||
|
||||
const allFields = DriveOptionsSchema.shape;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `DriveOptionsSchema: z.ZodObject` — top-level validator for `DriveOptions`.
|
||||
- Throws `ZodError` (via `.parse`) or returns `{ success: false }` (via `.safeParse`) for malformed input.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema is intentionally loose at the leaves (`z.custom<unknown>()` for plugins / storages / middleware / clock / rng / logger / events) because deep validation lives in those packages: a `DrivePlugin`-shaped object would cost a redundant tree-walk here. The job of this schema is to guarantee the top-level `DriveOptions` shape is structurally valid (object with known keys, no extras), not to re-validate every nested shape — Zod's strict-mode-by-default at the top level catches misspelt keys without forcing brittle deep schemas.
|
||||
59
docs/networking/drive/drive/src/types/drive.types.md
Normal file
59
docs/networking/drive/drive/src/types/drive.types.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# drive.types
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the public TypeScript contracts for `@suaveplan/drive` — the structural shapes that `Drive` implements and that plugins / middleware / hydration consumers depend on. All cross-cutting primitives (`Clock`, `Rng`, `LoggerLike`, `EventBusLike`, `PeerId`) are imported from `@suaveplan/types` to avoid diamond dependencies; types defined here are package-specific and intentionally stay in this package's owning surface.
|
||||
|
||||
## Features
|
||||
|
||||
- `DriveStorage` — structural extension of `AbstractStorage` adding `id: string` plus optional `connect` / `dispose` / `isConnected` lifecycle hooks. Drive consumes any concrete `AbstractStorage` directly through this view.
|
||||
- `DrivePlugin<THandle>` — install / dispose contract with a typed handle the install method returns. `Drive.usePlugin` propagates the handle back to the caller.
|
||||
- `DriveMiddleware` — interceptor chain for `read` / `write` / `delete` / `stat` / `list`. Each handler receives a `MiddlewareContext` (operation, path, drive, free-form bag) and a `next()` continuation.
|
||||
- `DriveLike` — minimal structural surface plugins depend on, deliberately narrower than `Drive` so plugin packages can avoid importing the concrete class.
|
||||
- `DriveOptions` — constructor bag (`defaultStorage`, `plugins`, `middleware`, `clock`, `rng`, `logger`, `events`, `ourPubkey`).
|
||||
- `DriveEntry` / `DriveStats` — directory-listing and stat shapes; `DriveEntry.keyStatus` and `DriveEntry.presence` are optional fields populated by the hydration index.
|
||||
- `KeyStatus` — `'unencrypted' | 'encrypted-locked' | 'encrypted-unlocked'` three-state union.
|
||||
- `ChunkPresence` — chunk-level progress shape returned by `Drive.getPresence`.
|
||||
- `HydrateOptions` / `HydrateProgressEvent` / `HydrateResult` — argument and result shapes for `Drive.hydrateFrom`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { DriveStorage, DriveLike } from "@suaveplan/drive";
|
||||
|
||||
declare const storage: DriveStorage;
|
||||
declare const drive: DriveLike;
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
ChunkPresence,
|
||||
DriveMiddleware,
|
||||
KeyStatus,
|
||||
} from "@suaveplan/drive";
|
||||
|
||||
function describe(status: KeyStatus): string {
|
||||
return status === "encrypted-locked" ? "🔒" : "📄";
|
||||
}
|
||||
|
||||
const tracer: DriveMiddleware = {
|
||||
async read(ctx, next) {
|
||||
console.log("read", ctx.path);
|
||||
return next();
|
||||
},
|
||||
};
|
||||
|
||||
declare const presence: ChunkPresence;
|
||||
const pct = (presence.chunksPresent / presence.chunksTotal) * 100;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `DriveStorage`, `DrivePlugin<THandle>`, `DriveMiddleware`, `DriveLike`, `DriveOptions`, `ReadOptions`, `WriteOptions`, `ListOptions`, `DriveEntry`, `DriveStats`, `MiddlewareContext`, `MiddlewareNext<T>`.
|
||||
- `KeyStatus`, `ChunkPresence`, `HydrateOptions`, `HydrateProgressEvent`, `HydrateResult`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`DriveLike` is intentionally minimal so plugin packages can depend on it without importing the full `Drive` class — this matters because `Drive` pulls in storage / events / templates and the plugin host doesn't always need the concrete class. `DriveStorage` does not redeclare `capabilities`; it inherits `AbstractStorage.capabilities` directly so suaveplan-wide capability negotiation stays in one place. The hydration types (`KeyStatus` / `ChunkPresence` / `HydrateOptions` / `HydrateResult`) live here rather than in `@suaveplan/types` because they are drive-shape-specific — only `@suaveplan/drive` emits them and only drive consumers care.
|
||||
Reference in New Issue
Block a user