# Unified `DriveStorage` — design sketch Reference for `drive-port`. Drive does **not** define its own `StorageAdapter` class; it consumes `AbstractStorage` from `@suaveplan/storage` directly and layers a thin capability protocol on top. ## 1. The base contract is already there `packages/storage/storage/src/abstract/abstract.ts:56` declares the abstract surface drive needs: ```ts abstract class AbstractStorage implements StorageLike { abstract get(key, signal?): Promise; abstract set(key, value, opts?, signal?): Promise; abstract delete(key, signal?): Promise; abstract exists(key, signal?): Promise; abstract list(opts?, signal?): Promise; abstract clear(signal?): Promise; abstract getWithMetadata(key, signal?): Promise | null>; abstract getMetadata(key, signal?): Promise; abstract getSize(signal?): Promise; abstract getPartial(key, offset, length, signal?): Promise; abstract getItemSize(key, signal?): Promise; abstract createReadStream(key, signal?): ReadableStream; abstract createWriteStream(key, signal?): WritableStream; } ``` Every method drive's source `StorageAdapter` provided (`read`, `write`, `readPartial`, `size`, streams) is already present here. The `MIGRATION.md` §6.3 enhancement work is a no-op in genesis. ## 2. The `DriveStorage` view ```ts // @suaveplan/drive/src/types/drive-storage.types.ts import type { AbstractStorage } from "@suaveplan/storage/abstract"; export interface StorageCapabilities { /** `getPartial` is a native byte-range read (S3, OPFS). */ partialReads?: boolean; /** `createReadStream`/`createWriteStream` are native (no buffer-and-chunk emulation). */ nativeStreams?: boolean; /** `set` is atomic (write-or-rollback, no partial visibility). */ atomicWrites?: boolean; /** Keys are content-derived hashes (chunk maps can be cross-mounted without re-read). */ contentAddressable?: boolean; /** `connect` MUST be called before use; `dispose` releases handles. */ requiresLifecycle?: boolean; } export interface DriveStorage extends AbstractStorage { /** Stable identifier used by `Drive.use(id, storage)` and `getStorage(id)`. */ readonly id: string; /** Optional self-description; absent flags = treat as `false`. */ readonly capabilities?: StorageCapabilities; /** Optional lifecycle hooks (drive only invokes them when defined and `isConnected?.() !== true`). */ connect?(): Promise; dispose?(): Promise; isConnected?(): boolean; } ``` `DriveStorage` is **structural** — any `AbstractStorage` subclass with an `id` field satisfies it; capabilities and lifecycle hooks are duck-typed and probed at runtime. ## 3. How drive uses capabilities ```ts async readPartial(path: string, offset: number, length: number, signal?: AbortSignal) { const storage = this.resolve(path); if (storage.capabilities?.partialReads) { const slice = await storage.getPartial(path, offset, length, signal); if (slice !== null) return slice; } const full = await storage.get(path, signal); if (full === undefined) throw new DriveError(DRIVE_CODES.IO_FAILED, `not found: ${path}`); return full.slice(offset, offset + length); } ``` The capability flag is the contract; the inherited `defaultCreate*Stream` from `AbstractStorage` already provides the emulation fallback, so drive doesn't reimplement it. ## 4. Lifecycle in `Drive` ```ts class Drive> { readonly #storages = new Map(); readonly #connectedByMe = new WeakSet(); #state: "fresh" | "ready" | "disposed" = "fresh"; async initialize(): Promise { if (this.#state === "ready") return; if (this.#state === "disposed") throw new DriveError(DRIVE_CODES.DISPOSED); for (const storage of this.#storages.values()) { if (storage.isConnected?.() === true) continue; if (typeof storage.connect !== "function") continue; try { await storage.connect(); this.#connectedByMe.add(storage); } catch (cause) { throw new DriveError( DRIVE_CODES.STORAGE_CONNECT_FAILED, `failed to connect storage "${storage.id}"`, { cause }, ); } } this.#state = "ready"; } async dispose(): Promise { if (this.#state === "disposed") return; for (const storage of this.#storages.values()) { if (!this.#connectedByMe.has(storage)) continue; await storage.dispose?.(); } this.#state = "disposed"; } } ``` **Ownership rule:** drive only closes what drive opened. Pre-connected storages passed in by the caller are left open on `dispose()`. This is the property `connectedByMe: WeakSet` enforces. ## 5. Generic narrowing ```ts class Drive = {}> { use( id: K, storage: S, ): Drive> { this.#storages.set(id, storage); return this as unknown as Drive>; } getStorage(id: K): TStorageMap[K] { const storage = this.#storages.get(id); if (!storage) throw new DriveError(DRIVE_CODES.STORAGE_NOT_FOUND, id); return storage as TStorageMap[K]; } } ``` Usage: ```ts const drive = new Drive() .use("local", new MemoryStorage({ id: "local" })) .use("remote", new S3Storage({ id: "remote", bucket: "vault" })); const local: MemoryStorage = drive.getStorage("local"); // narrowed drive.getStorage("nope"); // TS error: argument not assignable to keyof TStorageMap ``` ## 6. Constructor overloads ```ts function Drive(): Drive; function Drive(storage: DriveStorage): Drive<{ default: DriveStorage }>; function Drive(storage: DriveStorage, options: DriveOptions): Drive<{ default: DriveStorage }>; function Drive(options: DriveOptions): Drive; ``` The bare-storage form is sugar: ```ts new Drive(storage) // equivalent to new Drive({ defaultStorage: storage }) ``` Both register the storage under id `"default"` so `drive.getStorage("default")` works in either path. ## 7. Plugin middleware ```ts export interface DrivePlugin { readonly name: string; install(drive: Drive): void; dispose?(): void | Promise; } export interface DriveMiddleware { read?(ctx: ReadCtx, next: () => Promise): Promise; write?(ctx: WriteCtx, next: () => Promise): Promise; delete?(ctx: DeleteCtx, next: () => Promise): Promise; stat?(ctx: StatCtx, next: () => Promise): Promise; list?(ctx: ListCtx, next: () => Promise): Promise; } ``` `drive.use(plugin)` registers in order; middleware execution is outer-first on the request path, outer-last on the response path; `plugin.dispose()` runs in reverse-registration order during `Drive.dispose()`. ## 8. What this replaces | Source-side artefact | Replaced by | | --- | --- | | `drive/src/storage/adapter.ts` (StorageAdapter base class) | Direct `AbstractStorage` consumption + structural `DriveStorage` interface | | `drive/src/storage/memory.ts` (drive-internal MemoryStorage) | `MemoryStorage` from `@suaveplan/storage` | | `storageRegistry` inside drive | `StorageFactory` from `@suaveplan/storage` (single registration point) | | `MIGRATION.md` §6.4 `StorageProviderAdapter` | Not needed — `AbstractStorage` already exposes the four methods | | Constructor flags (`enableCow`, `enableDedup`, ...) | Plugins via `drive.use(cowPlugin())` | | Hard-wired `MemoryStorage` default | No default — caller must pass a storage or use `Drive.fromConfig({ storage: { type, config } })` |