@asaidimu/utils-database 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.mts CHANGED
@@ -1,16 +1,80 @@
1
1
  import { QueryFilter, PaginationOptions } from '@asaidimu/query';
2
- import { SchemaDefinition, SchemaChange, DataTransform, PredicateMap } from '@asaidimu/anansi';
2
+ import { IndexDefinition, SchemaDefinition, SchemaChange, DataTransform, PredicateMap } from '@asaidimu/anansi';
3
3
  import { EventBus } from '@asaidimu/events';
4
4
  import { StandardSchemaV1 } from '@standard-schema/spec';
5
5
 
6
+ /**
7
+ * Buffers write operations across one or more stores and commits them atomically.
8
+ *
9
+ * ## How atomicity works
10
+ *
11
+ * ### IndexedDB stores (same database)
12
+ * At commit time, TransactionContext collects the names of every IDB store that
13
+ * received operations, then opens a **single** `IDBTransaction` spanning all of
14
+ * them via `ConnectionManager.openTransaction`. Each store's `executeInTransaction`
15
+ * receives that shared transaction object and performs its writes against it
16
+ * without opening a new transaction of its own. IDB commits or aborts the
17
+ * entire multi-store transaction as one unit.
18
+ *
19
+ * ### MemoryStore
20
+ * MemoryStore's `executeInTransaction` receives `null` for the shared transaction.
21
+ * It applies ops against an internal staging map and returns. If a later
22
+ * participant fails, TransactionContext calls `rollbackMemory` on each
23
+ * MemoryStore that already applied its staged ops. MemoryStore restores its
24
+ * pre-transaction snapshot.
25
+ *
26
+ * ### Mixed (IDB + Memory in the same transaction)
27
+ * All IDB stores are committed first as a single atomic IDB transaction, then
28
+ * each MemoryStore is committed. If a MemoryStore fails after IDB has already
29
+ * committed, the IDB side cannot be rolled back — this is an inherent limitation
30
+ * of mixing two different storage engines. In practice the schema store is
31
+ * always MemoryStore-or-IDB consistently, so mixed transactions should not arise
32
+ * in normal usage.
33
+ */
6
34
  declare class TransactionContext {
7
35
  readonly id: string;
8
- private buffer;
9
- private committed;
36
+ /**
37
+ * Flat list of every operation staged so far, in the order they were added.
38
+ * We keep the store reference alongside the op so commit() can group them.
39
+ */
40
+ private staged;
41
+ private done;
10
42
  constructor();
11
- addOp(store: Store<any>, type: "put" | "delete", data: any): void;
43
+ /**
44
+ * Stages a single write operation against a store.
45
+ * Does NOT touch the store — no I/O happens until commit().
46
+ */
47
+ addOp<T extends Record<string, any>>(store: Store<T>, type: "put" | "delete" | "add", data: any): Promise<void>;
48
+ /**
49
+ * Commits all staged operations atomically.
50
+ *
51
+ * For IDB stores: opens one shared IDBTransaction across all participating
52
+ * stores, then dispatches ops to each store's executeInTransaction.
53
+ * For MemoryStores: dispatches sequentially; rolls back on failure.
54
+ */
12
55
  commit(): Promise<void>;
56
+ /**
57
+ * Discards all staged operations. No I/O has occurred so there is nothing
58
+ * to undo — we simply clear the buffer.
59
+ */
13
60
  rollback(): void;
61
+ /**
62
+ * Opens ONE IDBTransaction across all participating IDB stores and lets
63
+ * each store execute its ops against the shared transaction handle.
64
+ *
65
+ * We obtain the IDBDatabase from the first store (they all share the same
66
+ * ConnectionManager / database) and open the transaction ourselves so that
67
+ * the commit/abort lifecycle belongs entirely to this method.
68
+ */
69
+ private commitIDB;
70
+ /**
71
+ * Commits MemoryStore groups sequentially.
72
+ * Maintains a list of stores that have already applied their ops; if any
73
+ * store throws, all previously-applied stores are rolled back via the
74
+ * store-level `_rollbackMemory(snapshot)` escape hatch.
75
+ */
76
+ private commitMemory;
77
+ completed(): boolean;
14
78
  }
15
79
 
16
80
  declare const DEFAULT_KEYPATH = "$id";
@@ -30,8 +94,9 @@ interface CursorCallbackResult<T> {
30
94
  * @template T - The type of records stored.
31
95
  * @param value - The current record value (cloned, not a live reference).
32
96
  * @param key - The key (ID) of the current record.
33
- * @param cursor - The underlying cursor object (implementationspecific; may be `null` in memory adapters).
34
- * @returns A promise that resolves to an object indicating whether iteration should stop, and an optional offset to advance.
97
+ * @param cursor - The underlying cursor object (implementation-specific; may be `null` in memory adapters).
98
+ * @returns A promise resolving to an object indicating whether iteration should stop,
99
+ * and an optional offset to advance.
35
100
  */
36
101
  type CursorCallback<T> = (value: T, key: string | number, cursor: any) => Promise<CursorCallbackResult<T>>;
37
102
  /**
@@ -43,123 +108,100 @@ interface StoreKeyRange {
43
108
  lowerOpen?: boolean;
44
109
  upperOpen?: boolean;
45
110
  }
111
+ /**
112
+ * A single buffered operation staged inside a TransactionContext.
113
+ * Kept intentionally minimal — the context only needs to know what to
114
+ * replay against a store during commit.
115
+ */
116
+ type BufferedOperation<T> = {
117
+ type: "add" | "put";
118
+ data: T | T[];
119
+ } | {
120
+ type: "delete";
121
+ data: string | number | (string | number)[];
122
+ };
46
123
  /**
47
124
  * Storage adapter interface for a single object store (collection).
48
125
  *
49
- * This interface abstracts all low‑level persistence operations, allowing
50
- * different backends (IndexedDB, memory, remote, etc.) to be used interchangeably.
51
- * All methods return promises and operate on clones of data to prevent
52
- * unintended mutations.
126
+ * Stores own their indexes. Index lifecycle (create, drop) and index-aware reads
127
+ * (findByIndex) are part of this contract so that both MemoryStore and IndexedDBStore
128
+ * implement them natively MemoryStore via in-memory index maps, IndexedDB via its
129
+ * native index mechanism.
53
130
  *
54
- * @template T - The type of objects stored in this store. Must include the key path property.
131
+ * @template T - The type of objects stored. Must include the key path property.
55
132
  */
56
- interface Store<T = any> {
133
+ interface Store<T extends Record<string, any> = Record<string, any>> {
134
+ /**
135
+ * Returns the name of this store (the IDB object store / collection name).
136
+ * Used by TransactionContext to group operations and open a correctly-scoped
137
+ * multi-store IDB transaction at commit time.
138
+ */
139
+ name(): string;
140
+ /**
141
+ * Opens the store, ensuring underlying storage structures exist.
142
+ */
143
+ open(): Promise<void>;
57
144
  /**
58
145
  * Adds one or more records to the store.
59
- *
60
146
  * If a record does not have a value for the store's key path, an automatic
61
- * key (e.g., auto‑incremented number) may be assigned. The store's key path
62
- * property is then updated on the added record(s).
63
- *
64
- * @param data - A single record or an array of records to add.
65
- * @returns A promise that resolves to:
66
- * - the key(s) of the added record(s) – a single key if `data` was a single record,
67
- * or an array of keys if `data` was an array.
68
- * @throws {Error} If any record lacks the key path property and auto‑keying is not supported,
69
- * or if a record with the same key already exists.
147
+ * key may be assigned. Throws if a record with the same key already exists.
70
148
  */
71
149
  add(data: T | T[]): Promise<string | number | (string | number)[]>;
72
150
  /**
73
- * Removes all records from the store.
74
- *
75
- * @returns A promise that resolves when the store is cleared.
76
- * @throws {Error} If the operation fails (e.g., store is closed).
151
+ * Removes all records from the store without destroying index structures.
77
152
  */
78
153
  clear(): Promise<void>;
79
154
  /**
80
155
  * Returns the total number of records in the store.
81
- *
82
- * @returns A promise that resolves to the record count.
83
- * @throws {Error} If the operation fails.
84
156
  */
85
157
  count(): Promise<number>;
86
158
  /**
87
159
  * Deletes one or more records by their keys.
88
- *
89
- * @param id - A single key or an array of keys to delete.
90
- * @returns A promise that resolves when the records are deleted.
91
- * @throws {Error} If any key is `undefined` or the operation fails.
92
160
  */
93
161
  delete(id: string | number | (string | number)[]): Promise<void>;
94
162
  /**
95
163
  * Retrieves a single record by its primary key.
96
- *
97
- * @param id - The key of the record to retrieve.
98
- * @returns A promise that resolves to the record (cloned) if found, otherwise `undefined`.
99
- * @throws {Error} If the key is `undefined` or the operation fails.
100
164
  */
101
165
  getById(id: string | number): Promise<T | undefined>;
102
166
  /**
103
- * Retrieves a single record by an index and index key.
167
+ * Retrieves the first record matching an exact index key (point lookup).
168
+ * Useful for unique indexes — returns the single matching record or undefined.
104
169
  *
105
- * @param index - The name of the index to query.
106
- * @param key - The exact key value to look up in the index.
107
- * @returns A promise that resolves to the first matching record (cloned), or `undefined` if none.
108
- * @throws {Error} If the index does not exist, the key is `undefined`, or the operation fails.
170
+ * @param indexName - The name of the index to query.
171
+ * @param key - The exact key value to look up.
109
172
  */
110
- getByIndex(index: string, key: any): Promise<T | undefined>;
173
+ getByIndex(indexName: string, key: any): Promise<T | undefined>;
111
174
  /**
112
- * Retrieves multiple records from an index, optionally within a key range.
175
+ * Retrieves all records from a named index, optionally filtered by a key range.
176
+ * Use this for range scans over an index (e.g. all records where age >= 18).
113
177
  *
114
- * @param index - The name of the index to query.
115
- * @param keyRange - Optional `StoreKeyRange` to filter results. If omitted, all records are returned.
116
- * @returns A promise that resolves to an array of matching records (each cloned).
117
- * @throws {Error} If the index does not exist or the operation fails.
178
+ * @param indexName - The name of the index to query.
179
+ * @param keyRange - Optional range to filter results.
118
180
  */
119
- getByKeyRange(index: string, keyRange?: StoreKeyRange): Promise<T[]>;
181
+ getByKeyRange(indexName: string, keyRange?: StoreKeyRange): Promise<T[]>;
120
182
  /**
121
- * Retrieves all records from the store.
122
- *
123
- * @returns A promise that resolves to an array of all records (each cloned).
124
- * @throws {Error} If the operation fails.
183
+ * Retrieves all records from the store without index involvement.
125
184
  */
126
185
  getAll(): Promise<T[]>;
127
186
  /**
128
- * Inserts or replaces a record.
129
- *
130
- * If a record with the same key already exists, it is replaced.
131
- * The record must contain the store's key path property.
132
- *
133
- * @param data - The record to store.
134
- * @returns A promise that resolves to the key of the stored record.
135
- * @throws {Error} If the record lacks the key path property or the operation fails.
187
+ * Inserts or replaces a record. Validates OCC if a record with the same key exists.
136
188
  */
137
189
  put(data: T): Promise<string | number>;
138
190
  /**
139
191
  * Iterates over records using a cursor, allowing early termination and skipping.
140
192
  *
141
- * The callback is invoked for each record in iteration order. The callback
142
- * can control the iteration by returning `{ done: true }` to stop, or
143
- * `{ offset: n }` to skip ahead `n` records.
144
- *
145
- * @param callback - Function called for each record.
146
- * @param direction - Iteration direction: `"forward"` (ascending keys) or `"backward"` (descending keys).
147
- * @param keyRange - An optional StoreKeyRange to start from specific points.
148
- * @returns A promise that resolves to the last record processed (or `null` if none).
149
- * @throws {Error} If the callback throws or the operation fails.
193
+ * @param callback - Invoked for each record; return `{ done: true }` to stop,
194
+ * `{ offset: n }` to skip ahead n records.
195
+ * @param direction - Iteration order.
196
+ * @param keyRange - Optional range to restrict iteration.
150
197
  */
151
198
  cursor(callback: CursorCallback<T>, direction?: "forward" | "backward", keyRange?: StoreKeyRange): Promise<T | null>;
152
199
  /**
153
- * Executes a batch of write operations atomically.
200
+ * Executes a batch of write operations atomically within this store.
201
+ * All operations succeed or fail together.
154
202
  *
155
- * All operations in the batch succeed or fail together. This is useful for
156
- * maintaining consistency when multiple writes are required.
157
- *
158
- * @param operations - An array of operations. Each operation can be:
159
- * - `{ type: "add" | "put", data: T | T[] }`
160
- * - `{ type: "delete", data: string | number | (string | number)[] }`
161
- * @returns A promise that resolves when the batch is committed.
162
- * @throws {Error} If any operation fails or the batch cannot be completed.
203
+ * Used for standalone (single-store) atomic writes. For cross-store atomicity,
204
+ * use executeInTransaction instead.
163
205
  */
164
206
  batch(operations: Array<{
165
207
  type: "add" | "put";
@@ -168,44 +210,81 @@ interface Store<T = any> {
168
210
  type: "delete";
169
211
  data: string | number | (string | number)[];
170
212
  }>): Promise<void>;
171
- open(): Promise<void>;
213
+ /**
214
+ * Registers a new index on the store. Idempotent — no-op if the index already exists.
215
+ * For IndexedDB, this triggers a database version upgrade.
216
+ * For MemoryStore, this builds the index map from existing records.
217
+ *
218
+ * @param definition - The full index definition from the schema.
219
+ */
220
+ createIndex(definition: IndexDefinition): Promise<void>;
221
+ /**
222
+ * Removes a named index from the store.
223
+ * For IndexedDB, this triggers a database version upgrade.
224
+ * For MemoryStore, this drops the in-memory index map.
225
+ *
226
+ * @param name - The index name as declared in IndexDefinition.name.
227
+ */
228
+ dropIndex(name: string): Promise<void>;
229
+ /**
230
+ * Returns all records matching an exact index key.
231
+ * Unlike getByIndex (which returns only the first match), this returns all matches —
232
+ * essential for non-unique indexes where multiple records share the same indexed value.
233
+ *
234
+ * @param indexName - The name of the index to query.
235
+ * @param value - The exact value to look up.
236
+ */
237
+ findByIndex(indexName: string, value: any): Promise<T[]>;
238
+ /**
239
+ * Executes a set of buffered operations as part of a cross-store atomic transaction.
240
+ *
241
+ * For IndexedDBStore: `sharedTx` is the single IDBTransaction opened across all
242
+ * participating stores. Operations are applied directly to `sharedTx.objectStore(name)`
243
+ * without opening a new transaction — IDB commits or aborts the whole thing atomically.
244
+ *
245
+ * For MemoryStore: `sharedTx` is null. The store applies ops against its own staging
246
+ * area. The caller (TransactionContext) is responsible for coordinating rollback across
247
+ * all MemoryStores if any participant fails.
248
+ *
249
+ * This method must NOT open, commit, or abort any transaction itself.
250
+ *
251
+ * @param ops - The buffered operations to apply.
252
+ * @param sharedTx - The shared IDBTransaction (IndexedDB only), or null (MemoryStore).
253
+ */
254
+ executeInTransaction(ops: BufferedOperation<T>[], sharedTx: IDBTransaction | null): Promise<void>;
172
255
  }
173
256
  interface Collection<T> {
174
257
  /**
175
258
  * Finds a single document matching the query.
176
- * @param query - The query to execute.
177
- * @returns A promise resolving to the matching document or `null` if not found.
178
259
  */
179
260
  find: (query: QueryFilter<T>) => Promise<Document<T> | null>;
180
261
  /**
181
- * Lists documents based on the provided query.
182
- * @param query - The query to list documents (supports pagination and sorting).
183
- * @returns A promise resolving to an array of documents.
262
+ * Lists documents with pagination. Returns an AsyncIterator so consumers can
263
+ * wrap it in their own iteration protocol (e.g. for-await-of via AsyncIterable).
184
264
  */
185
265
  list: (query: PaginationOptions) => Promise<AsyncIterator<Document<T>[]>>;
186
266
  /**
187
- * Filters documents based on the provided query.
188
- * @param query - The query to filter documents.
189
- * @returns A promise resolving to an array of matching documents.
267
+ * Filters all documents matching the query.
190
268
  */
191
269
  filter: (query: QueryFilter<T>) => Promise<Document<T>[]>;
192
270
  /**
193
- * Creates a new document in the schema.
271
+ * Creates a new document in the collection.
272
+ *
273
+ * When a TransactionContext is provided the initial store.add is buffered into
274
+ * the transaction rather than written immediately. The document is returned in
275
+ * its fully initialised in-memory state regardless — callers can use it before
276
+ * the transaction commits.
277
+ *
194
278
  * @param initial - The initial data for the document.
195
- * @returns A promise resolving to the created document.
279
+ * @param tx - Optional transaction to buffer the write into.
196
280
  */
197
- create: (initial: T) => Promise<Document<T>>;
281
+ create: (initial: T, tx?: TransactionContext) => Promise<Document<T>>;
198
282
  /**
199
- * Subscribes to schema-level events (e.g., "create", "update", "delete", "access").
200
- * @param event - The event type to subscribe to.
201
- * @param callback - The function to call when the event occurs.
202
- * @returns A promise resolving to an unsubscribe function.
283
+ * Subscribes to collection-level events.
203
284
  */
204
- subscribe: (event: CollectionEventType | TelemetryEventType, callback: (event: CollectionEvent<T> | TelemetryEvent) => void) => Promise<() => void>;
285
+ subscribe: (event: CollectionEventType | TelemetryEventType, callback: (event: CollectionEvent<T> | TelemetryEvent) => void) => () => void;
205
286
  /**
206
- * Validate data
207
- * @param data - The data to validate
208
- * @returns An object containing validation results
287
+ * Validates data against the collection's schema.
209
288
  */
210
289
  validate(data: Record<string, any>): Promise<{
211
290
  value?: any;
@@ -214,9 +293,10 @@ interface Collection<T> {
214
293
  path: Array<string>;
215
294
  }>;
216
295
  }>;
296
+ invalidate(): void;
217
297
  }
218
298
  /**
219
- * Event payload for DocumentCursor events.
299
+ * Event payload for Collection events.
220
300
  */
221
301
  type CollectionEventType = "document:create" | "collection:read" | "migration:start" | "migration:end";
222
302
  type CollectionEvent<T> = {
@@ -229,91 +309,49 @@ type CollectionEvent<T> = {
229
309
  };
230
310
  interface Database {
231
311
  /**
232
- * Accesses a schema model by name.
233
- * @param schemaName - The name of the schema to access.
234
- * @returns A promise resolving to the schema's DocumentCursor.
235
- * @throws DatabaseError
312
+ * Opens an existing collection by name.
236
313
  */
237
314
  collection: <T>(schemaName: string) => Promise<Collection<T>>;
238
315
  /**
239
- * Creates a new schema model.
240
- * @param schema - The schema definition.
241
- * @returns A promise resolving to the created schema's DocumentCursor.
242
- * @throws DatabaseError
316
+ * Creates a new collection from a schema definition.
243
317
  */
244
318
  createCollection: <T>(schema: SchemaDefinition) => Promise<Collection<T>>;
245
319
  /**
246
- * Deletes a schema by name.
247
- * @param schemaName - The name of the schema to delete.
248
- * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
249
- * @throws DatabaseError
320
+ * Deletes a collection and its schema record.
250
321
  */
251
322
  deleteCollection: (schemaName: string) => Promise<boolean>;
252
323
  /**
253
- * Updates an existing schema.
254
- * @param schema - The updated schema definition.
255
- * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
256
- * @throws DatabaseError
324
+ * Updates an existing collection's schema record.
257
325
  */
258
326
  updateCollection: (schema: SchemaDefinition) => Promise<boolean>;
259
327
  /**
260
- * Migrates an existing collection's data and updates its schema definition metadata.
261
- * This function processes data in a streaming fashion to prevent loading
262
- * the entire collection into memory.
263
- *
264
- * It will:
265
- * 1. Verify the target collection exists.
266
- * 2. Retrieve the collection's current schema definition from a metadata store ($index).
267
- * 3. Initialize a `MigrationEngine` with this schema.
268
- * 4. Allow a callback to define specific data transformations using the `MigrationEngine`.
269
- * 5. **Crucially, it uses `migrationEngine.dryRun()` to get the `newSchema` that results**
270
- * **from the transformations defined in the callback.**
271
- * 6. Execute these transformations by streaming data from the collection,
272
- * through the `MigrationEngine`, and back into the same collection.
273
- * 7. Finally, update the schema definition for the collection in the `$index` metadata store
274
- * to reflect this `newSchema`.
275
- * All these steps for data and metadata updates happen within a single atomic IndexedDB transaction.
276
- *
277
- * Note: This function focuses solely on *data transformation* and *metadata updates*.
278
- * It does NOT handle structural IndexedDB changes like adding/removing physical indexes or object stores,
279
- * which still require an `onupgradeneeded` event (i.e., a database version upgrade).
280
- *
281
- * @param name - The name of the collection (IndexedDB object store) to migrate.
282
- * @param {Object} opts - Options for the new migration
283
- * @param {SchemaChange<any>[]} opts.changes - Array of schema changes
284
- * @param {string} opts.description - Description of the migration
285
- * @param {SchemaChange<any>[]} [opts.rollback] - Optional rollback changes
286
- * @param {DataTransform<any, any>} [opts.transform] - Optional data transform
287
- * @returns A Promise resolving to `true` if the migration completes successfully,
288
- * @throws {DatabaseError} If the collection does not exist, its schema metadata is missing,
289
- * or any IndexedDB operation/streaming fails critically.
328
+ * Migrates an existing collection's data and schema definition.
329
+ * Processes data in a streaming fashion to avoid loading the full collection
330
+ * into memory.
331
+ */
332
+ migrateCollection: <T>(name: string, opts: CollectionMigrationOptions, batchSize?: number) => Promise<Collection<T>>;
333
+ /**
334
+ * Executes a callback within a TransactionContext.
335
+ * Writes buffered inside the callback are flushed atomically on commit.
336
+ * If the callback throws, the buffer is discarded (no writes are flushed).
290
337
  */
291
- migrateCollection: (name: string, opts: CollectionMigrationOptions, batchSize?: number) => Promise<boolean>;
338
+ transaction: (callback: (tx: TransactionContext) => Promise<void>) => Promise<void>;
292
339
  /**
293
- * Subscribes to database-level events (e.g., "schemaAdded", "schemaDeleted", "schemaAccessed", "migrate").
294
- * @param event - The event type to subscribe to.
295
- * @param callback - The function to call when the event occurs.
296
- * @returns A promise resolving to an unsubscribe function.
340
+ * Subscribes to database-level events.
297
341
  */
298
- subscribe: (event: DatabaseEventType | "telemetry", callback: (event: DatabaseEvent | TelemetryEvent) => void) => Promise<() => void>;
342
+ subscribe: (event: DatabaseEventType | "telemetry", callback: (event: DatabaseEvent | TelemetryEvent) => void) => () => void;
299
343
  /**
300
- * Closes the connection to the database
344
+ * Releases in-memory references and event bus subscriptions.
345
+ * Does not delete any persisted data.
301
346
  */
302
347
  close: () => void;
348
+ clear: () => Promise<void>;
303
349
  /**
304
- * Ensures a collection exists; creates it if it doesn't.
305
- * Idempotent – safe to call multiple times.
306
- * @param schema - The schema definition for the collection.
307
- * @returns A promise that resolves when the collection exists.
308
- * @throws {DatabaseError} If validation fails or an unexpected error occurs.
309
- */
350
+ * Ensures a collection exists; creates it if it doesn't. Idempotent.
351
+ */
310
352
  ensureCollection: (schema: SchemaDefinition) => Promise<void>;
311
353
  /**
312
- * Ensures multiple collections exist; creates any that don't.
313
- * Idempotent – safe to call multiple times.
314
- * @param schemas - An array of schema definitions.
315
- * @returns A promise that resolves when all collections exist.
316
- * @throws {DatabaseError} If any schema validation fails or an unexpected error occurs.
354
+ * Ensures multiple collections exist; creates any that don't. Idempotent.
317
355
  */
318
356
  setupCollections: (schemas: SchemaDefinition[]) => Promise<void>;
319
357
  }
@@ -323,9 +361,6 @@ type CollectionMigrationOptions = {
323
361
  rollback?: SchemaChange<any>[];
324
362
  transform?: string | DataTransform<any, any>;
325
363
  };
326
- /**
327
- * Event payload for Database events.
328
- */
329
364
  type DatabaseEventType = "collection:create" | "collection:delete" | "collection:update" | "collection:read" | "migrate";
330
365
  type DatabaseEvent = {
331
366
  type: DatabaseEventType;
@@ -335,60 +370,17 @@ type DatabaseEvent = {
335
370
  type Document<T> = {
336
371
  readonly [K in keyof T]: T[K];
337
372
  } & {
338
- /**
339
- * Unique identifier for the document (assigned automatically if not provided).
340
- */
341
373
  $id?: string;
342
- /**
343
- * Timestamp of document creation (ISO string or Date).
344
- */
345
374
  $created?: string | Date;
346
- /**
347
- * Timestamp of last document update (ISO string or Date).
348
- */
349
375
  $updated?: string | Date;
350
- /**
351
- * Version number incremented on each change (used for optimistic concurrency control).
352
- */
353
376
  $version?: number;
354
- /**
355
- * Fetches the latest data from the database and updates the document instance.
356
- * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
357
- */
358
377
  read: () => Promise<boolean>;
359
- /**
360
- * Saves the current document state to the database.
361
- * Normally called automatically by `update()` or `delete()`, but can be used manually.
362
- * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
363
- */
364
378
  save: (tx?: TransactionContext) => Promise<boolean>;
365
- /**
366
- * Updates the document with the provided properties.
367
- * @param props - Partial object containing the fields to update.
368
- * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
369
- */
370
- update: (props: Partial<T>) => Promise<boolean>;
371
- /**
372
- * Deletes the document from the database.
373
- * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
374
- */
375
- delete: () => Promise<boolean>;
376
- /**
377
- * Subscribes to document events (e.g., "update", "delete", "access").
378
- * @param event - The event type to subscribe to.
379
- * @param callback - The function to call when the event occurs.
380
- * @returns An unsubscribe function.
381
- */
379
+ update: (props: Partial<T>, tx?: TransactionContext) => Promise<boolean>;
380
+ delete: (tx?: TransactionContext) => Promise<boolean>;
382
381
  subscribe: (event: DocumentEventType | TelemetryEventType, callback: (event: DocumentEvent<T> | TelemetryEvent) => void) => () => void;
383
- /**
384
- * Returns a plain object containing only the user-defined data (without system fields like $id, $version, etc.).
385
- * @returns The current user data.
386
- */
387
382
  state(): T;
388
383
  };
389
- /**
390
- * Event payload for DocumentModel events.
391
- */
392
384
  type DocumentEventType = "document:create" | "document:write" | "document:update" | "document:delete" | "document:read";
393
385
  type DocumentEvent<T> = {
394
386
  type: DocumentEventType;
@@ -412,7 +404,7 @@ type TelemetryEvent = {
412
404
  document?: string;
413
405
  };
414
406
  result?: {
415
- type: 'array' | string;
407
+ type: "array" | string;
416
408
  size?: number;
417
409
  };
418
410
  error: {
@@ -423,22 +415,88 @@ type TelemetryEvent = {
423
415
  };
424
416
  };
425
417
  interface DatabaseConfig {
426
- name: string;
418
+ database: string;
427
419
  keyPath?: string;
428
420
  schemasStoreName?: string;
429
421
  enableTelemetry?: boolean;
430
422
  predicates?: PredicateMap;
431
423
  validate?: boolean;
432
424
  }
425
+ type StoreConfig = DatabaseConfig & {
426
+ collection: string;
427
+ };
433
428
 
429
+ /**
430
+ * Internal structure for a single maintained index.
431
+ */
432
+ interface IndexEntry {
433
+ definition: IndexDefinition;
434
+ /** Maps a composite/single index key → set of record primary keys ($id) */
435
+ map: Map<string, Set<string | number>>;
436
+ }
437
+ /**
438
+ * Snapshot of the store's mutable state, used for cross-store transaction rollback.
439
+ */
440
+ interface MemoryStoreSnapshot {
441
+ data: Map<string | number, Readonly<Record<string, any>>>;
442
+ indexes: Map<string, IndexEntry>;
443
+ nextId: number;
444
+ }
434
445
  declare class MemoryStore<T extends Record<string, any>> implements Store<T> {
446
+ private readonly storeName;
435
447
  private readonly keyPath;
436
448
  private data;
449
+ private indexes;
437
450
  private nextId;
438
- constructor(keyPath?: string);
451
+ /**
452
+ * @param storeName - The logical name of this store (matches the collection name).
453
+ * @param keyPath - The primary key field name (default: "$id").
454
+ * @param indexDefs - Index definitions from the schema. The store will maintain
455
+ * these indexes on every write operation.
456
+ */
457
+ constructor(storeName: string, keyPath?: string, indexDefs?: IndexDefinition[]);
458
+ name(): string;
439
459
  open(): Promise<void>;
440
- private getKey;
441
- private clone;
460
+ /**
461
+ * Returns a deep snapshot of the store's mutable state.
462
+ * Called by TransactionContext immediately before executeInTransaction so
463
+ * that if a later store in the same transaction fails, this store can be
464
+ * fully restored via _rollbackMemory.
465
+ *
466
+ * Prefixed with underscore to signal it is an internal contract between
467
+ * MemoryStore and TransactionContext — not part of the public Store API.
468
+ */
469
+ _snapshotMemory(): MemoryStoreSnapshot;
470
+ /**
471
+ * Restores the store to a previously snapshotted state.
472
+ * Called by TransactionContext when a later participant in the same
473
+ * cross-store transaction fails, requiring all already-applied stores to
474
+ * be unwound.
475
+ */
476
+ _rollbackMemory(snapshot: MemoryStoreSnapshot): void;
477
+ /**
478
+ * Registers a new index. Idempotent — no-op if the name already exists.
479
+ * Immediately indexes all existing records so the index is consistent.
480
+ */
481
+ createIndex(definition: IndexDefinition): Promise<void>;
482
+ /**
483
+ * Removes a named index. No-op if the index does not exist.
484
+ */
485
+ dropIndex(name: string): Promise<void>;
486
+ /**
487
+ * Returns the first record whose indexed value exactly matches `value`.
488
+ * Intended for unique indexes — for non-unique indexes use findByIndex.
489
+ */
490
+ getByIndex(indexName: string, value: any): Promise<T | undefined>;
491
+ /**
492
+ * Returns all records whose indexed value exactly matches `value`.
493
+ * O(k) where k is the result set size — avoids a full table scan.
494
+ */
495
+ findByIndex(indexName: string, value: any): Promise<T[]>;
496
+ /**
497
+ * Returns all records from a named index within an optional key range.
498
+ */
499
+ getByKeyRange(indexName: string, keyRange?: StoreKeyRange): Promise<T[]>;
442
500
  add(data: T | T[]): Promise<string | number | (string | number)[]>;
443
501
  put(data: T): Promise<string | number>;
444
502
  batch(operations: Array<{
@@ -448,15 +506,47 @@ declare class MemoryStore<T extends Record<string, any>> implements Store<T> {
448
506
  type: "delete";
449
507
  data: string | number | (string | number)[];
450
508
  }>): Promise<void>;
451
- getById(id: string | number): Promise<T | undefined>;
452
509
  delete(id: string | number | (string | number)[]): Promise<void>;
453
510
  clear(): Promise<void>;
454
- count(): Promise<number>;
455
- getByIndex(index: string, key: any): Promise<T | undefined>;
456
- getByKeyRange(indexName: string, keyRange?: StoreKeyRange): Promise<T[]>;
457
- private isInKeyRange;
511
+ /**
512
+ * Applies buffered ops directly to the live data and index maps.
513
+ *
514
+ * `sharedTx` is always null for MemoryStore — there is no shared transaction
515
+ * object. Atomicity across multiple MemoryStores is managed by the caller
516
+ * (TransactionContext), which takes a snapshot via _snapshotMemory() before
517
+ * calling this method and calls _rollbackMemory(snapshot) if a later store
518
+ * in the same transaction fails.
519
+ *
520
+ * This method applies ops eagerly (no staging) because the snapshot already
521
+ * guards against partial failure at the cross-store level.
522
+ */
523
+ executeInTransaction(ops: BufferedOperation<T>[], sharedTx: IDBTransaction | null): Promise<void>;
524
+ getById(id: string | number): Promise<T | undefined>;
458
525
  getAll(): Promise<T[]>;
526
+ count(): Promise<number>;
459
527
  cursor(callback: CursorCallback<T>, direction?: "forward" | "backward", keyRange?: StoreKeyRange): Promise<T | null>;
528
+ private getKey;
529
+ private clone;
530
+ /**
531
+ * Adds a document to all index maps. Skips indexes where the document
532
+ * does not satisfy partial conditions or is missing indexed fields.
533
+ */
534
+ private indexDocument;
535
+ private indexOne;
536
+ /**
537
+ * Removes a document from all index maps.
538
+ */
539
+ private unindexDocument;
540
+ /**
541
+ * Enforces unique index constraints before a write.
542
+ * Throws CONFLICT if another record (other than `existing`) already holds the
543
+ * same indexed value for any unique index.
544
+ *
545
+ * @param incoming - The record about to be written.
546
+ * @param existing - The record currently stored at this key (undefined for new records).
547
+ */
548
+ private enforceUniqueIndexes;
549
+ private isInKeyRange;
460
550
  }
461
551
 
462
552
  declare function createEphemeralStore<T extends Record<string, any>>(config: DatabaseConfig): MemoryStore<T>;
@@ -469,77 +559,125 @@ declare class ConnectionManager {
469
559
  private readonly config;
470
560
  private connectionInitializer;
471
561
  private readonly schemasStoreName;
562
+ constructor(config: DatabaseConfig);
563
+ private openDatabase;
472
564
  /**
473
- * Initializes a new ConnectionManager.
565
+ * Bumps the database version and runs the provided upgrade callback.
566
+ * The callback receives both the IDBDatabase and the active IDBTransaction
567
+ * so callers can access existing object stores via tx.objectStore(name).
474
568
  *
475
- * @param config - The database configuration options.
569
+ * Note: IDB only allows structural changes (createObjectStore, createIndex,
570
+ * deleteIndex) inside an onupgradeneeded handler. This method is the single
571
+ * entry point for all such changes.
476
572
  */
477
- constructor(config: DatabaseConfig);
573
+ private upgradeDatabase;
478
574
  /**
479
- * Opens the database and ensures the base schemas store exists.
575
+ * Ensures a collection object store exists, creating it (and its indexes) if absent.
576
+ * Triggers an upgrade only when the store does not yet exist; if it already exists,
577
+ * this is a fast no-op.
480
578
  *
481
- * @returns A promise that resolves to the opened IDBDatabase instance.
482
- * @throws {Error} If the database fails to open.
579
+ * @param collection - Name of the IDB object store.
580
+ * @param keyPath - Primary key field (default: "$id").
581
+ * @param indexes - Index definitions to create alongside the store.
582
+ * These are only applied during the initial store creation.
583
+ * Use createStoreIndex / dropStoreIndex for post-creation changes.
483
584
  */
484
- private openDatabase;
585
+ ensureStore(collection: string, keyPath?: string, indexes?: IndexDefinition[]): Promise<void>;
485
586
  /**
486
- * Upgrades the database version and executes the provided upgrade logic.
587
+ * Adds a named index to an existing object store.
588
+ * Triggers a database version upgrade.
487
589
  *
488
- * @param currentConnection - The existing active IndexedDB connection to be closed.
489
- * @param upgradeCallback - A callback function containing the structural changes to apply during the upgrade.
490
- * @returns A promise that resolves to the newly upgraded IDBDatabase instance.
491
- * @throws {Error} If the database upgrade fails.
590
+ * @param collection - The object store to add the index to.
591
+ * @param definition - The index definition.
492
592
  */
493
- private upgradeDatabase;
593
+ createStoreIndex(collection: string, definition: IndexDefinition): Promise<void>;
494
594
  /**
495
- * Ensures a collection object store exists within the database.
496
- * Triggers an upgrade if the specified store is missing.
595
+ * Removes a named index from an existing object store.
596
+ * Triggers a database version upgrade.
497
597
  *
498
- * @param collectionName - The name of the collection store to ensure.
499
- * @param keyPath - The primary key path for the collection (defaults to "$id").
500
- * @returns A promise that resolves when the store is confirmed to exist.
598
+ * @param storeName - The object store to remove the index from.
599
+ * @param indexName - The name of the index to remove.
501
600
  */
502
- ensureStore(collectionName: string, keyPath?: string): Promise<void>;
601
+ dropStoreIndex(storeName: string, indexName: string): Promise<void>;
503
602
  /**
504
603
  * Retrieves or opens the active database connection.
505
- * Handles connection loss and version changes automatically.
506
- *
507
- * @returns A promise resolving to the active IDBDatabase connection.
508
- * @throws {DatabaseError} If the connection initialization fails.
509
604
  */
510
605
  getConnection: () => Promise<IDBDatabase>;
511
606
  /**
512
- * Performs a version upgrade using custom structural logic.
513
- * Resets the internal initialization state so subsequent callers wait for the new version.
607
+ * Opens a readwrite IDBTransaction spanning the given store names.
514
608
  *
515
- * @param upgradeLogic - A callback executed during the IndexedDB upgradeneeded event.
516
- * @returns A promise resolving to the upgraded IDBDatabase connection.
517
- * @throws {DatabaseError} If the internal upgrade procedure fails.
609
+ * This is the entry point for all cross-store atomic writes. The returned
610
+ * transaction is NOT managed here the caller (TransactionContext) owns the
611
+ * commit/abort lifecycle by wiring oncomplete / onerror / onabort handlers.
612
+ *
613
+ * All named stores must already exist (i.e. ensureStore must have been called
614
+ * for each before this point). Requesting a store that doesn't exist will cause
615
+ * IDB to throw a DOMException synchronously when the transaction is opened.
616
+ *
617
+ * @param storeNames - Object store names to include in the transaction.
618
+ * @param mode - IDB transaction mode (default: "readwrite").
619
+ */
620
+ openTransaction(storeNames: string[], mode?: IDBTransactionMode): Promise<IDBTransaction>;
621
+ /**
622
+ * Performs a version upgrade. Resets the internal initialiser so concurrent
623
+ * callers wait for the upgraded connection rather than using the stale one.
624
+ *
625
+ * The callback receives both `db` (for creating new stores) and `tx`
626
+ * (for accessing existing stores to add/remove indexes).
518
627
  */
519
- upgrade(upgradeLogic: (db: IDBDatabase) => void): Promise<IDBDatabase>;
628
+ upgrade(upgradeLogic: (db: IDBDatabase, tx: IDBTransaction) => void): Promise<IDBDatabase>;
520
629
  /**
521
- * Closes the active database connection and resets the internal initialization state.
630
+ * Closes the active connection and resets the initialiser.
631
+ * Does not delete any persisted data.
522
632
  */
523
633
  close(): void;
524
634
  }
525
635
 
526
636
  declare class IndexedDBStore<T extends Record<string, any>> implements Store<T> {
527
- private readonly getConnection;
528
- private readonly storeName;
637
+ private readonly connectionManager;
638
+ private readonly collection;
529
639
  private readonly keyPath;
530
- private readonly onOpen;
531
- constructor(getConnection: () => Promise<IDBDatabase>, storeName: string, keyPath: string | undefined, onOpen: () => Promise<void>);
640
+ private readonly indexes;
641
+ constructor(connectionManager: ConnectionManager, collection: string, keyPath?: string, indexes?: IndexDefinition[]);
642
+ name(): string;
643
+ /**
644
+ * Internal escape hatch used by TransactionContext to obtain the shared
645
+ * IDBDatabase so it can open a single multi-store IDBTransaction.
646
+ *
647
+ * Prefixed with underscore to signal that nothing outside of
648
+ * TransactionContext should call this directly.
649
+ */
650
+ _getIDBConnection(): Promise<IDBDatabase>;
651
+ /**
652
+ * Ensures the underlying IDB object store (and its declared indexes) exist.
653
+ * Safe to call multiple times — no-op if the store is already present.
654
+ */
532
655
  open(): Promise<void>;
533
656
  /**
534
- * Map native DOMExceptions to internal DatabaseErrors.
657
+ * Adds a new index to the IDB object store. Triggers a database version upgrade.
658
+ * Idempotent — no-op if the index already exists.
535
659
  */
536
- private mapError;
660
+ createIndex(definition: IndexDefinition): Promise<void>;
537
661
  /**
538
- * Core wrapper for transaction safety. It converts IDB request callbacks
539
- * into a single Promise while preserving transaction activity.
662
+ * Removes a named index from the IDB object store. Triggers a version upgrade.
663
+ * No-op if the index does not exist.
540
664
  */
541
- private withTx;
542
- private requestToPromise;
665
+ dropIndex(name: string): Promise<void>;
666
+ /**
667
+ * Returns the first record matching an exact index key.
668
+ * Use for unique index point lookups.
669
+ */
670
+ getByIndex(indexName: string, key: any): Promise<T | undefined>;
671
+ /**
672
+ * Returns all records from a named index within an optional key range.
673
+ */
674
+ getByKeyRange(indexName: string, keyRange?: StoreKeyRange): Promise<T[]>;
675
+ /**
676
+ * Returns all records whose indexed value exactly matches `value`.
677
+ * Unlike getByIndex, this uses index.getAll(IDBKeyRange.only(value)) so it
678
+ * correctly returns multiple records for non-unique indexes.
679
+ */
680
+ findByIndex(indexName: string, value: any): Promise<T[]>;
543
681
  put(data: T): Promise<string | number>;
544
682
  add(data: T | T[]): Promise<string | number | (string | number)[]>;
545
683
  batch(operations: Array<{
@@ -549,30 +687,46 @@ declare class IndexedDBStore<T extends Record<string, any>> implements Store<T>
549
687
  type: "delete";
550
688
  data: string | number | (string | number)[];
551
689
  }>): Promise<void>;
552
- getById(id: string | number): Promise<T | undefined>;
553
690
  delete(id: string | number | (string | number)[]): Promise<void>;
554
691
  clear(): Promise<void>;
555
- count(): Promise<number>;
556
- getByIndex(indexName: string, key: any): Promise<T | undefined>;
557
- getByKeyRange(indexName: string, keyRange?: StoreKeyRange): Promise<T[]>;
692
+ /**
693
+ * Executes buffered ops against a shared IDBTransaction opened by
694
+ * TransactionContext. This method MUST NOT open, commit, or abort a
695
+ * transaction — the caller owns the transaction lifecycle entirely.
696
+ *
697
+ * @param ops - Operations to apply.
698
+ * @param sharedTx - The IDBTransaction shared across all participating stores.
699
+ * Never null for IndexedDBStore.
700
+ */
701
+ executeInTransaction(ops: BufferedOperation<T>[], sharedTx: IDBTransaction | null): Promise<void>;
702
+ getById(id: string | number): Promise<T | undefined>;
558
703
  getAll(): Promise<T[]>;
704
+ count(): Promise<number>;
559
705
  cursor(callback: CursorCallback<T>, direction?: "forward" | "backward", keyRange?: StoreKeyRange): Promise<T | null>;
706
+ private mapError;
707
+ /**
708
+ * Opens a fresh single-store IDB transaction for standalone (non-atomic)
709
+ * operations. Not used by executeInTransaction — that receives an externally
710
+ * managed shared transaction.
711
+ */
712
+ private withTx;
713
+ private requestToPromise;
560
714
  }
561
715
 
562
- declare const createIndexedDbStore: <T extends Record<string, any>>(config: DatabaseConfig) => Store<T>;
563
-
564
- declare function DatabaseConnection(config: Omit<DatabaseConfig, "keyPath">, createStore: <T extends Record<string, any>>(config: DatabaseConfig) => Store<T>): Promise<Database>;
716
+ /**
717
+ * Retrieves or creates an IndexedDB store instance.
718
+ * * This function utilizes a synchronous execution path to retrieve the
719
+ * ConnectionManager. If the connection is currently being initialized
720
+ * asynchronously, or if the lock is contended, it throws a DatabaseError.
721
+ *
722
+ * @template T - The schema type for the collection.
723
+ * @param config - The database and collection configuration.
724
+ * @returns A functional Store instance.
725
+ * @throws {DatabaseError} CONNECTION_FAILED if the manager is busy or fails to init.
726
+ */
727
+ declare const createIndexedDbStore: <T extends Record<string, any>>(config: StoreConfig) => Store<T>;
565
728
 
566
- declare class IndexManager<T extends Record<string, any>> {
567
- private indexes;
568
- createIndex(field: string): void;
569
- removeIndex(field: string): void;
570
- hasIndex(field: string): boolean;
571
- indexDocument(doc: T): void;
572
- removeDocument(doc: T): void;
573
- lookup(field: string, value: any): Set<string | number> | undefined;
574
- clear(): void;
575
- }
729
+ declare function DatabaseConnection(config: Omit<DatabaseConfig, "keyPath">, createStore: <T extends Record<string, any>>(config: StoreConfig, indexes: IndexDefinition[]) => Store<T>): Promise<Database>;
576
730
 
577
731
  interface MiddlewareContext {
578
732
  collection?: string;
@@ -599,6 +753,11 @@ declare class Mutex {
599
753
  }
600
754
 
601
755
  interface DocumentOptions<T extends Record<string, any>> {
756
+ /**
757
+ * The already-persisted initial state of the document.
758
+ * createDocument does NOT call store.add — the caller is responsible
759
+ * for having written this record before constructing the document proxy.
760
+ */
602
761
  initial: Partial<T>;
603
762
  collection: string;
604
763
  validator?: StandardSchemaV1;
@@ -606,8 +765,18 @@ interface DocumentOptions<T extends Record<string, any>> {
606
765
  bus: EventBus<Record<DocumentEventType | TelemetryEventType, DocumentEvent<T> | TelemetryEvent>>;
607
766
  pipeline: Pipeline;
608
767
  lockManager: Mutex;
609
- indexManager: IndexManager<T>;
610
768
  }
769
+ /**
770
+ * Constructs an in-memory Document proxy around an already-persisted record.
771
+ *
772
+ * Responsibility split:
773
+ * - createDocument: validates, initialises in-memory state, wires operations. NO I/O.
774
+ * - openCollection.create: owns the initial store.add (or tx.addOp for transactional creates).
775
+ *
776
+ * This separation ensures that transactional creates are correctly buffered:
777
+ * the document object is available immediately in-memory, while the actual
778
+ * store write is deferred until transaction commit.
779
+ */
611
780
  declare function createDocument<T extends Record<string, any>>(opts: DocumentOptions<T>): Promise<Document<T & {
612
781
  $id: string | number;
613
782
  }>>;
@@ -620,4 +789,4 @@ declare function openCollection<T extends Record<string, any>>({ collection: sch
620
789
  validate: boolean;
621
790
  }): Promise<Collection<T>>;
622
791
 
623
- export { type Collection, type CollectionEvent, type CollectionEventType, type CollectionMigrationOptions, ConnectionManager, type CursorCallback, type CursorCallbackResult, type CursorPaginationOptions, DEFAULT_KEYPATH, type Database, type DatabaseConfig, DatabaseConnection, type DatabaseEvent, type DatabaseEventType, type Document, type DocumentEvent, type DocumentEventType, IndexedDBStore, type Store, type StoreKeyRange, type TelemetryEvent, type TelemetryEventType, createDocument, createEphemeralStore, createIndexedDbStore, openCollection };
792
+ export { type BufferedOperation, type Collection, type CollectionEvent, type CollectionEventType, type CollectionMigrationOptions, ConnectionManager, type CursorCallback, type CursorCallbackResult, type CursorPaginationOptions, DEFAULT_KEYPATH, type Database, type DatabaseConfig, DatabaseConnection, type DatabaseEvent, type DatabaseEventType, type Document, type DocumentEvent, type DocumentEventType, IndexedDBStore, type Store, type StoreConfig, type StoreKeyRange, type TelemetryEvent, type TelemetryEventType, createDocument, createEphemeralStore, createIndexedDbStore, openCollection };