3.3 KiB
3.3 KiB
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:56declaresgetPartial,getItemSize,createReadStream,createWriteStreamas abstract — there is nosetPartialcounterpart.defaultGetPartialexists as a protected helper (abstract.ts:178); the symmetricdefaultSetPartialdoes not.MemoryStorageoverrides streams with zero-copy variants but has no random-access write API.StorageCapabilitiesis informal; this change pins it as a required readonly property on every storage instance.
What changes
1. Abstract method addition
abstract setPartial(
key: string,
offset: number,
bytes: Uint8Array,
signal?: AbortSignal,
): Promise<void>;
Semantics:
- Writes
bytesatoffsetwithin the storage object atkey. - 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
setPartialcalls 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
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
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.
capabilitiesbecomes required readonly property; constructors set it.storage-s3gains the fallback wiring (no behaviour change for non-setPartialcallers).- New conformance tests pinning the contract.
Out of scope
storage-s3multipart-upload native impl — follow-up.- IndexedDB streaming chunked write — follow-up.
- Atomicity guarantees — covered by
atomicWritesflag.