Initial Commit
This commit is contained in:
89
openspec/archive/2026-04-28-storage-set-partial/proposal.md
Normal file
89
openspec/archive/2026-04-28-storage-set-partial/proposal.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 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<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
|
||||
|
||||
```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.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Storage — Spec Delta (storage-set-partial)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### STORAGE-PARTIAL-WRITE-01 — `setPartial` is part of the abstract surface
|
||||
|
||||
`AbstractStorage` MUST declare `abstract setPartial(key: string, offset: number, bytes: Uint8Array, signal?: AbortSignal): Promise<void>`. Every concrete subclass MUST provide an implementation. A protected `defaultSetPartial` helper MUST exist as the read-modify-write reference fallback.
|
||||
|
||||
**Verified by:** `src/abstract/abstract.spec-conformance.test.ts` (`STORAGE-PARTIAL-WRITE-01`).
|
||||
|
||||
### STORAGE-PARTIAL-WRITE-02 — sparse extension semantics
|
||||
|
||||
When `setPartial(key, offset, bytes)` is called on a non-existent key or on an existing object shorter than `offset + bytes.byteLength`, the storage object MUST be created/extended to length `max(currentLength, offset + bytes.byteLength)`. Bytes in the gap `[currentLength, offset)` MUST be zero (`0x00`) on subsequent reads.
|
||||
|
||||
**Verified by:** `src/abstract/abstract.spec-conformance.test.ts` (`STORAGE-PARTIAL-WRITE-02`).
|
||||
|
||||
### STORAGE-PARTIAL-WRITE-03 — overlap is in-place overwrite
|
||||
|
||||
When `setPartial(key, offset, bytes)` is called on a key whose existing object overlaps the written range, bytes within `[offset, offset + bytes.byteLength)` MUST be overwritten with `bytes`. Bytes outside that range MUST be preserved exactly.
|
||||
|
||||
**Verified by:** `src/abstract/abstract.spec-conformance.test.ts` (`STORAGE-PARTIAL-WRITE-03`).
|
||||
|
||||
### STORAGE-PARTIAL-WRITE-04 — capabilities are explicit
|
||||
|
||||
`AbstractStorage` MUST expose a readonly `capabilities` property of shape `{ partialReads: boolean; partialWrites: boolean; nativeStreams: boolean; atomicWrites: boolean; contentAddressable: boolean }`. Defaults MUST be all `false`. Subclasses MUST override per native support. `MemoryStorage.capabilities.partialWrites` MUST be `true`.
|
||||
|
||||
**Verified by:** `src/abstract/abstract.spec-conformance.test.ts` (`STORAGE-PARTIAL-WRITE-04`); `src/memory/memory.spec-conformance.test.ts` (`STORAGE-PARTIAL-WRITE-04`).
|
||||
33
openspec/archive/2026-04-28-storage-set-partial/tasks.md
Normal file
33
openspec/archive/2026-04-28-storage-set-partial/tasks.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Tasks: storage-set-partial
|
||||
|
||||
## 1. Abstract API
|
||||
|
||||
- [x] 1.1 Add `abstract setPartial(key, offset, bytes, signal?): Promise<void>` to `AbstractStorage`.
|
||||
- [x] 1.2 Add `protected defaultSetPartial(...)` helper alongside `defaultGetPartial`.
|
||||
- [x] 1.3 Add required readonly `capabilities` property; default all flags `false`.
|
||||
|
||||
## 2. MemoryStorage native impl
|
||||
|
||||
- [x] 2.1 `MemoryStorage.setPartial(key, offset, bytes)` — resize backing `Uint8Array`, `arr.set(bytes, offset)`.
|
||||
- [x] 2.2 `capabilities = { partialReads: true, partialWrites: true, nativeStreams: true, atomicWrites: true, contentAddressable: false }`.
|
||||
- [x] 2.3 `NamespacedStorage.setPartial` delegates with prefixed key; capabilities mirror inner.
|
||||
|
||||
## 3. storage-s3 fallback wiring
|
||||
|
||||
- [x] 3.1 `S3Storage.setPartial = defaultSetPartial` via thin override.
|
||||
- [x] 3.2 `capabilities = { partialReads: <native if Range>, partialWrites: false, nativeStreams: true, atomicWrites: true, contentAddressable: false }`.
|
||||
|
||||
## 4. Spec
|
||||
|
||||
- [x] 4.1 Add `STORAGE-PARTIAL-WRITE-01` … `STORAGE-PARTIAL-WRITE-04` with `**Verified by:**` lines.
|
||||
|
||||
## 5. Conformance tests
|
||||
|
||||
- [x] 5.1 `src/abstract/abstract.spec-conformance.test.ts` — `-01` (signature), `-02` (sparse extension), `-03` (overlap overwrite), `-04` (capabilities default).
|
||||
- [x] 5.2 `src/memory/memory.spec-conformance.test.ts` — native `setPartial`, `partialWrites === true`.
|
||||
|
||||
## 6. Verify
|
||||
|
||||
- [x] 6.1 `bunx turbo lint typecheck test --filter=@suaveplan/storage` green.
|
||||
- [x] 6.2 `bunx turbo test:coverage --filter=@suaveplan/storage` 100/100/100/100.
|
||||
- [x] 6.3 `bun scripts/openspec-conformance.ts @suaveplan/storage` PASS.
|
||||
Reference in New Issue
Block a user