{"version":3,"file":"create-offline-database.cjs","names":[],"sources":["../../src/offline/create-offline-database.ts"],"sourcesContent":["import Dexie, { type Table } from \"dexie\";\n\nimport { buildStore, type OfflineStore, type OfflineStoreConfig } from \"./create-offline-store\";\n\nexport interface OfflineTableConfig<TItem> {\n    /**\n     * Dexie index definition for this table. Use `&` for a unique primary key,\n     * e.g. `\"&id, service_id, created_at\"`.\n     */\n    indexes: string;\n    /** Property used as the primary key (default: `\"id\"`). */\n    keyPath?: keyof TItem & string;\n    /**\n     * Optional owner scoping for this table. Set per table, since one database\n     * commonly mixes scoped and unscoped data.\n     */\n    ownerField?: keyof TItem & string;\n}\n\n/** Maps each table name to the record type it stores. */\nexport type OfflineSchema = Record<string, unknown>;\n\nexport type OfflineTablesConfig<TSchema extends OfflineSchema> = {\n    [K in keyof TSchema]: OfflineTableConfig<TSchema[K]>;\n};\n\nexport interface OfflineDatabaseConfig<TSchema extends OfflineSchema> {\n    /** IndexedDB database name. */\n    databaseName: string;\n    /** Schema version. Bump when changing any table's indexes. */\n    version: number;\n    /** One entry per object store, all inside this single database. */\n    tables: OfflineTablesConfig<TSchema>;\n}\n\nexport interface OfflineDatabase<TSchema extends OfflineSchema> {\n    /**\n     * The {@link OfflineStore} for one table.\n     *\n     * The name is checked against the declared schema; the record type is\n     * supplied by the caller — `store<Chat>(\"chats\")`. Deriving it from the\n     * schema instead (`OfflineStore<TSchema[K], string>`) is what the shape\n     * below documents as unavailable: Dexie's `Table<T>` expands `UpdateSpec<T>`\n     * over the keys of `T`, and an unresolved indexed access there makes the\n     * checker answer TS2589 no matter how the value is cast.\n     *\n     * Stores are created once and memoised, so repeated calls with the same\n     * name return the same object.\n     */\n    store: <TItem>(name: keyof TSchema & string) => OfflineStore<TItem, string>;\n    /** The Dexie instance shared by every store. */\n    db: Dexie;\n    /** Delete the whole database from the browser. */\n    destroy: () => Promise<void>;\n}\n\nclass MultiTableDb extends Dexie {\n    constructor(name: string, version: number, stores: Record<string, string>) {\n        super(name);\n        this.version(version).stores(stores);\n    }\n}\n\n/**\n * Build several {@link OfflineStore}s that share one IndexedDB database.\n *\n * `createOfflineStore` gives each store a database of its own, which is the\n * right shape for one isolated cache. It is the wrong shape as soon as the\n * tables belong together: chats and their messages, an entity and its drafts,\n * anything you would read or clear as a unit. Splitting those across databases\n * costs a real transaction — Dexie runs one atomically only *within* a single\n * database — and it splits the version bump for a related change across two\n * places.\n *\n * This keeps them in one database at one version, so a schema change is one\n * bump and a multi-table write can be wrapped in `db.transaction(...)`.\n *\n * Stores are reached through `store<TItem>(name)` rather than a prebuilt map.\n * That is forced rather than chosen: Dexie's `Table<T>` expands `UpdateSpec<T>`\n * over the keys of `T`, so building `{ [K in keyof TSchema]: OfflineStore<…> }`\n * — or even naming `OfflineStore<TSchema[K], string>` inside the accessor —\n * makes the checker answer TS2589 (\"excessively deep\"). Taking the record type\n * as a parameter keeps it a plain type argument, which resolves fine. The table\n * name is still checked against the declared schema.\n *\n * The store surface is identical to `createOfflineStore`; only ownership of the\n * database changes. `ownerField` is set per table, since a database commonly\n * mixes per-user data with shared data.\n *\n * @param config - Database name, version, and one entry per table.\n * @returns A `store(name)` accessor, the shared Dexie instance, and a\n *   `destroy()` that drops the database.\n *\n * @example\n * type Chat = { id: string; service_id: string; updated_at: string };\n * type Message = { id: string; service_chat_id: string; created_at: string };\n *\n * const database = createOfflineDatabase<{ chats: Chat; messages: Message }>({\n *     databaseName: \"ChatDatabase\",\n *     version: 1,\n *     tables: {\n *         chats: { indexes: \"&id, service_id, updated_at\" },\n *         messages: { indexes: \"&id, service_chat_id, created_at\" },\n *     },\n * });\n *\n * const chats = database.store<Chat>(\"chats\");\n * const messages = database.store<Message>(\"messages\");\n *\n * // Both tables in one atomic transaction — impossible across two databases.\n * await database.db.transaction(\"rw\", chats.raw, messages.raw, async () => {\n *     await chats.put(chat);\n *     await messages.bulkPut(pending);\n * });\n */\nexport function createOfflineDatabase<TSchema extends OfflineSchema>(\n    config: OfflineDatabaseConfig<TSchema>,\n): OfflineDatabase<TSchema> {\n    const { databaseName, version, tables } = config;\n\n    const schema: Record<string, string> = {};\n    for (const name of Object.keys(tables)) {\n        schema[name] = tables[name as keyof TSchema].indexes;\n    }\n\n    const db = new MultiTableDb(databaseName, version, schema);\n    const built = new Map<string, unknown>();\n\n    function store<TItem>(name: keyof TSchema & string): OfflineStore<TItem, string> {\n        const cached = built.get(name);\n        if (cached) return cached as OfflineStore<TItem, string>;\n\n        if (!(name in tables)) {\n            throw new Error(\n                `Table \"${name}\" is not declared in ${databaseName}. ` +\n                    `Available: ${Object.keys(tables).join(\", \")}.`,\n            );\n        }\n\n        const table = db.table(name) as Table<TItem>;\n        const tableConfig = tables[name] as Pick<\n            OfflineStoreConfig<TItem>,\n            \"keyPath\" | \"ownerField\"\n        >;\n        // Dexie's `Table<T>` drags in `UpdateSpec<T>`, which maps over the keys\n        // of `T`. Instantiating that from inside a second generic function is\n        // past what the checker will unfold, and it answers TS2589\n        // (\"excessively deep\") — for every shape tried: `unknown`, `object`,\n        // `Record<string, unknown>`, a closed interface, an explicit type\n        // argument, and a cast through `never`. `createOfflineStore` calls this\n        // same `buildStore` without complaint, so it is the nesting, not the\n        // store.\n        //\n        // Suppressed rather than worked around: the emitted types and the\n        // runtime are both correct — `create-offline-database.test.ts` covers\n        // the full surface, cross-table transactions included.\n        // `@ts-expect-error` over `@ts-ignore` on purpose: if a later\n        // TypeScript raises the limit, this line starts failing and says so.\n        // @ts-expect-error TS2589 — see above\n        const created: OfflineStore<TItem> = buildStore<TItem>(db, table, tableConfig);\n        built.set(name, created);\n        return created;\n    }\n\n    return {\n        store,\n        db,\n        destroy: () => db.delete(),\n    };\n}\n"],"mappings":"qIAwDA,IAAM,EAAN,cAA2B,EAAA,OAAM,CAC7B,YAAY,EAAc,EAAiB,EAAgC,CACvE,MAAM,CAAI,EACV,KAAK,QAAQ,CAAO,CAAC,CAAC,OAAO,CAAM,CACvC,CACJ,EAsDA,SAAgB,EACZ,EACwB,CACxB,GAAM,CAAE,eAAc,UAAS,UAAW,EAEpC,EAAiC,CAAC,EACxC,IAAK,IAAM,KAAQ,OAAO,KAAK,CAAM,EACjC,EAAO,GAAQ,EAAO,EAAsB,CAAC,QAGjD,IAAM,EAAK,IAAI,EAAa,EAAc,EAAS,CAAM,EACnD,EAAQ,IAAI,IAElB,SAAS,EAAa,EAA2D,CAC7E,IAAM,EAAS,EAAM,IAAI,CAAI,EAC7B,GAAI,EAAQ,OAAO,EAEnB,GAAI,EAAE,KAAQ,GACV,MAAU,MACN,UAAU,EAAK,uBAAuB,EAAa,eACjC,OAAO,KAAK,CAAM,CAAC,CAAC,KAAK,IAAI,EAAE,EACrD,EAGJ,IAAM,EAAQ,EAAG,MAAM,CAAI,EACrB,EAAc,EAAO,GAmBrB,EAA+B,EAAA,WAAkB,EAAI,EAAO,CAAW,EAE7E,OADA,EAAM,IAAI,EAAM,CAAO,EAChB,CACX,CAEA,MAAO,CACH,QACA,KACA,YAAe,EAAG,OAAO,CAC7B,CACJ"}