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 { 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 { /** 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 abstract readFrom(stream: readonly string[], options: ReadOptions): AsyncIterable> abstract versionOf(stream: readonly string[]): Promise abstract streamsOf(parent: readonly string[], options: StreamKeysOptions): AsyncIterable child>(key: K): BaseEventStorage> { const node = Object.create(this) as BaseEventStorage> node.path = [...this.path, Keys.segment(key)] return node } as(): BaseEventStorage { return this as unknown as BaseEventStorage } /** `append(events, options?)` on this stream; `append(key, events, options?)` on a child stream. */ append>( ...args: [events: ValueOf[], options?: AppendOptions] | [key: K, events: ValueOf>[], options?: AppendOptions] ): Promise { 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>(...args: [options?: ReadOptions] | [key: K, options?: ReadOptions]): AsyncGenerator>> { 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>> } /** Current version of this stream (or a child's); 0 when empty. */ version(key?: KeyOf): Promise { return this.withStream(arguments.length > 0, key, (stream) => this.versionOf(stream)) } /** Keys of child streams. */ keys(options: StreamKeysOptions = {}): AsyncIterable> { return this.streamsOf(this.path, options) as AsyncIterable> } private withStream(hasKey: boolean, key: unknown, fn: (stream: readonly string[]) => Promise): Promise { 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 } }