import { createCollection, type Collection, type InferSchemaInput, type InferSchemaOutput, type SyncMode, type Transaction, type WritableDeep, } from "@tanstack/db"; import type { Table } from "drizzle-orm"; import type { CollectionUtils } from "@firtoz/db-helpers"; import type { IdOf, InsertSchema, InsertToSelectSchema, SelectSchema, } from "@firtoz/drizzle-utils"; import { drizzleIndexedDBCollectionOptions, type DrizzleIndexedDBCollectionConfig, } from "./collections/drizzle-indexeddb-collection"; import { migrateIndexedDBWithFunctions, type Migration, } from "./function-migrator"; import { openIndexedDb } from "./idb-operations"; import type { IDBCreator, IDBDatabaseLike } from "./idb-types"; /** * Configuration for creating a standalone IndexedDB collection */ export interface StandaloneCollectionConfig { /** * Name of the IndexedDB database */ dbName: string; /** * The Drizzle table definition */ table: TTable; /** * The name of the IndexedDB object store (defaults to table name) */ storeName?: string; /** * Migrations to apply (optional) */ migrations?: Migration[]; /** * Custom database creator (for testing/mocking) */ dbCreator?: IDBCreator; /** * Enable debug logging */ debug?: boolean; /** * Sync mode: 'eager' (immediate) or 'lazy' (on-demand) */ syncMode?: SyncMode; } /** * Type for the underlying collection */ type InternalCollection = Collection< InferSchemaOutput>, IdOf, CollectionUtils>>, InsertToSelectSchema, InferSchemaInput> >; /** * Transaction type for mutations */ type MutationTransaction = Transaction< InferSchemaOutput> >; /** * Insert input type (what you pass to insert) */ type InsertInput = InferSchemaInput>; /** * Item type (what you get back from getAll, etc.) */ type ItemType = InferSchemaOutput>; /** * Writable draft type for update callbacks */ type DraftType = WritableDeep>; /** * Standalone IndexedDB collection API */ export interface StandaloneCollection { /** * Promise that resolves when the collection is ready */ ready: Promise; /** * Check if the collection is ready (sync) */ isReady(): boolean; /** * Get all items (sync - returns current state) */ getAll(): ItemType[]; /** * Get an item by key (sync) */ get(key: IdOf): ItemType | undefined; /** * Insert item(s) * @returns Promise that resolves when persisted */ insert( data: InsertInput | InsertInput[], callback?: (transaction: MutationTransaction) => void, ): Promise>; /** * Update an item by key using a callback that receives a draft * @returns Promise that resolves when persisted */ update( key: IdOf, updater: (draft: DraftType) => void, callback?: (transaction: MutationTransaction) => void, ): Promise>; /** * Delete item(s) by key * @returns Promise that resolves when persisted */ delete( key: IdOf | IdOf[], callback?: (transaction: MutationTransaction) => void, ): Promise>; /** * Clear all items from the store * @returns Promise that resolves when truncate is complete */ truncate(): Promise; /** * Access to collection utils (truncate, receiveSync) */ utils: CollectionUtils>; /** * The underlying TanStack DB collection (for advanced usage) */ collection: InternalCollection; /** * The IndexedDB database instance (available after ready) */ db: IDBDatabaseLike | null; /** * Close the database connection */ close(): void; } /** * Create a standalone IndexedDB collection for use outside of React. * * @example * ```ts * const db = await createStandaloneCollection({ * dbName: "myapp.db", * table: schema.todos, * migrations, * }); * * // Wait for ready * await db.ready; * * // Get all items * const items = db.getAll(); * * // Insert * await db.insert({ title: "New todo" }); * * // Update * await db.update(itemId, { title: "Updated" }); * * // Delete * await db.delete(itemId); * * // Truncate * await db.truncate(); * * // Clean up * db.close(); * ``` */ export function createStandaloneCollection( config: StandaloneCollectionConfig, ): StandaloneCollection { const { dbName, table, storeName = (table as unknown as { _: { name: string } })._.name, migrations = [], dbCreator, debug = false, syncMode = "eager", } = config; // Create ready promise let resolveReady: () => void; const readyPromise = new Promise((resolve) => { resolveReady = resolve; }); // Database ref const indexedDBRef: { current: IDBDatabaseLike | null } = { current: null }; // Initialize database const initDB = async () => { try { if (migrations.length === 0) { if (debug) { console.log( `[StandaloneCollection] Opening database "${dbName}" directly`, ); } indexedDBRef.current = await openIndexedDb(dbName, dbCreator); } else { if (debug) { console.log(`[StandaloneCollection] Migrating database "${dbName}"`); } indexedDBRef.current = await migrateIndexedDBWithFunctions( dbName, migrations, debug, dbCreator, ); } if (debug) { console.log(`[StandaloneCollection] Database "${dbName}" initialized`); } resolveReady(); } catch (error) { console.error( `[StandaloneCollection] Failed to initialize database "${dbName}":`, error, ); throw error; } }; // Start initialization initDB(); // Create collection config const collectionConfig = drizzleIndexedDBCollectionOptions({ indexedDBRef, table, storeName, readyPromise, debug, syncMode, } as DrizzleIndexedDBCollectionConfig); const collection = createCollection( collectionConfig, ) as InternalCollection; // Wait for collection to be ready const collectionReady = new Promise((resolve) => { if (collection.isReady()) { resolve(); return; } collection.preload(); collection.onFirstReady(() => resolve()); }); // Combined ready promise const ready = Promise.all([readyPromise, collectionReady]).then(() => {}); // Helper to wait for transaction to persist const waitForPersist = async ( transaction: MutationTransaction, callback?: (transaction: MutationTransaction) => void, ): Promise> => { if (callback) { callback(transaction); } await transaction.isPersisted.promise; return transaction; }; return { ready, isReady(): boolean { return collection.isReady(); }, getAll(): ItemType[] { return collection.toArray; }, get(key: IdOf): ItemType | undefined { return collection.state.get(key); }, insert( data: InsertInput | InsertInput[], callback?: (transaction: MutationTransaction) => void, ): Promise> { const items = (Array.isArray(data) ? data : [data]) as InferSchemaOutput< SelectSchema >; const transaction = collection.insert( items, ) as MutationTransaction; return waitForPersist(transaction, callback); }, update( key: IdOf, updater: (draft: DraftType) => void, callback?: (transaction: MutationTransaction) => void, ): Promise> { const transaction = collection.update( key, updater, ) as MutationTransaction; return waitForPersist(transaction, callback); }, delete( key: IdOf | IdOf[], callback?: (transaction: MutationTransaction) => void, ): Promise> { const keys = Array.isArray(key) ? key : [key]; const transaction = collection.delete(keys); return waitForPersist(transaction, callback); }, truncate(): Promise { return collection.utils.truncate(); }, utils: collection.utils, collection, get db(): IDBDatabaseLike | null { return indexedDBRef.current; }, close(): void { indexedDBRef.current?.close(); indexedDBRef.current = null; }, }; }