- BaseKVStorage / BaseEventStorage / BaseDocumentStorage define the API once; implementations provide only raw primitives (client and server share an API) - Drop KV indexes, query planner, unique indexes; document stores take index fields - Escape every char <= '/' so child keys stay contiguous; skip-scan is now simple - Event-sourced documents = event storage + any document store for projections - New folder layout (kv-storage, event-storage, document-storage, utils); tests only in test/ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
98 lines
4.2 KiB
TypeScript
98 lines
4.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import fc from 'fast-check'
|
|
import { Keys } from '../../src/utils/keys.js'
|
|
import { SortedMap } from '../../src/utils/sorted-map.js'
|
|
import { Ulid } from '../../src/utils/ulid.js'
|
|
import { DocumentQuery } from '../../src/utils/document-query.js'
|
|
import { DocumentUpdate } from '../../src/utils/document-update.js'
|
|
|
|
describe('Keys', () => {
|
|
it('round-trips any segment and never emits a character at or below the separator', () => {
|
|
fc.assert(
|
|
fc.property(fc.string({ unit: 'binary', minLength: 1 }), (s) => {
|
|
const escaped = Keys.escape(s)
|
|
expect(Keys.unescape(escaped)).toBe(s)
|
|
expect([...escaped].every((c) => c > '/')).toBe(true)
|
|
expect(Keys.isReserved(escaped)).toBe(false)
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('keeps every child subtree contiguous in key order', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.uniqueArray(fc.string({ unit: fc.constantFrom('a', '-', '.', '/', '~', ' ', '0'), minLength: 1, maxLength: 3 }), { minLength: 2 }),
|
|
(segments) => {
|
|
const keys = segments.flatMap((s) => [Keys.escape(s), Keys.escape(s) + '/x']).sort()
|
|
const order = keys.map((k) => k.split('/')[0])
|
|
// Each segment's keys appear as one consecutive run.
|
|
expect(new Set(order).size).toBe(order.filter((s, i) => s !== order[i - 1]).length)
|
|
},
|
|
),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('SortedMap', () => {
|
|
it('matches a sorted reference, including iteration across mutations', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.array(fc.tuple(fc.boolean(), fc.string({ unit: fc.constantFrom('a', 'b', 'c'), maxLength: 4 })), { maxLength: 200 }),
|
|
fc.boolean(),
|
|
(ops, reverse) => {
|
|
const map = new SortedMap<number>(4)
|
|
const ref = new Set<string>()
|
|
for (const [add, key] of ops) add ? (map.set(key, 1), ref.add(key)) : (map.delete(key), ref.delete(key))
|
|
const sorted = [...ref].sort()
|
|
expect([...map.keys(undefined, undefined, reverse)]).toEqual(reverse ? sorted.reverse() : sorted)
|
|
// Deleting each key as we pass it must not skip the rest.
|
|
const seen = [...map.keys()].length
|
|
let visited = 0
|
|
for (const key of map.keys()) (visited++, map.delete(key))
|
|
expect(visited).toBe(seen)
|
|
},
|
|
),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('Ulid', () => {
|
|
it('is strictly increasing within a millisecond and sorts by time', () => {
|
|
let t = 1_700_000_000_000
|
|
const ulid = new Ulid(() => t)
|
|
const ids = Array.from({ length: 500 }, () => ulid.generate())
|
|
expect([...ids].sort()).toEqual(ids)
|
|
t++
|
|
expect(ulid.generate() > ids.at(-1)!).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('DocumentQuery', () => {
|
|
const q = (filter: object, doc: object) => new DocumentQuery(filter as never).matches(doc as never)
|
|
it('matches equality, comparisons, $in, dot paths, arrays and null-as-missing', () => {
|
|
expect(q({ a: 1 }, { a: 1 })).toBe(true)
|
|
expect(q({ 'a.b': 'x' }, { a: { b: 'x' } })).toBe(true)
|
|
expect(q({ n: { $gte: 2, $lt: 5 } }, { n: 4 })).toBe(true)
|
|
expect(q({ n: { $gt: 2 } }, { n: '9' })).toBe(false)
|
|
expect(q({ n: { $in: [1, 2] } }, { n: 2 })).toBe(true)
|
|
expect(q({ tags: 'x' }, { tags: ['y', 'x'] })).toBe(true)
|
|
expect(q({ tags: ['x'] }, { tags: ['x'] })).toBe(true)
|
|
expect(q({ a: null }, {})).toBe(true)
|
|
expect(() => q({ a: { $regex: 'x' } }, {})).toThrow(/Unsupported/)
|
|
})
|
|
})
|
|
|
|
describe('DocumentUpdate', () => {
|
|
it('resolves operators to final values', () => {
|
|
const state = { n: 5, tags: ['a', 'b'], nested: { x: 1 } }
|
|
const change = DocumentUpdate.resolve(state, { $inc: { n: 1 }, $push: { tags: { $each: ['c'] } }, $unset: { 'nested.x': 1 }, $set: { 'nested.y': 2 } })
|
|
expect(change).toEqual({ op: 'patch', set: { n: 6, tags: ['a', 'b', 'c'], 'nested.y': 2 }, unset: ['nested.x'] })
|
|
expect(DocumentUpdate.apply(state, change)).toEqual({ n: 6, tags: ['a', 'b', 'c'], nested: { y: 2 } })
|
|
expect(DocumentUpdate.apply(state, DocumentUpdate.resolve(state, { $pull: { tags: 'a' }, $addToSet: { more: { $each: [1, 1] } } }))).toMatchObject({
|
|
tags: ['b'],
|
|
more: [1],
|
|
})
|
|
expect(() => DocumentUpdate.resolve(state, { $set: { n: 1 }, $inc: { n: 1 } })).toThrow(/twice/)
|
|
})
|
|
})
|