Simplify: one abstract base class per layer
- 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
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { bench, describe } from 'vitest'
|
||||
import { DocumentStorageInMemory } from '../src/document-storage/document-storage-in-memory.js'
|
||||
import { EventSourcedDocumentStorage } from '../src/document-storage/event-sourced-document-storage.js'
|
||||
import { EventStorageKV } from '../src/event-storage/event-storage-kv.js'
|
||||
import { KVStorageInMemory } from '../src/kv-storage/kv-storage-in-memory.js'
|
||||
import type { BaseDocumentStorage } from '../src/document-storage/base-document-storage.js'
|
||||
|
||||
const N = 10_000
|
||||
const stores: [string, BaseDocumentStorage][] = [
|
||||
['in-memory', new DocumentStorageInMemory({ indexes: { users: ['age'] } })],
|
||||
['event-sourced', new EventSourcedDocumentStorage(new EventStorageKV(new KVStorageInMemory()), new DocumentStorageInMemory({ indexes: { users: ['age'] } }))],
|
||||
]
|
||||
for (const [name, docs] of stores) {
|
||||
const users = docs.child('users')
|
||||
for (let i = 0; i < N; i++) await users.set(`u${i}`, { name: `user ${i}`, age: i % 100, city: `c${i % 50}` })
|
||||
let i = 0
|
||||
describe(`documents (${name}) @ 10k`, () => {
|
||||
bench('get', async () => {
|
||||
await users.get(`u${i++ % N}`)
|
||||
})
|
||||
bench('update ($inc)', async () => {
|
||||
await users.update(`u${i++ % N}`, { $inc: { logins: 1 } })
|
||||
})
|
||||
bench('find by indexed field (~100 hits)', async () => {
|
||||
for await (const _ of users.find({ age: i++ % 100 })) {
|
||||
}
|
||||
})
|
||||
bench('find by unindexed field (scan, ~200 hits)', async () => {
|
||||
for await (const _ of users.find({ city: `c${i++ % 50}` })) {
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { bench, describe } from 'vitest'
|
||||
import { InMemoryKVDriver } from '../src/kv/memory.js'
|
||||
import { KVStorage } from '../src/kv/node.js'
|
||||
import { KVEventStorage } from '../src/events/kv.js'
|
||||
import { EventSourcedDocumentStorage } from '../src/documents/event-sourced.js'
|
||||
|
||||
const N = 10_000
|
||||
const docs = new EventSourcedDocumentStorage(new KVEventStorage(new KVStorage(new InMemoryKVDriver())), {
|
||||
indexes: { users: [{ fields: ['age'] }, { fields: ['email'], unique: true }] },
|
||||
warnOnFullScan: false,
|
||||
})
|
||||
const users = docs.child('users')
|
||||
for (let i = 0; i < N; i++) await users.set(`u${i}`, { name: `user ${i}`, email: `u${i}@x.io`, age: i % 100, city: `c${i % 50}` })
|
||||
let i = 0
|
||||
|
||||
describe(`documents @ ${N / 1000}k docs`, () => {
|
||||
bench('get', async () => {
|
||||
await users.get(`u${i++ % N}`)
|
||||
})
|
||||
bench('update ($inc, indexed node)', async () => {
|
||||
await users.update(`u${i++ % N}`, { $inc: { logins: 1 } })
|
||||
})
|
||||
bench('findOne by unique index (email)', async () => {
|
||||
await users.findOne({ email: `u${i++ % N}@x.io` })
|
||||
})
|
||||
bench('find indexed (age = k, ~100 hits)', async () => {
|
||||
for await (const _ of users.find({ age: i++ % 100 })) {
|
||||
}
|
||||
})
|
||||
bench('find full scan (city = k, ~200 hits)', async () => {
|
||||
for await (const _ of users.find({ city: `c${i++ % 50}` })) {
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { bench, describe } from 'vitest'
|
||||
import { EventStorageKV } from '../src/event-storage/event-storage-kv.js'
|
||||
import { KVStorageInMemory } from '../src/kv-storage/kv-storage-in-memory.js'
|
||||
|
||||
const events = new EventStorageKV(new KVStorageInMemory())
|
||||
const streams = events.child('streams')
|
||||
for (let i = 0; i < 10_000; i++) await streams.append('long', [{ i }])
|
||||
let s = 0
|
||||
|
||||
describe('events', () => {
|
||||
bench('append 1 event (warm cache)', async () => {
|
||||
await streams.append(`s${s++ % 1000}`, [{ type: 'e' }])
|
||||
})
|
||||
bench('read 100 events from version 5000 of 10k', async () => {
|
||||
for await (const _ of streams.read('long', { from: 5000, limit: 100 })) {
|
||||
}
|
||||
})
|
||||
bench('read full 10k-event stream', async () => {
|
||||
for await (const _ of streams.read('long')) {
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,27 +0,0 @@
|
||||
import { bench, describe } from 'vitest'
|
||||
import { InMemoryKVDriver } from '../src/kv/memory.js'
|
||||
import { KVStorage } from '../src/kv/node.js'
|
||||
import { KVEventStorage } from '../src/events/kv.js'
|
||||
|
||||
const events = new KVEventStorage(new KVStorage(new InMemoryKVDriver()))
|
||||
const streams = events.child('streams')
|
||||
const long = streams.child('long')
|
||||
for (let i = 0; i < 10_000; i++) await long.append([{ type: 'tick', i }])
|
||||
let s = 0
|
||||
|
||||
describe('events', () => {
|
||||
bench('append 1 event (warm head cache)', async () => {
|
||||
await streams.append(`s${s++ % 1000}`, [{ type: 'e' }])
|
||||
})
|
||||
bench('read 100 events from version 5000 of 10k', async () => {
|
||||
for await (const _ of long.read({ from: 5000, limit: 100 })) {
|
||||
}
|
||||
})
|
||||
bench('read full 10k-event stream', async () => {
|
||||
for await (const _ of long.read()) {
|
||||
}
|
||||
})
|
||||
bench('version() (cold head lookup)', async () => {
|
||||
await long.version()
|
||||
})
|
||||
})
|
||||
@@ -1,26 +0,0 @@
|
||||
import { bench, describe } from 'vitest'
|
||||
import { InMemoryKVDriver } from '../src/kv/memory.js'
|
||||
import { KVStorage } from '../src/kv/node.js'
|
||||
|
||||
// 1k children, each with 100 descendants (100k keys under the node).
|
||||
const db = new KVStorage(new InMemoryKVDriver({ clone: false }))
|
||||
const node = db.child('n')
|
||||
const writes: [string, number][] = []
|
||||
for (let c = 0; c < 1000; c++) {
|
||||
writes.push([`n/c${c}`, c])
|
||||
for (let d = 0; d < 100; d++) writes.push([`n/c${c}/d${d}`, d])
|
||||
}
|
||||
await db.driver.setMany(writes)
|
||||
|
||||
describe('keys() on 1k children x 100 descendants', () => {
|
||||
bench('keys() skip-scan', async () => {
|
||||
let n = 0
|
||||
for await (const _ of node.keys()) n++
|
||||
if (n !== 1000) throw new Error(`expected 1000, got ${n}`)
|
||||
})
|
||||
bench('naive full subtree scan (reference)', async () => {
|
||||
let n = 0
|
||||
for await (const _ of db.driver.scan({ gte: 'n/', lt: 'n0' })) n++
|
||||
if (n !== 101_000) throw new Error(`expected 101000, got ${n}`)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { bench, describe } from 'vitest'
|
||||
import { KVStorageInMemory } from '../src/kv-storage/kv-storage-in-memory.js'
|
||||
|
||||
for (const n of [10_000, 100_000]) {
|
||||
const db = new KVStorageInMemory()
|
||||
await db.writeMany(Array.from({ length: n }, (_, i) => [`users/u${i}`, { name: `user ${i}`, n: i }]))
|
||||
const users = db.child('users')
|
||||
const keys = Array.from({ length: 100 }, (_, i) => `u${(i * 7919) % n}`)
|
||||
let i = 0
|
||||
describe(`KV @ ${n / 1000}k keys`, () => {
|
||||
bench('get', async () => {
|
||||
await users.get(`u${i++ % n}`)
|
||||
})
|
||||
bench('set', async () => {
|
||||
await users.set(`u${i++ % n}`, { name: 'x', n: i })
|
||||
})
|
||||
bench('getMany x100', async () => {
|
||||
await users.getMany(keys)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const tree = new KVStorageInMemory()
|
||||
const writes: [string, number][] = []
|
||||
for (let c = 0; c < 1000; c++) {
|
||||
writes.push([`n/c${c}`, c])
|
||||
for (let d = 0; d < 100; d++) writes.push([`n/c${c}/d${d}`, d])
|
||||
}
|
||||
await tree.writeMany(writes)
|
||||
|
||||
describe('keys() on 1k children x 100 descendants', () => {
|
||||
bench('keys() skip-scan', async () => {
|
||||
for await (const _ of tree.child('n').keys()) {
|
||||
}
|
||||
})
|
||||
bench('full subtree scan (reference)', async () => {
|
||||
for await (const _ of tree.scan({ gte: 'n/', lt: 'n0' })) {
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,29 +0,0 @@
|
||||
import { bench, describe } from 'vitest'
|
||||
import { InMemoryKVDriver } from '../src/kv/memory.js'
|
||||
import { KVStorage } from '../src/kv/node.js'
|
||||
|
||||
async function populated(n: number) {
|
||||
const db = new KVStorage(new InMemoryKVDriver())
|
||||
const users = db.child('users')
|
||||
const batch: [string, { name: string; n: number }][] = []
|
||||
for (let i = 0; i < n; i++) batch.push([`u${i}`, { name: `user ${i}`, n: i }])
|
||||
await users.setMany(batch)
|
||||
return users
|
||||
}
|
||||
|
||||
for (const n of [10_000, 100_000]) {
|
||||
const users = await populated(n)
|
||||
const keys = Array.from({ length: 100 }, (_, i) => `u${(i * 7919) % n}`)
|
||||
let i = 0
|
||||
describe(`KV @ ${n / 1000}k keys`, () => {
|
||||
bench('get', async () => {
|
||||
await users.get(`u${i++ % n}`)
|
||||
})
|
||||
bench('set (overwrite)', async () => {
|
||||
await users.set(`u${i++ % n}`, { name: 'x', n: i })
|
||||
})
|
||||
bench('getMany x100', async () => {
|
||||
await users.getMany(keys)
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user