/** * Effect-based persistence functions for loading and saving collection data. * * Uses StorageAdapter and SerializerRegistry services for I/O and format handling. * Data is decoded/encoded through Effect Schema on load/save to ensure type safety. * Includes DebouncedWriter for coalescing rapid mutations into single file writes. */ import { Effect, PubSub, Ref, Schema, type Scope, Stream } from "effect"; import { ValidationError } from "../errors/crud-errors.js"; import { MigrationError } from "../errors/migration-errors.js"; import { SerializationError, StorageError, type UnsupportedFormatError } from "../errors/storage-errors.js"; import type { Migration } from "../migrations/migration-types.js"; import { SerializerRegistry } from "../serializers/serializer-service.js"; import type { DerivedIdConfig } from "../types/database-config-types.js"; import type { ChangeEvent } from "../types/reactive-types.js"; import { StorageAdapter } from "./storage-service.js"; /** * Options for loadData. */ export interface LoadDataOptions { /** * Optional schema version from collection config. * When provided, enables version checking and migration support. */ readonly version?: number; /** * Optional migrations array for automatic data migration. * Only used when version is also provided. */ readonly migrations?: ReadonlyArray; /** * Collection name for error messages. * Required when version is provided. */ readonly collectionName?: string; /** * Explicit serialization format override. * When provided, this format is used instead of inferring from the file extension. */ readonly format?: string; /** * Dot-notation path into the parsed document where the collection data lives. * When provided, navigates into the document structure before loading entities. * The resolved value can be a Record keyed by entity ID or an array of objects with `id`. */ readonly path?: string; /** * Validation mode: "strict" (default) aborts on first invalid entity, * "lenient" skips invalid entities with warnings. */ readonly validation?: "strict" | "lenient"; /** * Optional policy for deriving runtime id from object keys. */ readonly derivedId?: DerivedIdConfig; } /** * Load collection data from a file, decode each entity through the given Schema. * * Flow: * 1. Check file existence via StorageAdapter * 2. Read raw content * 3. Deserialize via SerializerRegistry (format determined by file extension) * 4. Validate the top-level structure is a Record * 5. Extract `_version` (default 0 if absent) and remove from entity map * 6. Decode each entity value through the Schema * 7. Return a ReadonlyMap keyed by entity ID * * If the file does not exist, returns an empty ReadonlyMap. * * When `options.version` is provided: * - Extracts `_version` from the file (defaults to 0 if absent) * - Compares file version to config version * - If file version < config version: runs migrations (task 5.2) * - If file version > config version: fails with MigrationError * - If file version === config version: proceeds normally */ export declare const loadData: (filePath: string, schema: Schema.Codec, options?: LoadDataOptions) => Effect.Effect, StorageError | SerializationError | UnsupportedFormatError | ValidationError | MigrationError, StorageAdapter | SerializerRegistry | R>; /** * Options for saveData. */ export interface SaveDataOptions { /** * Optional schema version to stamp into the file. * When provided, `_version` is injected at the top level before entities. */ readonly version?: number; /** * Explicit serialization format override. * When provided, this format is used instead of inferring from the file extension. */ readonly format?: string; /** * Dot-notation path into the document where the collection data should be written. * When provided, the existing file is read first and the collection data is set * at the specified path, preserving sibling data in the document. */ readonly path?: string; /** * Optional policy for deriving runtime id from object keys. */ readonly derivedId?: DerivedIdConfig; } /** * Save collection data to a file, encoding each entity through the given Schema. * * Flow: * 1. Encode each entity through the Schema (Type → Encoded) * 2. Build a Record keyed by entity ID * 3. Optionally inject `_version` at the top level if version is provided * 4. Serialize via SerializerRegistry * 5. Ensure parent directory exists * 6. Write via StorageAdapter */ export declare const saveData: (filePath: string, schema: Schema.Codec, data: ReadonlyMap, options?: SaveDataOptions) => Effect.Effect; /** * Configuration for loading a single collection from a multi-collection file. */ export interface LoadCollectionConfig { readonly name: string; readonly schema: Schema.Codec<{ readonly id: string; }, unknown, never, never>; /** * Optional schema version from collection config. * When provided, enables version checking and migration support. */ readonly version?: number; /** * Optional migrations array for automatic data migration. * Only used when version is also provided. */ readonly migrations?: ReadonlyArray; } /** * Load multiple collections from a single file. * * The file is expected to contain a top-level object where keys are collection names * and values are objects keyed by entity ID. Each collection is decoded independently * using its own schema. * * When collections have `version` and `migrations` specified, per-collection migration * is applied: * - Each collection's `_version` is extracted from its section (default 0 if absent) * - If file version < config version: migrations run for that collection * - If file version > config version: fails with MigrationError * - After any migrations, the entire file is rewritten with all collections at their current versions * * Returns a Record mapping collection name to ReadonlyMap. */ export declare const loadCollectionsFromFile: (filePath: string, collections: ReadonlyArray) => Effect.Effect>, StorageError | SerializationError | UnsupportedFormatError | ValidationError | MigrationError, StorageAdapter | SerializerRegistry>; type HasId = { readonly id: string; }; /** * Configuration for a collection to be saved to a multi-collection file. * * @template T - The decoded entity type (must have `id` field) * @template I - The encoded/serialized type (defaults to T for simple schemas) */ export interface SaveCollectionConfig { readonly name: string; readonly schema: Schema.Codec; readonly data: ReadonlyMap; /** * Optional schema version to stamp into this collection's section. * When provided, `_version` is injected first in the collection object. */ readonly version?: number; } /** * Save multiple collections to a single file. * * Encodes each entity in each collection through its schema, then writes * the combined data as { collectionName: { _version?, id: encodedEntity, ... }, ... }. * * If a collection has a `version` specified, `_version` is stamped first * in that collection's object for readability. */ export declare function saveCollectionsToFile(filePath: string, collections: ReadonlyArray>): Effect.Effect; /** * Options for loadDataFromDirectory. */ export interface LoadDataFromDirectoryOptions { /** * Optional schema version from collection config. */ readonly version?: number; /** * Optional migrations array for automatic data migration. */ readonly migrations?: ReadonlyArray; /** * Collection name for error messages. */ readonly collectionName?: string; /** * Validation mode: "strict" (default) aborts on first invalid entity, * "lenient" skips invalid entities with warnings. */ readonly validation?: "strict" | "lenient"; } /** * Load collection data from a directory where each entity is a separate file. * * Flow: * 1. List files in directory via StorageAdapter.listDirectory * 2. Filter files matching the given format extension * 3. Read and deserialize each file * 4. Decode each entity through the Schema * 5. Return a ReadonlyMap keyed by entity ID (derived from filename) * * If the directory does not exist, returns an empty ReadonlyMap. */ export declare const loadDataFromDirectory: (dirPath: string, schema: Schema.Codec, format: string, _options?: LoadDataFromDirectoryOptions) => Effect.Effect, StorageError | SerializationError | UnsupportedFormatError | ValidationError | MigrationError, StorageAdapter | SerializerRegistry | R>; /** * Save a single entity to a directory as `/.`. */ export declare const saveEntityToDirectory: (dirPath: string, entity: A, schema: Schema.Codec, format: string) => Effect.Effect; /** * Remove a single entity file from a directory. */ export declare const removeEntityFromDirectory: (dirPath: string, id: string, format: string) => Effect.Effect; /** * Entry type for streaming directory reads. */ export interface StreamCollectionEntry { readonly id: string; readonly data: A; } /** * Stream collection entities from a directory, reading files lazily. * * Returns a Stream that eagerly lists files, then lazily reads/decodes each. */ export declare const streamCollectionFromDirectory: (dirPath: string, schema: Schema.Codec, format: string) => Stream.Stream, StorageError | SerializationError | UnsupportedFormatError | ValidationError, StorageAdapter | SerializerRegistry | R>; /** * Configuration for a directory watcher. */ export interface DirectoryWatcherConfig { /** Path to the directory to watch */ readonly dirPath: string; /** Serialization format (e.g., "yaml", "json") */ readonly format: string; /** Schema to decode loaded data through */ readonly schema: Schema.Codec; /** Ref holding the collection state to update on file change */ readonly ref: Ref.Ref>; /** Optional debounce delay in ms for reload after change (default 50) */ readonly debounceMs?: number; /** Optional PubSub to publish reload events to for reactive query subscriptions */ readonly changePubSub?: PubSub.PubSub; /** Collection name (required when changePubSub is provided) */ readonly collectionName?: string; } /** * Create a managed directory watcher using Effect.acquireRelease. * * The watcher monitors a directory for file changes. When a change is detected, * it reloads the affected entity (or the full directory for unknown filenames) * and updates the collection Ref. */ export declare const createDirectoryWatcher: (config: DirectoryWatcherConfig) => Effect.Effect; /** * Handle returned by `createDebouncedWriter`. Provides methods to schedule * debounced writes, flush all pending writes, and query pending state. */ export interface DebouncedWriter { /** * Schedule a debounced write for the given key. If a write for this key * is already pending, it is cancelled and replaced with the new one. * The actual write executes after the configured delay unless superseded. */ readonly triggerSave: (key: string, save: Effect.Effect) => Effect.Effect; /** * Immediately execute all pending writes, cancelling their debounce timers. * Errors from individual saves are collected but do not prevent other saves. */ readonly flush: () => Effect.Effect; /** * Returns the number of writes currently pending. */ readonly pendingCount: () => Effect.Effect; } /** * Create a DebouncedWriter that coalesces rapid writes into single file operations. * * Each call to `triggerSave(key, saveEffect)` cancels any pending write for * that key and schedules a new one after `delayMs` milliseconds. If another * `triggerSave` for the same key arrives before the delay elapses, the timer * resets — only the last write within a burst actually hits the filesystem. * * @param delayMs - Debounce delay in milliseconds (default 100) */ export declare const createDebouncedWriter: (delayMs?: number) => Effect.Effect; /** * Handle returned by `createFileWatcher`. Provides the ability to check * whether the watcher is active. The watcher is automatically cleaned up * when the enclosing Effect Scope closes. */ export interface FileWatcher { /** * Returns true if the watcher is still active (has not been closed). */ readonly isActive: () => Effect.Effect; } /** * Configuration for a document-source watcher. */ export interface DocumentSourceWatcherConfig { /** Root directory for the document source. */ readonly root: string; /** Effect that rediscovers and reloads the source after a filesystem event. */ readonly onReload: Effect.Effect; /** Optional debounce delay in ms for reload after change (default 50) */ readonly debounceMs?: number; } /** * Create a managed document-source watcher. * * Document sources are reloaded by debounced whole-source rediscovery instead * of attempting to interpret individual adapter events. Failed reloads are * logged and leave the previous in-memory state intact. */ export declare const createDocumentSourceWatcher: (config: DocumentSourceWatcherConfig) => Effect.Effect; /** * Configuration for a single file watcher. */ export interface FileWatcherConfig { /** Path to the file to watch */ readonly filePath: string; /** Schema to decode loaded data through */ readonly schema: Schema.Codec; /** Ref holding the collection state to update on file change */ readonly ref: Ref.Ref>; /** Optional debounce delay in ms for reload after change (default 50) */ readonly debounceMs?: number; /** Optional PubSub to publish reload events to for reactive query subscriptions */ readonly changePubSub?: PubSub.PubSub; /** Collection name (required when changePubSub is provided) */ readonly collectionName?: string; /** Optional policy for deriving runtime id from object keys. */ readonly derivedId?: DerivedIdConfig; } /** * Create a managed file watcher using Effect.acquireRelease. * * The watcher monitors a file for external changes. When a change is detected, * it reloads the file through the Schema and updates the collection Ref. * * The watcher lifecycle is managed by Effect's Scope — it is automatically * closed when the Scope finalizes (database shutdown, test cleanup, etc.). * * Reload is debounced to avoid redundant reloads when editors write * multiple change events in quick succession. * * @param config - File watcher configuration * @returns Effect that yields a FileWatcher handle (requires Scope) */ export declare const createFileWatcher: (config: FileWatcherConfig) => Effect.Effect; /** * Create managed file watchers for multiple files at once. * * Convenience wrapper that creates a watcher for each config entry. * All watchers share the enclosing Scope and are cleaned up together. * * @param configs - Array of file watcher configurations * @returns Effect yielding an array of FileWatcher handles */ export declare const createFileWatchers: (configs: ReadonlyArray>) => Effect.Effect, StorageError, Scope.Scope | StorageAdapter | SerializerRegistry | R>; export {}; //# sourceMappingURL=persistence-effect.d.ts.map