Files
suaveplan/docs/drive/plugins.md
2026-05-27 23:29:53 +01:00

8.4 KiB

Drive Plugins

The drive core does five things: lifecycle, path resolution, the two indexes, the middleware chain, and capability fallbacks. Everything else is a plugin.

This document is the catalogue of plugins (shipped, planned, and conventional) and how they compose.

The plugin contract

interface DrivePlugin {
  readonly name: string;
  readonly hooks: Record<string, PluginHook>;
  readonly priority?: number;  // higher first
}

type PluginHook = (context: unknown, next: NextFunction) => Promise<unknown>;

Plugins register hooks against operation names — drive:read, drive:write, drive:delete, drive:stat, drive:list, drive:initialize, drive:dispose. Hooks run in priority order (high → low) on the request path, then the same order in reverse on the response path. A hook can:

  • transform the args before delegating (await next()),
  • transform the result after delegating,
  • short-circuit by not calling next() and returning a value directly,
  • throw, propagating up the chain.

useMiddleware(mw) is a thin sugar for a plugin whose only hook is the universal drive:* interceptor.

Plugin catalogue

Plugin Status Responsibility
chunks (@suaveplan/drive-plugin-chunks) shipped Splits files into signed, encrypted chunks. Owns the manifest. Owns layout selection (sparse-file vs chunk-keys).
sharing (@suaveplan/drive-sharing) pending — main Partition-based access control. Envelope encryption. Grant/revoke/rotate.
react-sharing (@suaveplan/drive-react-sharing) pending — main React UI for the sharing engine — <ShareDialog>, <PartitionBadge>, useSharing().
mesh (@suaveplan/drive-mesh) planned (genesis-original) Per-file chunk scheduling: which peer to ask for which chunk. Bitset advertisement. Per-peer scoring.
transport-https (@suaveplan/drive-transport-https) planned (genesis-original) HTTPS chunk transport — server fallback when WebRTC isn't available. GET /v1/chunks/<fileId>/<chunkId> with Range.
transport-webrtc (@suaveplan/drive-transport-webrtc) planned (genesis-original) WebRTC datachannel chunk transport — browser↔browser. [uint32 BE frameLen][uint8 frameType][payload] framing.
webrtc (@suaveplan/drive-webrtc) pending — main A pre-packaged "browser drive" plugin: chunks + transport-webrtc + signalling, in one.
cache (@suaveplan/cache) shipped Read-through cache middleware. Drop in to cache decrypted plaintext.
cow (planned) planned Copy-on-write versioning. Multiple version numbers per fileId. Snapshot create/restore/diff.
dedup (planned) planned Content-addressed dedup. Hashes content, points multiple fileLocIds at the same chunks.
metadata (planned, see streams-metadata) planned Side-channel metadata extraction (EXIF/MIME/audio) on every write.

How the chain runs end-to-end

A typical browser drive:

const drive = new Drive(opfsStorage)
  .usePlugin(chunksPlugin({ chunkSize: 65536, signing: { secretKey } }))
  .usePlugin(sharingPlugin({ partitions, kms }))
  .usePlugin(meshPlugin({ networkNode }))
  .usePlugin(transportWebrtcPlugin({ signaller }))
  .useMiddleware(loggingMiddleware);

Registration order matters:

                           drive.read("/photo.jpg")
                                       │
                          ┌────────────▼────────────┐
                          │ logging  (pre)          │
                          │   chunks (pre)          │  load manifest
                          │     sharing (pre)       │  unwrap envelope key
                          │       mesh (pre)        │  prepare peer scoring
                          │         transport (pre) │  open peer connections
                          └────────────┬────────────┘
                                       ▼
                                 storage.get
                                       │
                          ┌────────────▼────────────┐
                          │         transport (post)│  fetch missing chunks via WebRTC
                          │       mesh (post)       │  reassemble
                          │     sharing (post)      │  decrypt envelope
                          │   chunks (post)         │  verify+decrypt+concat
                          │ logging  (post)         │  log duration
                          └────────────┬────────────┘
                                       ▼
                                 Uint8Array

Each layer is independent. Strip any one and the layers above keep working — mesh without transport-webrtc falls back to transport-https; sharing without mesh works on a single-node drive; chunks without mesh works with no network at all.

Cross-layer ownership boundaries

The genesis design is meticulous about which layer owns what. The drive-network-integration change folder (proposal) is the canonical resolution document; the summary is:

Concern Owner Why
Peer identity & lifecycle network-plugin-registry Peers exist independent of files; one registry per mesh.
Per-file chunk-availability bitsets drive-mesh Specific to a file's manifest; doesn't belong in the generic registry.
Per-file chunk scheduling (which peer to ask, in what order) drive-mesh Highly file-specific; uses peer health from registry but decides locally.
Mesh-wide chunk redundancy (N copies, prefetch, integrity) network-plugin-content-delivery Cuts across files; lives at the network layer because it's about peers, not files.
Cross-mesh forwarding for chunks network-plugin-relay Same as message relay; a chunk request is just another envelope.

The interaction is event-driven via NetworkBus:

  • peer:joined / peer:left / peer:updated from network-plugin-registry.
  • cd:advertise from drive-mesh (this drive has these chunks).
  • cd:request from drive-mesh (this drive needs this chunk).
  • cd:response from network-plugin-content-delivery (here it is).

That keeps drive-mesh blissfully unaware of which transport is fetching the chunk; it just listens for cd:response.

Conventions for writing a new plugin

  1. No drive imports. Use DrivePlugin, MiddlewareHandler, and MiddlewareContext from @suaveplan/types. Never import Drive itself — that creates a circular dep.
  2. Idempotent install. A plugin's install hook may run more than once if a drive is reinitialized. Track state on the plugin instance, not in module scope.
  3. Reverse dispose. If your plugin allocates anything (sockets, workers, watchers), free it in drive:dispose. The drive guarantees this hook runs in reverse-registration order, so plugins added later get to clean up first.
  4. Surface errors as DriveError. Wrap thrown errors as DriveError(DRIVE.PLUGIN_FAILED, { cause }) so consumers see one error type at the boundary.
  5. Use @suaveplan/types for backend-agnostic capabilities. If you need to know what the storage can do, read storage.capabilities. If you need to know what the network can do, read it through NetworkBus capability events. Don't hardcode.

Why this is split, not a fat library

A drive that runs in a browser without WebRTC just doesn't install the WebRTC plugin. A server drive in a single-node deployment doesn't install mesh. A test drive doesn't install any plugin at all and uses MemoryStorage directly. None of those configurations cost extra bytes or extra surface area, because the core knows nothing about chunks, peers, or partitions — it just runs whatever middleware you registered.

This is the genesis bet: composability over completeness. The enterprise-drive monolith design tried the other way and ended up with a 2 MB package that imports half the platform on the import line. The split keeps every drive minimal.

Reading further