Files
Claude cb9ff514a0
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
Signed requests: X-Nonce header; composite: sync skips unconfirmed writes
- 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
2026-10-23 14:50:16 +00:00
..

import { afterEach, describe, expect, it } from 'vitest'
import {
  DocumentStorageKV,
  Ed25519Signer,
  Ed25519Verifier,
  EventStorageKV,
  KVStorageComposite,
  KVStorageEncrypted,
  KVStorageHttp,
  KVStorageHttpServer,
  KVStorageInMemory,
  type Fields,
  type Many,
} from '../src/index.js'
import { collect } from './helpers.js'

// The README's examples, run as written (imports from the package entry point), so they stay correct.

const closers: (() => Promise<void>)[] = []
afterEach(async () => {
  for (const close of closers.splice(0)) await close()
})

describe('README', () => {
  it('KV storage', async () => {
    const db = new KVStorageInMemory()
    const users = db.child('users')
    await users.set('1', { name: 'Ada' })
    expect(await users.child('1').get()).toEqual({ name: 'Ada' })
    expect(await users.get('1')).toEqual({ name: 'Ada' })
    await users.setMany([
      ['2', { name: 'Bob' }],
      ['3', { name: 'Cy' }],
    ])
    expect(await users.getMany(['1', 'missing'])).toEqual([{ name: 'Ada' }, undefined])
    expect(await collect(users.keys({ limit: 10 }))).toEqual(['1', '2', '3'])
    expect((await collect(users.entries({ reverse: true }))).map(([key]) => key)).toEqual(['3', '2', '1'])
    await users.delete('3')
    await users.deleteTree()
    expect(await collect(db.scan())).toEqual([])

    const settings = db.child('settings')
    await settings.setFields({ theme: 'dark', fontSize: 14 })
    expect(await settings.get('theme')).toBe('dark')
    expect(await settings.getFields()).toEqual({ fontSize: 14, theme: 'dark' })

    const changes: unknown[] = []
    db.on('change', ({ changes: c, origin }) => changes.push([origin, c]))
    await users.set('1', { name: 'Ada' })
    expect(changes).toEqual([['local', [{ key: 'users/1', value: { name: 'Ada' } }]]])
  })

  it('types', async () => {
    type User = { name: string }
    type Schema = { children: { users: Many<User>; settings: { children: Fields<{ theme: string; fontSize: number }> } } }
    const typed = new KVStorageInMemory<Schema>()
    await typed.child('users').set('1', { name: 'Ada' })
    // @ts-expect-error not a User
    await typed.child('users').set('2', { nom: 'x' })
    await typed.child('settings').set('theme', 'dark')
    const theme: string | undefined = await typed.child('settings').get('theme')
    expect(theme).toBe('dark')
  })

  it('event storage', async () => {
    const events = new EventStorageKV(new KVStorageInMemory())
    const orders = events.child('orders')
    const [id] = await orders.append('o1', [{ type: 'created', total: 30 }])
    await orders.child('o1').append([{ type: 'paid' }])
    expect((await collect(orders.read('o1'))).map((e) => e.data)).toEqual([{ type: 'created', total: 30 }, { type: 'paid' }])
    expect((await collect(orders.read('o1', { after: id }))).map((e) => e.data)).toEqual([{ type: 'paid' }])
    expect(await collect(orders.keys())).toEqual(['o1'])
  })

  it('document storage', async () => {
    const docs = new DocumentStorageKV(new KVStorageInMemory())
    const people = docs.child('people')
    const id = await people.insert({ name: 'Ada', age: 36, tags: ['math'] })
    await people.set('grace', { name: 'Grace', age: 45, tags: ['navy', 'cobol'] })
    await people.update(id, { $inc: { age: 1 }, $push: { tags: 'engines' } })
    expect((await collect(people.find({ age: { $gte: 37 } }, { sort: { age: -1 }, limit: 10 }))).map((r) => r.key)).toEqual(['grace', id])
    expect((await people.findOne({ tags: 'cobol' }))?.key).toBe('grace')
    expect(await people.get(id)).toEqual({ name: 'Ada', age: 37, tags: ['math', 'engines'] })
    expect(await people.delete('grace')).toBe(true)
  })

  it('sync, offline and encryption (server and client)', async () => {
    const server = new KVStorageHttpServer(new KVStorageInMemory(), {
      auth: { verifiers: [new Ed25519Verifier()] },
      basePath: '/kv',
      allowOrigins: ['https://app.example.com'],
    })
    const fetch = (url: string, init: RequestInit) => server.handle(new Request(url, init))

    const signer = await Ed25519Signer.generate()
    const saved = await signer.exportPrivateKey()
    expect((await Ed25519Signer.fromPrivateKey(saved)).publicKey).toBe(signer.publicKey)
    const salt = crypto.getRandomValues(new Uint8Array(16))
    const key = await KVStorageEncrypted.deriveKey('correct horse battery staple', salt, 1000)

    const local = new KVStorageInMemory() // stands in for KVStorageIndexedDB
    const db = new KVStorageComposite({
      source: new KVStorageEncrypted(new KVStorageHttp({ url: 'https://api.example.com/kv', signer, fetch }), { key }),
      replicas: [new KVStorageInMemory(), local],
      queue: local.child('~sync'),
      prefix: signer.publicKey,
    })
    closers.push(() => db.close())

    const me = db.child(signer.publicKey)
    await me.child('notes').set('1', { text: 'Works offline' })
    expect(await me.child('notes').get('1')).toEqual({ text: 'Works offline' })

    type Todo = { title: string; done: boolean }
    const todos = new DocumentStorageKV<{ children: { todos: Many<Todo> } }>(me).child('todos')
    await todos.insert({ title: 'Ship it', done: false })
    expect((await collect(todos.find({ done: false }))).map((r) => r.data.title)).toEqual(['Ship it'])

    // Another device with the same keys sees the data; the server only has ciphertext.
    const other = new KVStorageComposite({
      source: new KVStorageEncrypted(new KVStorageHttp({ url: 'https://api.example.com/kv', signer, fetch }), { key }),
      replicas: [new KVStorageInMemory()],
      prefix: signer.publicKey,
    })
    closers.push(() => other.close())
    expect(await other.child(signer.publicKey).child('notes').get('1')).toEqual({ text: 'Works offline' })

    const http = new KVStorageHttp({ url: 'https://api.example.com/kv', signer, fetch })
    closers.push(() => http.close())
    const [stored] = await http.readMany([`${signer.publicKey}/notes/1`])
    expect(stored).toBeInstanceOf(Uint8Array)
    const pushed = http.waitFor('change', () => true)
    await http.child(signer.publicKey).child('notes').subscribe()
    await me.child('notes').set('2', { text: 'live' })
    expect((await pushed).changes[0]!.key).toBe(`${signer.publicKey}/notes/2`)
  })
})