import { beforeEach, describe, expect, it } from 'vitest' import type { BaseKVStorage, ScanOptions } from '../../src/kv-storage/base-kv-storage.js' import { InvalidKeyError } from '../../src/utils/errors.js' import { collect } from '../helpers.js' /** Behaviour every KV storage must have. `make` returns an empty storage. */ export function runKVStorageConformance(name: string, make: () => BaseKVStorage | Promise): void { describe(`KV storage conformance: ${name}`, () => { let db: BaseKVStorage beforeEach(async () => { db = await make() }) const scanKeys = async (options: ScanOptions) => (await collect(db.scan(options))).map(([k]) => k) describe('primitives', () => { beforeEach(async () => { await db.writeMany([ ['b', 2], ['a/2', { n: 'a2' }], ['c', [3]], ['a', 1], ['a/1', 'a1'], ['a0', null], ['10', 10], ['9', 9], ]) }) it('reads in request order with undefined for missing keys and null preserved', async () => { expect(await db.readMany(['c', 'missing', 'a/2', 'a0'])).toEqual([[3], undefined, { n: 'a2' }, null]) }) it('scans in key order with bounds, reverse, limit and optional values', async () => { expect(await scanKeys({})).toEqual(['10', '9', 'a', 'a/1', 'a/2', 'a0', 'b', 'c']) expect(await scanKeys({ gte: 'a/', lt: 'a0' })).toEqual(['a/1', 'a/2']) expect(await scanKeys({ reverse: true, gte: 'a', lt: 'b' })).toEqual(['a0', 'a/2', 'a/1', 'a']) expect(await scanKeys({ limit: 2, reverse: true })).toEqual(['c', 'b']) expect(await scanKeys({ limit: 0 })).toEqual([]) expect(await collect(db.scan({ gte: 'a/', lt: 'a0', values: true }))).toEqual([ ['a/1', 'a1'], ['a/2', { n: 'a2' }], ]) expect((await collect(db.scan({ gte: 'b' })))[0]).toEqual(['b', undefined]) }) it('removes keys', async () => { await db.removeMany(['b', 'missing']) expect(await db.readMany(['b', 'c'])).toEqual([undefined, [3]]) }) it('writeIfAbsent writes only absent keys, and only one concurrent claim wins', async () => { expect( await db.writeIfAbsent([ ['a', 'x'], ['new', 'y'], ]), ).toEqual([false, true]) expect(await db.readMany(['a', 'new'])).toEqual([1, 'y']) const results = await Promise.all([1, 2, 3].map((n) => db.writeIfAbsent([['claim', n]]))) expect(results.flat().filter(Boolean)).toHaveLength(1) }) it('keeps scanning correctly when keys change mid-scan', async () => { // Keys removed mid-scan may or may not still appear (stores may read ahead), but every key // present throughout must appear exactly once, in order. const seen: string[] = [] for await (const [key] of db.scan({})) { seen.push(key) if (key === 'a') await db.removeMany(['a', 'a/1']) } expect(seen.filter((k) => k !== 'a/1')).toEqual(['10', '9', 'a', 'a/2', 'a0', 'b', 'c']) }) it('does not share references with callers', async () => { const [value] = (await db.readMany(['a/2'])) as [{ n: string }] value.n = 'changed' expect(await db.readMany(['a/2'])).toEqual([{ n: 'a2' }]) }) }) describe('nodes', () => { it('reads and writes own and child values at the same key', async () => { const users = db.child('users') await users.set('123', { name: 'Ada' }) expect(await users.child('123').get()).toEqual({ name: 'Ada' }) await users.child('123').set({ name: 'Bob' }) expect(await users.get('123')).toEqual({ name: 'Bob' }) expect(await db.readMany(['users/123'])).toEqual([{ name: 'Bob' }]) await db.child('n').set(5, 'five') expect(await db.child('n').get('5')).toBe('five') }) it('batch reads, writes and deletes children', async () => { const n = db.child('n') await n.setMany([ ['a', 1], ['b', 2], ]) expect(await n.getMany(['b', 'x', 'a'])).toEqual([2, undefined, 1]) await n.deleteMany(['a']) expect(await n.getMany(['a', 'b'])).toEqual([undefined, 2]) }) it('keeps own values and children independent', async () => { const user = db.child('users').child('1') await user.set('own') await user.set('name', 'Ada') await user.delete() expect(await user.get()).toBeUndefined() expect(await user.get('name')).toBe('Ada') }) it('rejects passed-but-undefined keys, root values and undefined values', async () => { const users = db.child('users') const id: string | undefined = undefined await expect(users.get(id as never)).rejects.toThrow(InvalidKeyError) await expect(users.delete(null as never)).rejects.toThrow(TypeError) await expect(users.set(id as never, 1 as never)).rejects.toThrow(TypeError) await expect(users.set('x', undefined)).rejects.toThrow(/undefined/) await expect(db.get()).rejects.toThrow(/root/) expect(() => db.child('')).toThrow(InvalidKeyError) }) it('escapes keys so special characters round-trip', async () => { const n = db.child('a/b') for (const key of ['x/y', '50%', '~e', 'a-1', 'a.b', 'a b', 'é']) await n.set(key, key) for (const key of ['x/y', '50%', '~e', 'a-1', 'a.b', 'a b', 'é']) expect(await n.get(key)).toBe(key) expect((await collect(n.keys())).sort()).toEqual(['50%', 'a b', 'a-1', 'a.b', 'x/y', '~e', 'é'].sort()) }) it('lists direct children (including ones with only descendants), skipping reserved keys', async () => { const n = db.child('n') await n.set('a', 1) await n.child('b').set('deep', 1) await n.set('c', 3) await n.child('c').child('x').set('y', 1) await n.set('a-1', 4) await db.writeMany([['n/~e/000000000001', {}]]) expect(await collect(n.keys())).toEqual(['a', 'a-1', 'b', 'c']) expect(await collect(n.keys({ reverse: true }))).toEqual(['c', 'b', 'a-1', 'a']) expect(await collect(n.keys({ after: 'a', limit: 2 }))).toEqual(['a-1', 'b']) expect(await collect(n.keys({ after: 'c', reverse: true }))).toEqual(['b', 'a-1', 'a']) expect(await collect(n.entries())).toEqual([ ['a', 1], ['a-1', 4], ['c', 3], ]) expect(await collect(n.entries({ reverse: true, limit: 2 }))).toEqual([ ['c', 3], ['a-1', 4], ]) expect(await collect(db.keys())).toEqual(['n']) }) it('deleteTree removes a subtree but not lookalike siblings', async () => { const n = db.child('n') await n.set('a', 1) await n.child('a').child('b').set('c', 3) await n.set('a!', 2) await n.set('a0', 3) await n.deleteTree('a') expect(await collect(n.keys())).toEqual(['a0', 'a!']) // 'a!' is stored as 'a~21' await db.deleteTree() expect(await collect(db.scan())).toEqual([]) }) }) }) }