113 lines
6.2 KiB
Markdown
113 lines
6.2 KiB
Markdown
# OpenSpec Change: drive-port
|
|
|
|
- **Change ID**: drive-port
|
|
- **Package**: `@suaveplan/drive` (new)
|
|
- **Status**: PROPOSED
|
|
- **Date**: 2026-04-28
|
|
|
|
## Summary
|
|
|
|
Port `@suaveplan/drive` from `../suaveplan/packages/universal/drive` into genesis as a **file-VFS-only** core. Drive in genesis exposes paths and bytes — `read`, `write`, `delete`, `stat`, `list`, `createReadStream`, `createWriteStream` — backed by `AbstractStorage` from `@suaveplan/storage`. Chunking, P2P mesh sync, transports, and `transfer()`-style cross-drive copy live in **separate packages** that consume drive via its plugin API. There is no `StorageAdapter` shim — drive sits directly on `AbstractStorage`. Constructor accepts a storage instance directly; `Drive.initialize()` calls `storage.connect()` for storages drive owns; `Drive.dispose()` closes only those it opened.
|
|
|
|
## Why this is file-only
|
|
|
|
The previous draft of this proposal carried chunks, `transfer()` / manager, and capability-aware streaming all in one package. Discussion this session split those into:
|
|
|
|
| Concern | Package | Layer |
|
|
| --- | --- | --- |
|
|
| File VFS (paths, bytes, metadata, plugin slot) | `@suaveplan/drive` | this change |
|
|
| Random-access offset writes on storage | `@suaveplan/storage` (`setPartial`) | `storage-set-partial` |
|
|
| Chunk format, manifest, packer, sig-verify gate | `@suaveplan/drive-plugin-chunks` | `drive-plugin-chunks-port` |
|
|
| Peer index, availability gossip, parallel pull | `@suaveplan/drive-mesh` | `drive-mesh-port` |
|
|
| Wire — HTTPS / WebRTC / libp2p / SMB-as-storage | `@suaveplan/drive-transport-*` | per transport |
|
|
|
|
Drive proper does **not** know about chunks, peers, signatures, or wires. It is a thin file abstraction over storage with a plugin slot.
|
|
|
|
## Current state
|
|
|
|
- Source `@suaveplan/drive` v1.1.1 (`../suaveplan/packages/universal/drive`): ~7 300 LOC, 127 files, ships only `MemoryStorage`, has its own incompatible `StorageAdapter` interface.
|
|
- Genesis `AbstractStorage` (`packages/storage/storage/src/abstract/abstract.ts:56`) already declares `getPartial`, `getItemSize`, `createReadStream`, `createWriteStream` as abstract — the `MIGRATION.md` §6.3 storage-bridge work is already done. No `StorageProviderAdapter` shim is created.
|
|
|
|
## What changes
|
|
|
|
### 1. Storage contract — `DriveStorage` is structural
|
|
|
|
`Drive` consumes `AbstractStorage` directly. `DriveStorage` is a structural view that adds an `id: string`, optional `capabilities`, and optional `connect`/`dispose`/`isConnected` lifecycle hooks. See `storage-adapter-sketch.md` in this folder for the full TypeScript sketch.
|
|
|
|
### 2. Constructor overloads
|
|
|
|
```ts
|
|
new Drive() // empty, attach storages later
|
|
new Drive(storage) // bare AbstractStorage, registered as id "default"
|
|
new Drive(storage, options) // storage + options
|
|
new Drive({ defaultStorage, plugins, ... }) // options-only form
|
|
```
|
|
|
|
`new Drive(storage)` is sugar for `new Drive({ defaultStorage: storage })` — observably equivalent.
|
|
|
|
### 3. Lifecycle ownership
|
|
|
|
`Drive.initialize()` walks `this.storages` and calls `storage.connect?.()` only when `storage.isConnected?.() !== true`. Storages drive itself connects are tracked in `connectedByMe: WeakSet<DriveStorage>`. `Drive.dispose()` closes only those — pre-connected storages passed in by the caller are left open. Both `initialize` and `dispose` are idempotent.
|
|
|
|
### 4. Plugin slot
|
|
|
|
```ts
|
|
interface DrivePlugin<THandle = void> {
|
|
readonly name: string;
|
|
install(drive: Drive): THandle | Promise<THandle>;
|
|
dispose?(): void | Promise<void>;
|
|
}
|
|
|
|
const chunks = drive.use(chunksPlugin({ packer, serverPubKey }));
|
|
// drive.* — file-level
|
|
// chunks.* — chunk-level (via the plugin's returned handle)
|
|
```
|
|
|
|
`drive.use(plugin)` returns the plugin's typed handle. Drive's own type surface stays file-level; plugins extend functionality through their handles, not through module augmentation.
|
|
|
|
Plugins also register middleware on drive's file ops (`read`, `write`, `delete`, `stat`, `list`) via `drive.useMiddleware(...)`. Middleware execute outer-first on request, outer-last on response; `plugin.dispose()` runs in reverse-registration order on `Drive.dispose()`.
|
|
|
|
### 5. Generics
|
|
|
|
```ts
|
|
class Drive<TStorageMap extends Record<string, DriveStorage> = Record<string, DriveStorage>> {
|
|
use<K extends string, S extends DriveStorage>(id: K, storage: S):
|
|
Drive<TStorageMap & Record<K, S>>;
|
|
getStorage<K extends keyof TStorageMap & string>(id: K): TStorageMap[K];
|
|
}
|
|
```
|
|
|
|
Storage IDs are typed end-to-end. Calls with an unknown id are a TypeScript error.
|
|
|
|
### 6. Module-augmentation factories
|
|
|
|
`StorageFactory.register('file', FileSystem)` (already in `@suaveplan/storage`) is the single registration point. `Drive.fromConfig({ storage: { type: 'file', config: { ... } } })` resolves through `StorageFactory`. Drive does not maintain a separate adapter registry.
|
|
|
|
### 7. Error codes
|
|
|
|
`DRIVE_CODES = defineCodes("DRIVE", [NOT_INITIALIZED, DISPOSED, STORAGE_CONNECT_FAILED, STORAGE_NOT_FOUND, IO_FAILED, INVALID_PATH, PLUGIN_FAILED] as const)`. `DriveError extends SuaveplanError<DriveErrorCode>`. Frozen registry.
|
|
|
|
### 8. Error handling boundaries
|
|
|
|
- `storage.connect()` errors → `DriveError(DRIVE.STORAGE_CONNECT_FAILED, { cause })`
|
|
- ops on disposed drive → `DriveError(DRIVE.DISPOSED)`
|
|
- ops before `initialize()` → `DriveError(DRIVE.NOT_INITIALIZED)`
|
|
- `getStorage("nope")` → `DriveError(DRIVE.STORAGE_NOT_FOUND)`
|
|
- plugin install/dispose throw → `DriveError(DRIVE.PLUGIN_FAILED, { cause })`
|
|
|
|
## Impact
|
|
|
|
- New package `packages/drive/drive/` at `0.1.0`.
|
|
- New OpenSpec capability `drive` at `openspec/specs/drive/spec.md`.
|
|
- No changes to `@suaveplan/storage` (the `setPartial` addition is `storage-set-partial`, a separate change).
|
|
|
|
## Out of scope (covered by sibling changes)
|
|
|
|
- Chunk format, manifest, packer integration → `drive-plugin-chunks-port`
|
|
- Sparse-file storage layout → requires `storage-set-partial`
|
|
- Mesh peer index / parallel pull → `drive-mesh-port`
|
|
- Wire transports → `drive-transport-https-port`, `drive-transport-webrtc-port`
|
|
- `transfer()` / `DriveManager` cross-drive copy → deferred; mesh subsumes most of it
|
|
- `@suaveplan/drive-sharing` port → separate change
|
|
- `@suaveplan/vault` orchestration layer → separate change after drive lands
|