Signed requests: X-Nonce header; composite: sync skips unconfirmed writes
CI / Node 22 (push) Successful in 13s
CI / Node 24 (push) Successful in 13s
CI / Node 26 (push) Successful in 14s
CI / Node 22 (pull_request) Successful in 12s
CI / Node 24 (pull_request) Successful in 13s
CI / Node 26 (pull_request) Successful in 14s
CI / Chromium (push) Successful in 46s
CI / Publish to staging registry (push) Successful in 9s
CI / Chromium (pull_request) Successful in 45s
CI / Publish to staging registry (pull_request) Skipped

- Requests carry a random X-Nonce, signed as ${timestamp}:${nonce}:${path}:${body}. The
  server rejects a nonce the same key already used within the timestamp window. Identical
  requests at the same moment (two tabs, parallel reads) are now distinct, replacing the
  per-process increasing-timestamp workaround.
- Composite: a sync no longer reverts keys with local writes the source hasn't confirmed
  yet (in flight or queued); they briefly disappeared when a sync's scan ran before the
  write reached the source. Pending keys are reloaded from a persistent queue on start.

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:50:16 +00:00
parent 7d755cecae
commit cb9ff514a0
6 changed files with 128 additions and 65 deletions
@@ -257,4 +257,29 @@ describe('KVStorageComposite sync', () => {
['b', 2],
])
})
it('a sync started while a write is on its way to the source does not undo it', async () => {
let release!: () => void
const gate = new Promise<void>((r) => (release = r))
const inner = new KVStorageInMemory()
const source = new KVStorageCounting(inner)
const writeMany = source.writeMany.bind(source)
source.writeMany = async (changes) => {
await gate // a slow request
return writeMany(changes)
}
const replica = new KVStorageInMemory()
const db = new KVStorageComposite({ source, replicas: [replica] })
composites.push(db)
await db.sync()
const writing = db.set('a', 1)
await vi.waitFor(async () => expect(await replica.readMany(['a'])).toEqual([1])) // applied locally, not yet on the source
const syncing = db.sync()
await new Promise((r) => setTimeout(r, 10))
expect(await replica.readMany(['a'])).toEqual([1]) // the sync skips the unconfirmed key
release()
await Promise.all([writing, syncing])
expect(await replica.readMany(['a'])).toEqual([1])
expect(await inner.readMany(['a'])).toEqual([1])
})
})