7.3 KiB
Design — add-drive-uri-writes
Non-normative. The normative contract is ./specs/drive-uri/spec.md.
Per-scheme operation matrix
| Operation | drive:// |
drive+mesh:// |
drive+template:// |
|---|---|---|---|
set(key, bytes) (resolver write) |
Drive.write(key, bytes, { signal }) → void |
Drive.write(key, bytes, { signal }) → void; mesh advertise NOT triggered |
throw DRIVE_URI.TEMPLATE_READ_ONLY |
delete(key) (resolver remove) |
Drive.delete(key, signal) → boolean |
Drive.delete(key, signal) → boolean; mesh "no-longer-available" advert NOT triggered |
throw DRIVE_URI.TEMPLATE_READ_ONLY |
clear() |
throw DRIVE_URI.WRITE_UNSUPPORTED (Drive-wide wipe footgun) |
same | same |
createReadStream |
unchanged | unchanged | unchanged |
get / exists / list / stat |
unchanged | unchanged | unchanged |
clear() retains DRIVE_URI.WRITE_UNSUPPORTED to preserve the existing error contract for that footgun call site. The new TEMPLATE_READ_ONLY code is specific to template-backed mutation attempts — operators differentiate "this drive forbids wholesale wipes" (a config-time concern) from "this URI scheme is intrinsically read-only" (an architectural constraint).
Error code transition
| Old behaviour | New behaviour | Code |
|---|---|---|
set on drive:// → throw WRITE_UNSUPPORTED |
succeeds | (success) |
set on drive+mesh:// → throw WRITE_UNSUPPORTED |
succeeds | (success) |
set on drive+template:// → throw WRITE_UNSUPPORTED |
throw TEMPLATE_READ_ONLY |
DRIVE_URI.TEMPLATE_READ_ONLY |
delete on drive+template:// → forwarded to underlying Drive.delete (silent footgun: would mutate the hydrated cache, breaking the dehydrated semantics) |
throw TEMPLATE_READ_ONLY |
DRIVE_URI.TEMPLATE_READ_ONLY |
clear() on any scheme → throw WRITE_UNSUPPORTED |
unchanged | DRIVE_URI.WRITE_UNSUPPORTED |
DRIVE_URI.WRITE_UNSUPPORTED is intentionally retained — consumers who already asserted on it for the clear() path continue to work; we narrow its meaning rather than churn the constant.
Mesh write — propagation timing and observability
drive+mesh:// writes complete when local persist is durable. Specifically:
Drive.write(path, bytes, { signal })returns when the installedchunksPluginmiddleware has resolved (encrypted, chunked, persisted to the configured storage).- Mesh re-advertise (
mesh.advertise(fileId, bitset, totalChunks)) is NOT triggered by the URI surface. This matches direct-Drivebehaviour and avoids surprising consumers who manageGenerationTracker/AdvertiseThrottlelifecycles themselves. - The plugin emits a span event
drive_uri.mesh_write_local_onlyon every successfuldrive+mesh://write, with attributes{ scheme: "drive+mesh", path }. Operators can grep the trace to confirm the contract holds in their environment. - Consumers wanting gossip propagation invoke
mesh.advertise(fileId, ...)after the write resolves. Propagation thereafter follows the existingGossipEnginetiming (best-effort, backoff-throttled, observable via the mesh package's own events).
This deliberate choice preserves separation of concerns: the URI surface is a routing layer; mesh participation is a Drive-tier policy.
Key-rotation impact (none, by inheritance)
A write may produce data encrypted under a NEW KeySlotPublic version. The URI surface does no key derivation, no slot resolution, and no grant signing — those live in chunksPlugin and in Drive.hydrateFrom's key:grant subscription. The read path validates retroactive grants today; nothing in the write path needs to change for new key versions to be honoured on subsequent reads.
No new wiring required at the URI tier.
AbortSignal threading
resolver.write(uri, body, signal?)passessignaltoInstrumentedDriveAdapter.set(key, value, signal).InstrumentedDriveAdapter.setupdates its signature to acceptAbortSignal(currently typedset(key, value)perStorageLike; we add the third arg as an optional propagated toDrive.write({ signal })). Pre-aborted signals short-circuit toDRIVE_URI.ABORTEDbefore any work.resolver.remove(uri, signal?)already threadssignaltoadapter.delete(key); we widen the override to accept and forwardsignaltoDrive.delete(path, signal).
StorageLike itself does not specify signal on set/delete. DriveStorageLikeAdapter adds the parameter as a tolerated extension; the resolver already passes it via the streaming-write surface (createWriteStream(key, body, signal)), so the precedent is established.
File-layout impact
New / edited files (under packages/drive/drive-uri/src/):
adapter/
drive-storage-like.ts # widen set/delete to accept signal; forward through
drive-storage-like.md # doc update — note new TEMPLATE_READ_ONLY case
error-codes/
error-codes.ts # add TEMPLATE_READ_ONLY to defineCodes
error-codes.md # update; describe narrowed WRITE_UNSUPPORTED role
plugins/
drive/
drive.streams.ts # remove unconditional WRITE_UNSUPPORTED; per-scheme dispatch
drive.streams.md # doc update
drive-template/
drive-template.ts # override set/delete → TEMPLATE_READ_ONLY
drive-template.md # doc update
otel/
otel.ts # add `mesh_write_local_only` span-event constant
otel.md # doc update
README.md # update Features / Modules sections
tests/integration/
drive-roundtrip-write.test.ts # NEW — real Drive, write+read cycle
drive-mesh-roundtrip-write.test.ts # NEW — mesh-configured Drive, assert no implicit advertise
template-write-rejection.test.ts # NEW — TEMPLATE_READ_ONLY round-trip
All new code stays inside the existing 300-line / 500-line file budgets; the changed drive.streams.ts currently sits at 148 lines and the per-scheme branching adds ~30 lines.
Trade-offs considered
- Per-scheme
setdispatch viaif (scheme === "drive+template")vs. polymorphic adapter subclass. Chose the inline branch — only one scheme has the constraint and a subclass would duplicate the entire instrumented method body. If a fourth scheme appears, revisit. - Auto-advertise on mesh writes. Rejected: violates the direct-
Drivecontract, would surprise consumers who runGenerationTrackerthemselves, and forces every mesh-write call site through an advertise budget. The span event gives operators visibility without enforcing policy. - Reusing
WRITE_UNSUPPORTEDfor template rejections. Rejected: it conflates a temporary state (the deferred scope this change closes) with a permanent architectural constraint (templates are signed). Distinct codes let consumers branch correctly. - Removing
WRITE_UNSUPPORTEDentirely. Rejected: it would churn the public surface (consumers may already assert on it for theclear()path) and §2c forbids deleting a registered code without a real reason. Narrowing the role is the right move.
Cross-reference
This change is the second consumer of structural DriveLike (after add-drive-range-reads), strengthening the case for hoist-drivelike-to-types. Independent landing — drive-uri imports DriveLike from @suaveplan/drive today and will move to @suaveplan/types once hoist-drivelike-to-types archives.