@durable-streams/state
Version:
State change event protocol for Durable Streams
284 lines (280 loc) • 9.46 kB
TypeScript
import { Collection, Collection as Collection$1, SyncConfig, and, avg, count, createCollection, createOptimisticAction, createOptimisticAction as createOptimisticAction$1, eq, gt, gte, ilike, inArray, isNull, isUndefined, like, lt, lte, max, min, not, or, sum } from "@tanstack/db";
import { DurableStream, DurableStreamOptions } from "@durable-streams/client";
import { StandardSchemaV1 } from "@standard-schema/spec";
//#region src/types.d.ts
/**
* Operation types for change events
*/
/**
* Operation types for change events
*/
type Operation = `insert` | `update` | `delete` | `upsert`;
/**
* A generic value type supporting primitives, arrays, and objects
*/
type Value<Extensions = never> = string | number | boolean | bigint | null | Array<Value<Extensions>> | {
[key: string]: Value<Extensions>;
} | Extensions;
/**
* A row is a record of values
*/
type Row<Extensions = never> = Record<string, Value<Extensions>>;
/**
* Headers for change messages
*/
type ChangeHeaders = {
operation: Operation;
txid?: string;
timestamp?: string;
};
/**
* A change event represents a state change event (insert/update/delete)
*/
type ChangeEvent<T = unknown> = {
type: string;
key: string;
value?: T;
old_value?: T;
headers: ChangeHeaders;
};
/**
* Control event types for stream management
*/
type ControlEvent = {
headers: {
control: `snapshot-start` | `snapshot-end` | `reset`;
offset?: string;
};
};
/**
* A state event is either a change event or a control event
*/
type StateEvent<T = unknown> = ChangeEvent<T> | ControlEvent;
/**
* Type guard to check if an event is a change event
*/
declare function isChangeEvent<T = unknown>(event: StateEvent<T>): event is ChangeEvent<T>;
/**
* Type guard to check if an event is a control event
*/
declare function isControlEvent<T = unknown>(event: StateEvent<T>): event is ControlEvent;
//#endregion
//#region src/materialized-state.d.ts
/**
* MaterializedState maintains an in-memory view of state from change events.
*
* It organizes data by type, where each type contains a map of key -> value.
* This supports multi-type streams where different entity types can coexist.
*/
declare class MaterializedState {
private data;
constructor();
/**
* Apply a single change event to update the materialized state
*/
apply(event: ChangeEvent): void;
/**
* Apply a batch of change events
*/
applyBatch(events: Array<ChangeEvent>): void;
/**
* Get a specific value by type and key
*/
get<T = unknown>(type: string, key: string): T | undefined;
/**
* Get all entries for a specific type
*/
getType(type: string): Map<string, unknown>;
/**
* Clear all state
*/
clear(): void;
/**
* Get the number of types in the state
*/
get typeCount(): number;
/**
* Get all type names
*/
get types(): Array<string>;
}
//#endregion
//#region src/stream-db.d.ts
/**
* Definition for a single collection in the stream state
*/
interface CollectionDefinition<T = unknown> {
/** Standard Schema for validating values */
schema: StandardSchemaV1<T>;
/** The type field value in change events that map to this collection */
type: string;
/** The property name in T that serves as the primary key */
primaryKey: string;
}
/**
* Helper methods for creating change events for a collection
*/
interface CollectionEventHelpers<T> {
/**
* Create an insert change event
*/
insert: (params: {
key?: string;
value: T;
headers?: Omit<Record<string, string>, `operation`>;
}) => ChangeEvent<T>;
/**
* Create an update change event
*/
update: (params: {
key?: string;
value: T;
oldValue?: T;
headers?: Omit<Record<string, string>, `operation`>;
}) => ChangeEvent<T>;
/**
* Create a delete change event
*/
delete: (params: {
key?: string;
oldValue?: T;
headers?: Omit<Record<string, string>, `operation`>;
}) => ChangeEvent<T>;
/**
* Create an upsert change event (insert or update)
*/
upsert: (params: {
key?: string;
value: T;
headers?: Omit<Record<string, string>, `operation`>;
}) => ChangeEvent<T>;
}
/**
* Collection definition enhanced with event creation helpers
*/
type CollectionWithHelpers<T = unknown> = CollectionDefinition<T> & CollectionEventHelpers<T>;
/**
* Stream state definition containing all collections
*/
type StreamStateDefinition = Record<string, CollectionDefinition>;
/**
* Stream state schema with helper methods for creating change events
*/
type StateSchema<T extends Record<string, CollectionDefinition>> = { [K in keyof T]: CollectionWithHelpers<T[K] extends CollectionDefinition<infer U> ? U : unknown> };
/**
* Definition for a single action that can be passed to createOptimisticAction
*/
interface ActionDefinition<TParams = any, TContext = any> {
onMutate: (params: TParams) => void;
mutationFn: (params: TParams, context: TContext) => Promise<any>;
}
/**
* Factory function for creating actions with access to db and stream context
*/
type ActionFactory<TDef extends StreamStateDefinition, TActions extends Record<string, ActionDefinition<any>>> = (context: {
db: StreamDB<TDef>;
stream: DurableStream;
}) => TActions;
/**
* Map action definitions to callable action functions
*/
type ActionMap<TActions extends Record<string, ActionDefinition<any>>> = { [K in keyof TActions]: ReturnType<typeof createOptimisticAction$1<any>> };
/**
* Options for creating a stream DB
*/
interface CreateStreamDBOptions<TDef extends StreamStateDefinition = StreamStateDefinition, TActions extends Record<string, ActionDefinition<any>> = Record<string, never>> {
/** Options for creating the durable stream (stream is created lazily on preload) */
streamOptions: DurableStreamOptions;
/** The stream state definition */
state: TDef;
/** Optional factory function to create actions with db and stream context */
actions?: ActionFactory<TDef, TActions>;
}
/**
* Extract the value type from a CollectionDefinition
*/
type ExtractCollectionType<T extends CollectionDefinition> = T extends CollectionDefinition<infer U> ? U : unknown;
/**
* Map collection definitions to TanStack DB Collection types
*/
type CollectionMap<TDef extends StreamStateDefinition> = { [K in keyof TDef]: Collection$1<ExtractCollectionType<TDef[K]> & object, string> };
/**
* The StreamDB interface - provides typed access to collections
*/
type StreamDB<TDef extends StreamStateDefinition> = {
collections: CollectionMap<TDef>;
} & StreamDBMethods;
/**
* StreamDB with actions
*/
type StreamDBWithActions<TDef extends StreamStateDefinition, TActions extends Record<string, ActionDefinition<any>>> = StreamDB<TDef> & {
actions: ActionMap<TActions>;
};
/**
* Utility methods available on StreamDB
*/
interface StreamDBUtils {
/**
* Wait for a specific transaction ID to be synced through the stream
* @param txid The transaction ID to wait for (UUID string)
* @param timeout Optional timeout in milliseconds (defaults to 5000ms)
* @returns Promise that resolves when the txid is synced
*/
awaitTxId: (txid: string, timeout?: number) => Promise<void>;
}
/**
* Methods available on a StreamDB instance
*/
interface StreamDBMethods {
/**
* The underlying DurableStream instance
*/
stream: DurableStream;
/**
* Preload all collections by consuming the stream until up-to-date
*/
preload: () => Promise<void>;
/**
* Close the stream connection and cleanup
*/
close: () => void;
/**
* Utility methods for advanced stream operations
*/
utils: StreamDBUtils;
}
/**
* Create a state schema definition with typed collections and event helpers
*/
declare function createStateSchema<T extends Record<string, CollectionDefinition>>(collections: T): StateSchema<T>;
/**
* Create a stream-backed database with TanStack DB collections
*
* This function is synchronous - it creates the stream handle and collections
* but does not start the stream connection. Call `db.preload()` to connect
* and sync initial data.
*
* @example
* ```typescript
* const stateSchema = createStateSchema({
* users: { schema: userSchema, type: "user", primaryKey: "id" },
* messages: { schema: messageSchema, type: "message", primaryKey: "id" },
* })
*
* // Create a stream DB (synchronous - stream is created lazily on preload)
* const db = createStreamDB({
* streamOptions: {
* url: "https://api.example.com/streams/my-stream",
* contentType: "application/json",
* },
* state: stateSchema,
* })
*
* // preload() creates the stream and loads initial data
* await db.preload()
* const user = await db.collections.users.get("123")
* ```
*/
declare function createStreamDB<TDef extends StreamStateDefinition, TActions extends Record<string, ActionDefinition<any>> = Record<string, never>>(options: CreateStreamDBOptions<TDef, TActions>): TActions extends Record<string, never> ? StreamDB<TDef> : StreamDBWithActions<TDef, TActions>;
//#endregion
export { ActionDefinition, ActionFactory, ActionMap, ChangeEvent, ChangeHeaders, Collection, CollectionDefinition, CollectionEventHelpers, CollectionWithHelpers, ControlEvent, CreateStreamDBOptions, MaterializedState, Operation, Row, StateEvent, StateSchema, StreamDB, StreamDBMethods, StreamDBUtils, StreamDBWithActions, StreamStateDefinition, SyncConfig, Value, and, avg, count, createCollection, createOptimisticAction, createStateSchema, createStreamDB, eq, gt, gte, ilike, inArray, isChangeEvent, isControlEvent, isNull, isUndefined, like, lt, lte, max, min, not, or, sum };