Files
PotentialStorage/test/utils/building-blocks.test.ts
T
Claude 1f82ec9110
CI / Node 22 (pull_request) Successful in 11s
CI / Node 24 (pull_request) Successful in 12s
CI / Node 26 (pull_request) Successful in 12s
Break KVStorageComposite into focused classes
- KVReplicaSet: seed, apply, local scan, diff against the source, sync meta
- KVOutbox: persisted offline queue with in-order replay (load() resumes it)
- utils: KeyRange (also used for server authorization), KeyCounter,
  SerialQueue (also used by KVStorageHttp and the event-sourced store),
  RetryTimer (separate timers for sync and replay; previously one shared
  timer could cancel the other's retry)

The composite now only orchestrates. Behaviour unchanged; new unit tests for
the building blocks and the outbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
2026-10-23 12:43:49 +00:00

90 lines
3.4 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import fc from 'fast-check'
import { KeyRange } from '../../src/utils/key-range.js'
import { KeyCounter } from '../../src/utils/key-counter.js'
import { SerialQueue } from '../../src/utils/serial-queue.js'
import { RetryTimer } from '../../src/utils/retry-timer.js'
import { Keys } from '../../src/utils/keys.js'
describe('KeyRange', () => {
it('contains a key and its subtree, not lookalikes', () => {
const range = new KeyRange('ab')
expect(['ab', 'ab/c', 'ab/c/d'].every((k) => range.contains(k))).toBe(true)
expect(['a', 'abc', 'ab0', 'ab~2D1', 'b'].some((k) => range.contains(k))).toBe(false)
expect(new KeyRange('').contains('anything')).toBe(true)
})
it('agrees with its scan bounds for escaped keys (property)', () => {
const path = fc.array(fc.string({ unit: fc.constantFrom('a', 'b', '-', '/', '~', '0', '.'), minLength: 1, maxLength: 3 }), { minLength: 1, maxLength: 3 })
fc.assert(
fc.property(path, path, (p, q) => {
const range = new KeyRange(Keys.join(p))
const key = Keys.join(q)
expect(range.contains(key)).toBe(key >= range.gte! && key < range.lt!)
}),
)
})
it('clamps and checks scan ranges', () => {
const range = new KeyRange('ab')
expect(range.clamp({ limit: 5 })).toEqual({ limit: 5, gte: 'ab', lt: 'ab0' })
expect(range.clamp({ gte: 'ab/x', lt: 'zz' })).toEqual({ gte: 'ab/x', lt: 'ab0' })
expect(range.clamp({ gte: 'b' })).toBeUndefined()
expect(range.covers('ab/', 'ab0')).toBe(true)
expect(range.covers('a', 'ab0')).toBe(false)
expect(range.covers(undefined, 'ab0')).toBe(false)
expect(new KeyRange('').covers(undefined, undefined)).toBe(true)
})
})
describe('KeyCounter', () => {
it('counts per key and in total', () => {
const counter = new KeyCounter()
counter.add(['a', 'a', 'b'])
expect(counter.size).toBe(3)
expect(counter.take('a')).toBe(true)
expect(counter.has('a')).toBe(true)
expect(counter.take('a')).toBe(true)
expect(counter.has('a')).toBe(false)
expect(counter.take('a')).toBe(false)
expect(counter.size).toBe(1)
counter.clear()
expect(counter.size).toBe(0)
})
})
describe('SerialQueue', () => {
it('runs tasks in order per key, keys independently, and survives failures', async () => {
const queue = new SerialQueue()
const log: string[] = []
const task = (name: string, ms: number) => () => new Promise<void>((r) => setTimeout(() => (log.push(name), r()), ms))
const a1 = queue.run(task('a1', 20), 'a')
const failed = queue.run(() => Promise.reject(new Error('boom')), 'a')
const a2 = queue.run(task('a2', 1), 'a')
const b1 = queue.run(task('b1', 1), 'b')
await expect(failed).rejects.toThrow('boom')
await Promise.all([a1, a2, b1])
expect(log).toEqual(['b1', 'a1', 'a2'])
await queue.idle('a')
})
})
describe('RetryTimer', () => {
it('replaces a pending retry and stops cleanly', () => {
vi.useFakeTimers()
const timer = new RetryTimer({ baseDelay: 10, maxDelay: 10 })
const first = vi.fn()
const second = vi.fn()
timer.schedule(first)
timer.schedule(second)
vi.advanceTimersByTime(20)
expect(first).not.toHaveBeenCalled()
expect(second).toHaveBeenCalledOnce()
timer.schedule(first)
timer.stop()
vi.advanceTimersByTime(20)
expect(first).not.toHaveBeenCalled()
vi.useRealTimers()
})
})