- Every KV storage emits change events (base class owns writeMany/removeMany/ writeIfAbsent; implementations provide protected apply* methods) - KVStorageHttpServer streams changes over SSE (GET /events) with a resumable history; KVStorageHttp.watch() subscribes and resumes via Last-Event-ID - KVStorageIndexedDB for local persistence - KVStorageComposite keeps replicas of a source in sync: local reads, resume instead of full sync, offline outbox replayed in order, echo tracking - Values may contain bigint/Uint8Array (Extended JSON over the network) - Vendor sse-session, event-emitter and extended-json (no runtime deps) - Freeze child nodes so shadowed state on children fails loudly - CI on Node 22, 24 and 26 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
59 lines
2.0 KiB
TypeScript
59 lines
2.0 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { EventEmitter } from '../../src/utils/event-emitter.js'
|
|
|
|
type Events = { tick: number; done: void }
|
|
|
|
describe('EventEmitter', () => {
|
|
it('delivers payloads as-is (no cloning or freezing) and supports off', () => {
|
|
const emitter = new EventEmitter<Events & { obj: { n: number } }>()
|
|
const seen: unknown[] = []
|
|
const payload = { n: 1 }
|
|
const off = emitter.on('obj', (p) => seen.push(p))
|
|
expect(emitter.emit('obj', payload)).toBe(true)
|
|
off()
|
|
expect(emitter.emit('obj', payload)).toBe(false)
|
|
expect(seen[0]).toBe(payload)
|
|
expect(Object.isFrozen(payload)).toBe(false)
|
|
})
|
|
|
|
it('once fires a single time; listener errors are logged, not thrown', () => {
|
|
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
const emitter = new EventEmitter<Events>()
|
|
const once = vi.fn()
|
|
emitter.once('tick', once)
|
|
emitter.on('tick', () => {
|
|
throw new Error('boom')
|
|
})
|
|
emitter.emit('tick', 1)
|
|
emitter.emit('tick', 2)
|
|
expect(once).toHaveBeenCalledTimes(1)
|
|
expect(error).toHaveBeenCalledTimes(2)
|
|
error.mockRestore()
|
|
})
|
|
|
|
it('debounces and cancels pending debounced calls on off', async () => {
|
|
vi.useFakeTimers()
|
|
const emitter = new EventEmitter<Events>()
|
|
const listener = vi.fn()
|
|
const off = emitter.on('tick', listener, 50)
|
|
emitter.emit('tick', 1)
|
|
emitter.emit('tick', 2)
|
|
vi.advanceTimersByTime(60)
|
|
expect(listener).toHaveBeenCalledExactlyOnceWith(2)
|
|
emitter.emit('tick', 3)
|
|
off()
|
|
vi.advanceTimersByTime(60)
|
|
expect(listener).toHaveBeenCalledTimes(1)
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('waitFor resolves on a matching payload and rejects on timeout', async () => {
|
|
const emitter = new EventEmitter<Events>()
|
|
const waiting = emitter.waitFor('tick', (n) => n > 1)
|
|
emitter.emit('tick', 1)
|
|
emitter.emit('tick', 2)
|
|
expect(await waiting).toBe(2)
|
|
await expect(emitter.waitFor('done', () => true, 5)).rejects.toThrow(/Timed out waiting for "done"/)
|
|
})
|
|
})
|