/** * Effect-based database factory. * * Creates an in-memory database with typed collections, each backed by * Ref> for O(1) ID lookup and atomic state updates. * * Query pipeline: Ref snapshot → Stream.fromIterable → filter → populate → sort → paginate → select * CRUD: Effect-based operations with typed error channels * Persistence: Optional debounced save after each CRUD mutation via Effect.forkDetach */ import { Effect, Schema, type Scope, Stream } from "effect"; import { type DuplicateKeyError, type ForeignKeyError, type HookError, NotFoundError, OperationError, type TransactionError, type UniqueConstraintError, ValidationError } from "../errors/crud-errors.js"; import type { MigrationError } from "../errors/migration-errors.js"; import type { PluginError } from "../errors/plugin-errors.js"; import type { DanglingReferenceError } from "../errors/query-errors.js"; import type { SourceError } from "../errors/source-errors.js"; import type { SerializationError, StorageError, UnsupportedFormatError } from "../errors/storage-errors.js"; import type { DryRunResult } from "../migrations/migration-types.js"; import type { ProseQLPlugin } from "../plugins/plugin-types.js"; import { type FormatCodec } from "../serializers/format-codec.js"; import { SerializerRegistry } from "../serializers/serializer-service.js"; import { type DocumentGraphDiagnostic, type DocumentGraphRecordProvenance } from "../storage/document-graph-source.js"; import { StorageAdapter } from "../storage/storage-service.js"; import { type AggregateConfig, type AggregateResult, type GroupedAggregateResult } from "../types/aggregate-types.js"; import type { CreateWithRelationshipsInput, DeleteWithRelationshipsOptions, DeleteWithRelationshipsResult, UpdateWithRelationshipsInput } from "../types/crud-relationship-types.js"; import type { CreateInput, CreateManyOptions, CreateManyResult, DeleteManyResult, MinimalEntity, TransactionContext, UpdateManyResult, UpdateWithOperators, UpsertInput, UpsertManyResult, UpsertResult } from "../types/crud-types.js"; import type { CursorConfig, RunnableCursorPage } from "../types/cursor-types.js"; import { type ConfiguredCollections, type DatabaseConfig } from "../types/database-config-types.js"; import type { GenerateDatabase, GenerateDatabaseWithPersistence, RelationshipDef } from "../types/types.js"; /** * An Effect with a lazy `runPromise` getter for non-Effect consumers. * Accessing `.runPromise` runs the effect and returns a Promise. */ export type RunnableEffect = Effect.Effect & { readonly runPromise: Promise; }; /** * A Stream with a lazy `runPromise` getter for non-Effect consumers. * Accessing `.runPromise` collects the stream into an array and returns a Promise. */ export type RunnableStream = Stream.Stream & { readonly runPromise: Promise>; }; type HasId = { readonly id: string; }; /** * Shape of a single Effect-based collection. * Query returns a RunnableStream (or RunnableCursorPage when cursor is specified), * CRUD methods return RunnableEffects. * Both have a `.runPromise` getter for non-Effect consumers. */ export interface EffectCollection { readonly query: (options?: { readonly where?: Record; readonly populate?: Record; readonly sort?: Record; readonly select?: Record | ReadonlyArray; readonly limit?: number; readonly offset?: number; readonly cursor?: CursorConfig; }) => RunnableStream, DanglingReferenceError | ValidationError> | RunnableCursorPage, DanglingReferenceError | ValidationError>; readonly findById: (id: string) => RunnableEffect; readonly exists: (id: string) => RunnableEffect; readonly create: (input: CreateInput) => RunnableEffect; readonly createMany: (inputs: ReadonlyArray>, options?: CreateManyOptions) => RunnableEffect, ValidationError | DuplicateKeyError | ForeignKeyError | HookError | UniqueConstraintError>; readonly update: (id: string, updates: UpdateWithOperators) => RunnableEffect; readonly updateMany: (predicate: (entity: T) => boolean, updates: UpdateWithOperators) => RunnableEffect, ValidationError | ForeignKeyError | HookError | UniqueConstraintError>; readonly delete: (id: string, options?: { readonly soft?: boolean; }) => RunnableEffect; readonly deleteMany: (predicate: (entity: T) => boolean, options?: { readonly soft?: boolean; readonly limit?: number; }) => RunnableEffect, OperationError | ForeignKeyError | HookError>; readonly upsert: (input: UpsertInput) => RunnableEffect, ValidationError | ForeignKeyError | HookError | UniqueConstraintError>; readonly upsertMany: (inputs: ReadonlyArray>) => RunnableEffect, ValidationError | ForeignKeyError | HookError | UniqueConstraintError>; readonly createWithRelationships: (input: CreateWithRelationshipsInput>) => RunnableEffect; readonly updateWithRelationships: (id: string, input: UpdateWithRelationshipsInput>) => RunnableEffect; readonly deleteWithRelationships: (id: string, options?: DeleteWithRelationshipsOptions>) => RunnableEffect, NotFoundError | ValidationError | OperationError>; readonly deleteManyWithRelationships: (predicate: (entity: T) => boolean, options?: DeleteWithRelationshipsOptions> & { readonly limit?: number; }) => RunnableEffect<{ readonly count: number; readonly deleted: ReadonlyArray; readonly cascaded?: Record; }>; }, ValidationError | OperationError>; readonly aggregate: (config: C) => C extends { readonly groupBy: string | ReadonlyArray; } ? RunnableEffect : RunnableEffect; /** * Create a reactive subscription that emits query results whenever the collection changes. * * The stream: * 1. Emits the current result set immediately upon subscription * 2. Re-emits whenever the underlying data changes (create/update/delete/reload) * 3. Deduplicates consecutive identical result sets to avoid spurious emissions * * The stream is scoped: it subscribes to change notifications on creation and * automatically unsubscribes when the scope closes or the stream is interrupted. * * @param config - Optional query configuration (where, sort, select, limit, offset, debounceMs) * @returns A scoped Effect that produces a Stream of result arrays */ readonly watch: (config?: { readonly where?: Record; readonly sort?: Record; readonly select?: Record | ReadonlyArray; readonly limit?: number; readonly offset?: number; readonly debounceMs?: number; }) => Effect.Effect>, never, Scope.Scope>; /** * Create a reactive subscription for a single entity by ID. * * Emits the entity immediately if it exists (or null if not), then re-emits * whenever the entity is created, updated, or deleted. * * The stream is scoped: it subscribes to change notifications on creation and * automatically unsubscribes when the scope closes or the stream is interrupted. * * @param id - The entity ID to watch * @returns A scoped Effect that produces a Stream of T | null */ readonly watchById: (id: string) => Effect.Effect, never, Scope.Scope>; } /** * Database type: a record of collection names to EffectCollections, * plus the $transaction method for atomic operations. */ export interface DocumentGraphMetadata { readonly getRecordProvenance: (collection: string, id: string) => Effect.Effect; readonly getDiagnostics: () => Effect.Effect>; } export type EffectDatabase = { readonly [K in keyof ConfiguredCollections]: EffectCollection[K]["schema"]> & HasId>; } & { /** * Execute multiple operations atomically within a transaction. * On success, all changes are committed and persistence is triggered. * On failure, all changes are rolled back and the original error is re-raised. */ readonly $transaction: (fn: (ctx: TransactionContext) => Effect.Effect) => RunnableEffect; readonly $documentGraph: DocumentGraphMetadata; }; /** * Configuration for database persistence. * When provided, CRUD mutations trigger debounced saves to disk. */ export interface EffectDatabasePersistenceConfig { /** Debounce delay in milliseconds (default 100) */ readonly writeDebounce?: number; /** * Plugin codecs to merge with the serializer registry. * When provided, these codecs are layered on top of the user-provided * SerializerRegistry service, with plugin codecs taking precedence. * @internal Used by plugin system integration */ readonly _pluginCodecs?: ReadonlyArray; } /** * Extended database type with persistence control methods. */ export type EffectDatabaseWithPersistence = EffectDatabase & { /** Flush all pending debounced writes immediately. Returns a Promise. */ readonly flush: () => Promise; /** Returns the number of writes currently pending. */ readonly pendingCount: () => number; /** * Preview which files need migration and what transforms would apply. * No transforms are executed. No files are written. */ readonly $dryRunMigrations: () => RunnableEffect; }; /** * Options for creating an Effect-based database. */ export interface EffectDatabaseOptions { /** Plugins to load, providing custom codecs, operators, ID generators, and global hooks */ readonly plugins?: ReadonlyArray; } /** * Create an Effect-based in-memory database. * * Accepts a DatabaseConfig and optional initial data (arrays keyed by collection name). * Returns an Effect that initializes Ref state for each collection and wires up * the query pipeline and CRUD methods. * * Optionally accepts plugins that provide custom codecs, operators, ID generators, * and global lifecycle hooks. * * Usage: * ```ts * const db = yield* createEffectDatabase(config, { * users: [{ id: "1", name: "Alice", age: 30 }], * companies: [{ id: "c1", name: "TechCorp" }], * }) * * // Query * const results = yield* Stream.runCollect(db.users.query({ where: { age: { $gt: 18 } } })) * * // CRUD * const user = yield* db.users.create({ name: "Bob", age: 25 }) * ``` * * With plugins: * ```ts * const db = yield* createEffectDatabase(config, initialData, { * plugins: [regexPlugin, snowflakeIdPlugin] * }) * ``` */ export declare const createEffectDatabase: (config: Config, initialData?: { readonly [K in keyof ConfiguredCollections]?: ReadonlyArray>; }, options?: EffectDatabaseOptions) => Effect.Effect, MigrationError | PluginError>; /** * Create an Effect-based in-memory database with persistence. * * Like `createEffectDatabase`, but additionally wires debounced persistence hooks * so that each CRUD mutation triggers a fire-and-forget save to disk. * * Collections with a `file` field in their config are persisted. Collections * without a `file` are in-memory only. * * Requires `StorageAdapter` and `SerializerRegistry` services in the environment. * * Optionally accepts plugins that provide custom codecs, operators, ID generators, * and global lifecycle hooks. * * Usage: * ```ts * const db = yield* createPersistentEffectDatabase(config, initialData, { writeDebounce: 200 }) * // CRUD mutations now trigger debounced saves * yield* db.users.create({ name: "Alice", age: 30 }) * // Flush all pending writes before shutdown * yield* db.flush() * ``` * * With plugins: * ```ts * const db = yield* createPersistentEffectDatabase(config, initialData, persistenceConfig, { * plugins: [regexPlugin, snowflakeIdPlugin] * }) * ``` */ export declare const createPersistentEffectDatabase: (config: Config, initialData?: { readonly [K in keyof ConfiguredCollections]?: ReadonlyArray>; }, persistenceConfig?: EffectDatabasePersistenceConfig, options?: EffectDatabaseOptions) => Effect.Effect, MigrationError | StorageError | SerializationError | UnsupportedFormatError | ValidationError | SourceError | PluginError, StorageAdapter | SerializerRegistry | Scope.Scope>; export {}; //# sourceMappingURL=database-effect.d.ts.map