# OpenSpec Change: storage-set-partial - **Change ID**: storage-set-partial - **Package**: `@suaveplan/storage` (modify) - **Status**: PROPOSED - **Date**: 2026-04-28 ## Summary Add `setPartial(key, offset, bytes, signal?)` to `AbstractStorage` with a `partialWrites` capability flag and a `defaultSetPartial` read-modify-write fallback. This unblocks `drive-plugin-chunks-port`'s sparse-file layout (pre-allocate a file at full size, drop chunks at deterministic offsets as they arrive) and matches the existing `getPartial` symmetry. ## Current state - `packages/storage/storage/src/abstract/abstract.ts:56` declares `getPartial`, `getItemSize`, `createReadStream`, `createWriteStream` as abstract — there is no `setPartial` counterpart. - `defaultGetPartial` exists as a protected helper (`abstract.ts:178`); the symmetric `defaultSetPartial` does not. - `MemoryStorage` overrides streams with zero-copy variants but has no random-access write API. - `StorageCapabilities` is informal; this change pins it as a required readonly property on every storage instance. ## What changes ### 1. Abstract method addition ```ts abstract setPartial( key: string, offset: number, bytes: Uint8Array, signal?: AbortSignal, ): Promise; ``` Semantics: - Writes `bytes` at `offset` within the storage object at `key`. - If the object does not exist, MUST create one large enough to hold `[offset, offset + bytes.byteLength)`. Bytes outside the written range are zero-valued (`0x00`) on read. - If the object exists and is shorter than `offset + bytes.byteLength`, MUST extend it; intermediate bytes are zero-valued. - Concurrent `setPartial` calls on the same key are NOT required to be atomic; callers MUST serialise per-key. - AbortSignal honoured on a best-effort basis. ### 2. Default fallback ```ts protected async defaultSetPartial(key, offset, bytes, signal?) { const current = (await this.get(key, signal)) ?? new Uint8Array(0); const end = offset + bytes.byteLength; const out = new Uint8Array(Math.max(current.byteLength, end)); out.set(current, 0); out.set(bytes, offset); await this.set(key, out, undefined, signal); } ``` O(N) memory per call; sparse-file consumers prefer native-capable backends. ### 3. Capability flag ```ts readonly capabilities: Readonly<{ partialReads: boolean; partialWrites: boolean; nativeStreams: boolean; atomicWrites: boolean; contentAddressable: boolean; }>; ``` Default all `false`. Subclasses override per backend. ### 4. Backend wiring (this change) | Backend | `setPartial` | `partialWrites` | | --- | --- | --- | | `MemoryStorage` | Native: resize `Uint8Array`, `set(bytes, offset)` | `true` | | `storage-s3` | `defaultSetPartial` (multipart wrapper deferred) | `false` | | Future ports (`storage-file`, `storage-browser-opfs`) | Native | `true` | | Future ports (`storage-browser` IndexedDB) | `defaultSetPartial` | `false` | ## Impact - New abstract method; every concrete subclass MUST provide an impl. - `capabilities` becomes required readonly property; constructors set it. - `storage-s3` gains the fallback wiring (no behaviour change for non-`setPartial` callers). - New conformance tests pinning the contract. ## Out of scope - `storage-s3` multipart-upload native impl — follow-up. - IndexedDB streaming chunked write — follow-up. - Atomicity guarantees — covered by `atomicWrites` flag.