Add encryption, Chromium tests and a README
CI / Node 24 (push) Successful in 15s
CI / Node 26 (push) Successful in 16s
CI / Node 22 (pull_request) Successful in 13s
CI / Node 24 (pull_request) Failing after 3s
CI / Node 26 (pull_request) Failing after 4s
CI / Chromium (push) Failing after 1m8s
CI / Publish to staging registry (push) Skipped
CI / Chromium (pull_request) Failing after 1m3s
CI / Publish to staging registry (pull_request) Skipped
CI / Node 22 (push) Failing after 4s

- KVStorageEncrypted: AES-GCM per value (fresh IV, storage key as AAD), keys stay
  readable; decrypts and re-emits pushed changes and passes subscriptions through, so it
  can be a composite source. generateKey / exportKey / importKey / deriveKey (PBKDF2).
- KVStorageHttpServer: allowOrigins (CORS), including cache-control for SSE preflights.
- Chromium tests (Vitest browser mode + Playwright) against a real cross-origin server:
  conformance for IndexedDB, encrypted, HTTP and composite stores; signed requests and
  SSE; two devices syncing, offline writes surviving a reload. New CI job.
- Composite fixes found by those tests: sync() during a running sync schedules one more
  (changes between the first scan and the stream connecting were missed); a queued write
  the source refuses is dropped, restored and reported (onError) instead of blocking the
  queue forever.
- README with examples, mirrored in test/readme.test.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
This commit is contained in:
Claude
2026-10-23 14:25:22 +00:00
parent 3525dbc622
commit 68d1a1c8e9
21 changed files with 1387 additions and 24 deletions
+32 -1
View File
@@ -41,7 +41,7 @@ describe('KVStorageComposite sync', () => {
network.store.reset()
expect(await db.child('users').get('1')).toEqual({ name: 'Ada' })
expect(await collect(db.child('users').keys())).toEqual(['1', '2'])
expect(network.store.calls).toEqual({})
expect(network.store.calls.readMany).toBeUndefined() // (a follow-up sync started on subscribe may still scan)
})
it('applies changes pushed from other clients to every replica', async () => {
@@ -226,4 +226,35 @@ describe('KVStorageComposite sync', () => {
['c', 3],
])
})
it('sync() during a running sync runs one more, so changes after its scan are picked up', async () => {
let release!: () => void
let reached!: () => void
const gate = new Promise<void>((r) => (release = r))
const paused = new Promise<void>((r) => (reached = r))
const inner = new KVStorageInMemory()
await inner.writeMany([{ key: 'a', value: 1 }])
const source = new KVStorageCounting(inner)
const scan = source.scan.bind(source)
let scans = 0
source.scan = async function* (options) {
const entries = await collect(scan(options)) // a snapshot, like a read-ahead stream
if (options?.values && scans++ === 0) (reached(), await gate)
yield* entries
}
const db = new KVStorageComposite({ source, replicas: [new KVStorageInMemory()] })
composites.push(db)
const first = db.sync()
await paused
await inner.writeMany([{ key: 'b', value: 2 }]) // after the running sync's snapshot
const second = db.sync()
expect(db.sync()).toBe(second) // later callers share the pending one
release()
await first
await second
expect(await collect(db.scan({ values: true }))).toEqual([
['a', 1],
['b', 2],
])
})
})