Files
PotentialStorage/src/event-storage/base-event-storage.ts
T
Claude 08fb78721f
CI / Node 20 (pull_request) Successful in 10s
CI / Node 22 (pull_request) Successful in 11s
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
2026-10-23 08:05:03 +00:00

101 lines
3.9 KiB
TypeScript

import { Keys } from '../utils/keys.js'
import type { AnySpec, ChildOf, KeyOf, KeyString, Spec, ValueAt, ValueOf } from '../utils/schema.js'
export interface AppendOptions {
/** Version the stream must be at (0 = empty). Default `'any'`. */
expectedVersion?: number | 'any'
}
export interface AppendResult {
/** Stream version after the append. */
version: number
}
export interface ReadOptions {
/** First version to read (inclusive). Default 1. */
from?: number
/** Last version to read (inclusive). Default: the head. */
to?: number
limit?: number
}
export interface StoredEvent<E> {
version: number
/** Append timestamp (ms since epoch). */
at: number
data: E
}
export interface StreamKeysOptions {
limit?: number
after?: string | number
reverse?: boolean
}
/**
* Append-only event streams. Every node is a stream (its own events) and a parent of child
* streams. Implementations provide four primitives over stream paths; the user-facing,
* self-or-child API is defined here once.
*/
export abstract class BaseEventStorage<S extends Spec = AnySpec> {
/** Unescaped path of this node (`[]` for the root, which is not a stream). */
path: readonly string[] = []
abstract appendTo(stream: readonly string[], events: unknown[], expectedVersion: number | 'any'): Promise<AppendResult>
abstract readFrom(stream: readonly string[], options: ReadOptions): AsyncIterable<StoredEvent<unknown>>
abstract versionOf(stream: readonly string[]): Promise<number>
abstract streamsOf(parent: readonly string[], options: StreamKeysOptions): AsyncIterable<string>
child<K extends KeyOf<S>>(key: K): BaseEventStorage<ChildOf<S, K>> {
const node = Object.create(this) as BaseEventStorage<ChildOf<S, K>>
node.path = [...this.path, Keys.segment(key)]
return node
}
as<S2 extends Spec>(): BaseEventStorage<S2> {
return this as unknown as BaseEventStorage<S2>
}
/** `append(events, options?)` on this stream; `append(key, events, options?)` on a child stream. */
append<K extends KeyOf<S>>(
...args: [events: ValueOf<S>[], options?: AppendOptions] | [key: K, events: ValueOf<ChildOf<S, K>>[], options?: AppendOptions]
): Promise<AppendResult> {
const hasKey = !Array.isArray(args[0])
const events = args[hasKey ? 1 : 0]
const options = (args[hasKey ? 2 : 1] ?? {}) as AppendOptions
if (!Array.isArray(events)) return Promise.reject(new TypeError('append() expects an array of events'))
return this.withStream(hasKey, args[0], (stream) => this.appendTo(stream, events, options.expectedVersion ?? 'any'))
}
/** `read(options?)` this stream; `read(key, options?)` a child stream. */
async *read<K extends KeyOf<S>>(...args: [options?: ReadOptions] | [key: K, options?: ReadOptions]): AsyncGenerator<StoredEvent<ValueAt<S, K>>> {
const hasKey = typeof args[0] !== 'object' && args.length > 0
const stream = this.streamPath(hasKey, args[0])
yield* this.readFrom(stream, ((hasKey ? args[1] : args[0]) ?? {}) as ReadOptions) as AsyncIterable<StoredEvent<ValueAt<S, K>>>
}
/** Current version of this stream (or a child's); 0 when empty. */
version(key?: KeyOf<S>): Promise<number> {
return this.withStream(arguments.length > 0, key, (stream) => this.versionOf(stream))
}
/** Keys of child streams. */
keys(options: StreamKeysOptions = {}): AsyncIterable<KeyString<S>> {
return this.streamsOf(this.path, options) as AsyncIterable<KeyString<S>>
}
private withStream<T>(hasKey: boolean, key: unknown, fn: (stream: readonly string[]) => Promise<T>): Promise<T> {
try {
return fn(this.streamPath(hasKey, key))
} catch (error) {
return Promise.reject(error)
}
}
private streamPath(hasKey: boolean, key: unknown): readonly string[] {
if (hasKey) return [...this.path, Keys.segment(key)]
if (this.path.length === 0) throw new TypeError('The root node is not a stream')
return this.path
}
}