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
68 lines
3.9 KiB
TypeScript
68 lines
3.9 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
import { KVStorageHttp } from '../../src/kv-storage/kv-storage-http.js'
|
|
import { KVStorageHttpServer } from '../../src/kv-storage/kv-storage-http-server.js'
|
|
import { KVStorageInMemory } from '../../src/kv-storage/kv-storage-in-memory.js'
|
|
import { EventStorageKV } from '../../src/event-storage/event-storage-kv.js'
|
|
import { DocumentStorageKV } from '../../src/document-storage/document-storage-kv.js'
|
|
import { EventSourcedDocumentStorage } from '../../src/document-storage/event-sourced-document-storage.js'
|
|
import { runKVStorageConformance } from './kv-storage.conformance.js'
|
|
import { serve } from './node-server.js'
|
|
|
|
runKVStorageConformance('KVStorageHttp (in-process)', () => {
|
|
const server = new KVStorageHttpServer(new KVStorageInMemory(), { auth: 'none', basePath: '/kv' })
|
|
return new KVStorageHttp({ url: 'http://test/kv', fetch: async (input, init) => server.handle(new Request(input, init)) })
|
|
})
|
|
|
|
describe('over a real socket', () => {
|
|
let server: Awaited<ReturnType<typeof serve>>
|
|
const stores: KVStorageHttpServer[] = []
|
|
beforeAll(async () => {
|
|
// One socket; each test storage gets its own backend under /<n>.
|
|
server = await serve((request) => stores[Number(new URL(request.url).pathname.split('/')[1])]!.handle(request))
|
|
})
|
|
afterAll(() => server.close())
|
|
const fresh = () => {
|
|
stores.push(new KVStorageHttpServer(new KVStorageInMemory(), { auth: 'none', basePath: `/${stores.length}` }))
|
|
return new KVStorageHttp({ url: `${server.url}/${stores.length - 1}` })
|
|
}
|
|
|
|
runKVStorageConformance('KVStorageHttp (node:http)', fresh)
|
|
|
|
it('stops a long scan early', async () => {
|
|
const db = fresh()
|
|
await db.writeMany(Array.from({ length: 5000 }, (_, i) => ({ key: `k${String(i).padStart(5, '0')}`, value: i })))
|
|
const seen: string[] = []
|
|
for await (const [key] of db.scan()) if (seen.push(key) === 3) break
|
|
expect(seen).toEqual(['k00000', 'k00001', 'k00002'])
|
|
})
|
|
|
|
it('runs events and event-sourced documents over HTTP', async () => {
|
|
const kv = fresh()
|
|
const docs = new EventSourcedDocumentStorage(new EventStorageKV(kv), new DocumentStorageKV(kv.child('projections')))
|
|
const id = await docs.child('people').insert({ email: 'a@x', n: 1 })
|
|
await docs.child('people').update(id, { $inc: { n: 1 } })
|
|
expect(await docs.child('people').findOne({ email: 'a@x' })).toEqual({ key: id, data: { email: 'a@x', n: 2 } })
|
|
})
|
|
})
|
|
|
|
describe('server', () => {
|
|
it('maps errors to 404 / 500', async () => {
|
|
const server = new KVStorageHttpServer(new KVStorageInMemory(), { auth: 'none' })
|
|
expect((await server.handle(new Request('http://x/nope', { method: 'POST', body: '{}' }))).status).toBe(404)
|
|
expect((await server.handle(new Request('http://x/readMany', { method: 'POST', body: '{"keys": 5}' }))).status).toBe(500)
|
|
})
|
|
|
|
it('answers CORS preflights and allows only listed origins', async () => {
|
|
const server = new KVStorageHttpServer(new KVStorageInMemory(), { auth: 'none', allowOrigins: ['https://app.example'] })
|
|
const preflight = await server.handle(new Request('http://x/write', { method: 'OPTIONS', headers: { origin: 'https://app.example' } }))
|
|
expect(preflight.status).toBe(204)
|
|
expect(preflight.headers.get('access-control-allow-origin')).toBe('https://app.example')
|
|
expect(preflight.headers.get('access-control-allow-headers')).toContain('X-Signature')
|
|
expect(preflight.headers.get('access-control-allow-headers')).toContain('cache-control') // sent by SSESession
|
|
const allowed = await server.handle(new Request('http://x/readMany', { method: 'POST', body: '{"keys":[]}', headers: { origin: 'https://app.example' } }))
|
|
expect(allowed.headers.get('access-control-allow-origin')).toBe('https://app.example')
|
|
const other = await server.handle(new Request('http://x/readMany', { method: 'POST', body: '{"keys":[]}', headers: { origin: 'https://evil.example' } }))
|
|
expect(other.headers.get('access-control-allow-origin')).toBeNull()
|
|
})
|
|
})
|