Files
2026-05-27 23:29:53 +01:00

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: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

abstract setPartial(
    key: string,
    offset: number,
    bytes: Uint8Array,
    signal?: AbortSignal,
): Promise<void>;

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

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.
  • 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.