@asaidimu/utils-database 2.0.0 → 3.1.0

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/README.md CHANGED
@@ -83,7 +83,7 @@ const db = await DatabaseConnection(
83
83
  enableTelemetry: true,
84
84
  predicates: {}, // custom validation predicates (optional)
85
85
  },
86
- createIndexedDbStore
86
+ createIndexedDbStore,
87
87
  );
88
88
  ```
89
89
 
@@ -171,7 +171,7 @@ await db.migrateCollection(
171
171
  },
172
172
  description: "Add active flag",
173
173
  },
174
- 100 // batch size (optional)
174
+ 100, // batch size (optional)
175
175
  );
176
176
  ```
177
177
 
@@ -199,7 +199,9 @@ const unsubDoc = doc.subscribe("document:update", (event) => {
199
199
 
200
200
  // Telemetry (when enableTelemetry: true)
201
201
  db.subscribe("telemetry", (event) => {
202
- console.log(`${event.method} took ${event.metadata.performance.durationMs}ms`);
202
+ console.log(
203
+ `${event.method} took ${event.metadata.performance.durationMs}ms`,
204
+ );
203
205
  });
204
206
  ```
205
207
 
@@ -291,10 +293,10 @@ For migrations: `migrateCollection` streams documents from the store, passes the
291
293
 
292
294
  ### Available Scripts
293
295
 
294
- | Command | Description |
295
- | ----------------- | -------------------------------------------------- |
296
- | `npm test` | Run tests once (Vitest) |
297
- | `npm run test:watch` | Run tests in watch mode |
296
+ | Command | Description |
297
+ | ---------------------- | ------------------------------------------------- |
298
+ | `npm test` | Run tests once (Vitest) |
299
+ | `npm run test:watch` | Run tests in watch mode |
298
300
  | `npm run test:browser` | Run tests in a real browser (Vitest browser mode) |
299
301
 
300
302
  ### Testing
@@ -323,12 +325,12 @@ Use the [GitHub issue tracker](https://github.com/asaidimu/erp-utils/issues) to
323
325
 
324
326
  ### Troubleshooting
325
327
 
326
- | Error | Likely cause & solution |
327
- | --------------------------------- | --------------------------------------------------------- |
328
- | `SCHEMA_NOT_FOUND` | The collection was not created. Call `createCollection` first. |
329
- | `CONFLICT` | Another operation updated the document. Re‑fetch and retry. |
330
- | `TRANSACTION_FAILED` | One of the batched writes failed. Check individual operations. |
331
- | `INVALID_DATA` | The document does not match the schema. Review validation errors. |
328
+ | Error | Likely cause & solution |
329
+ | -------------------- | ----------------------------------------------------------------- |
330
+ | `SCHEMA_NOT_FOUND` | The collection was not created. Call `createCollection` first. |
331
+ | `CONFLICT` | Another operation updated the document. Re‑fetch and retry. |
332
+ | `TRANSACTION_FAILED` | One of the batched writes failed. Check individual operations. |
333
+ | `INVALID_DATA` | The document does not match the schema. Review validation errors. |
332
334
 
333
335
  ### FAQ
334
336
 
@@ -355,6 +357,7 @@ This project is licensed under the **MIT License**. See the [LICENSE](https://gi
355
357
  ### Acknowledgments
356
358
 
357
359
  Built with ❤️ using:
360
+
358
361
  - [`@asaidimu/anansi`](https://github.com/asaidimu/anansi) – schema validation and migrations.
359
362
  - [`@standard-schema/spec`](https://github.com/standard-schema/standard-schema) – Standard Schema interoperability.
360
363
  - [`uuid`](https://github.com/uuidjs/uuid) – for v7 UUIDs.
package/index.d.mts CHANGED
@@ -278,6 +278,16 @@ interface Collection<T> {
278
278
  * @param tx - Optional transaction to buffer the write into.
279
279
  */
280
280
  create: (initial: T, tx?: TransactionContext) => Promise<Document<T>>;
281
+ /**
282
+ * Updates all documents matching the query with the provided partial data.
283
+ * Returns the number of documents updated.
284
+ */
285
+ update: (query: QueryFilter<T>, data: Partial<T>, tx?: TransactionContext) => Promise<number>;
286
+ /**
287
+ * Deletes all documents matching the query.
288
+ * Returns the number of documents deleted.
289
+ */
290
+ delete: (query: QueryFilter<T>, tx?: TransactionContext) => Promise<number>;
281
291
  /**
282
292
  * Subscribes to collection-level events.
283
293
  */
@@ -366,19 +376,22 @@ type DatabaseEvent = {
366
376
  schema?: SchemaDefinition | Partial<SchemaDefinition>;
367
377
  timestamp: number;
368
378
  };
369
- type Document<T> = {
370
- readonly [K in keyof T]: T[K];
371
- } & {
379
+ type DocumentMetadata = {
372
380
  $id?: string;
373
381
  $created?: string | Date;
374
382
  $updated?: string | Date;
375
383
  $version?: number;
384
+ };
385
+ type Document<T> = {
386
+ readonly [K in keyof T]: T[K];
387
+ } & DocumentMetadata & {
376
388
  read: () => Promise<boolean>;
377
389
  save: (tx?: TransactionContext) => Promise<boolean>;
378
390
  update: (props: Partial<T>, tx?: TransactionContext) => Promise<boolean>;
379
391
  delete: (tx?: TransactionContext) => Promise<boolean>;
380
392
  subscribe: (event: DocumentEventType | TelemetryEventType, callback: (event: DocumentEvent<T> | TelemetryEvent) => void) => () => void;
381
393
  state(): T;
394
+ $metadata(): DocumentMetadata;
382
395
  };
383
396
  type DocumentEventType = "document:create" | "document:write" | "document:update" | "document:delete" | "document:read";
384
397
  type DocumentEvent<T> = {
@@ -727,6 +740,73 @@ declare const createIndexedDbStore: <T extends Record<string, any>>(config: Stor
727
740
 
728
741
  declare function DatabaseConnection(config: Omit<DatabaseConfig, "keyPath">, createStore: <T extends Record<string, any>>(config: StoreConfig, indexes: IndexDefinition[]) => Store<T>): Promise<Database>;
729
742
 
743
+ interface MutexOptions {
744
+ /**
745
+ * Maximum number of pending requests allowed in the queue.
746
+ * If exceeded, tryLock/lock will fail.
747
+ * @default Infinity
748
+ */
749
+ capacity?: number;
750
+ /**
751
+ * Controls how lock handoff is scheduled when a waiter is unblocked.
752
+ *
753
+ * - `"macrotask"` (default): Uses setTimeout(fn, 0) to yield to the event
754
+ * loop between handoffs. Prevents microtask starvation under heavy
755
+ * contention — I/O, rendering, and other macrotasks can run between
756
+ * lock acquisitions. Use for Serializer and other coarse-grained
757
+ * serializers.
758
+ *
759
+ * - `"microtask"`: Uses queueMicrotask(fn) for handoff. Near-zero latency
760
+ * between acquisitions — no macrotask delay. Safe when you need
761
+ * back-to-back operations to complete as fast as possible and starvation
762
+ * is not a concern (e.g. Once, Semaphore where builds are infrequent
763
+ * and latency matters more than fairness).
764
+ */
765
+ yieldMode?: "macrotask" | "microtask";
766
+ }
767
+ /**
768
+ * A mutual exclusion lock.
769
+ * Allows only one execution context to access a resource at a time.
770
+ *
771
+ * Yield mode is configurable per instance:
772
+ * - "macrotask" (default): yields between handoffs, preventing microtask starvation.
773
+ * - "microtask": zero-delay handoff for latency-sensitive paths.
774
+ */
775
+ declare class Mutex {
776
+ private _locked;
777
+ private _capacity;
778
+ private _yieldMode;
779
+ private waiters;
780
+ constructor(options?: MutexOptions);
781
+ /**
782
+ * Acquires the lock. If already held, waits until released or timeout reached.
783
+ *
784
+ * @param timeout - Optional maximum wait time in milliseconds.
785
+ * @throws {TimeoutError} If the lock cannot be acquired within the timeout.
786
+ * @throws {Error} If the wait queue is full (backpressure).
787
+ */
788
+ lock(timeout?: number): Promise<void>;
789
+ /**
790
+ * Attempts to acquire the lock without waiting.
791
+ * @returns `true` if the lock was acquired, `false` otherwise.
792
+ */
793
+ tryLock(): boolean;
794
+ /**
795
+ * Releases the lock, scheduling the next waiter according to yieldMode.
796
+ *
797
+ * When a waiter exists, `_locked` intentionally remains `true` — ownership
798
+ * transfers directly to the next waiter without ever clearing the flag.
799
+ * Only when the queue is empty is `_locked` set to false.
800
+ *
801
+ * @throws {Error} If the mutex is not currently locked.
802
+ */
803
+ unlock(): void;
804
+ /** Returns true if the mutex is currently locked. */
805
+ locked(): boolean;
806
+ /** Returns the number of operations waiting for the lock. */
807
+ pending(): number;
808
+ }
809
+
730
810
  /**
731
811
  * Interface defining the shape of the EventBus.
732
812
  * @template TEventMap - A record mapping event names to their respective payload types.
@@ -797,13 +877,6 @@ declare class Pipeline {
797
877
  wrap<T extends object>(target: T, baseContext: Partial<MiddlewareContext>): T;
798
878
  }
799
879
 
800
- declare class Mutex {
801
- private queue;
802
- private locked;
803
- acquire(): Promise<() => void>;
804
- private release;
805
- }
806
-
807
880
  interface DocumentOptions<T extends Record<string, any>> {
808
881
  /**
809
882
  * The already-persisted initial state of the document.
@@ -812,6 +885,7 @@ interface DocumentOptions<T extends Record<string, any>> {
812
885
  */
813
886
  initial: Partial<T>;
814
887
  collection: string;
888
+ schema: SchemaDefinition;
815
889
  validator?: StandardSchemaV1;
816
890
  store: Store<T>;
817
891
  bus: EventBus<Record<DocumentEventType | TelemetryEventType, DocumentEvent<T> | TelemetryEvent>>;
@@ -832,13 +906,14 @@ interface DocumentOptions<T extends Record<string, any>> {
832
906
  declare function createDocument<T extends Record<string, any>>(opts: DocumentOptions<T>): Promise<Document<T & {
833
907
  $id: string | number;
834
908
  }>>;
835
- declare function openCollection<T extends Record<string, any>>({ collection: schema, validator, bus, store, pipeline, validate, }: {
909
+ declare function openCollection<T extends Record<string, any>>({ collection: schemaName, validator, bus, store, pipeline, validate, schema: schemaDefinition, }: {
836
910
  store: Store<T>;
837
911
  collection: string;
838
912
  validator: StandardSchemaV1;
839
913
  bus: EventBus<any>;
840
914
  pipeline: Pipeline;
841
915
  validate: boolean;
916
+ schema: SchemaDefinition;
842
917
  }): Promise<Collection<T>>;
843
918
 
844
919
  /**
@@ -879,13 +954,17 @@ declare class DatabaseError extends Error {
879
954
  * The schema associated with the error, if applicable.
880
955
  */
881
956
  schema?: SchemaDefinition;
957
+ /**
958
+ * Issues associated with the error
959
+ */
960
+ issues?: any[];
882
961
  /**
883
962
  * Constructs a new DatabaseErrorClass instance.
884
963
  * @param type - The type of error that occurred.
885
964
  * @param message - A human-readable message describing the error.
886
965
  * @param schema - The schema associated with the error, if applicable.
887
966
  */
888
- constructor(type: DatabaseErrorType, message: string, schema?: SchemaDefinition, cause?: unknown);
967
+ constructor(type: DatabaseErrorType, message: string, schema?: SchemaDefinition, cause?: unknown, issues?: any);
889
968
  }
890
969
 
891
- 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, DatabaseError, DatabaseErrorType, 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 };
970
+ 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, DatabaseError, DatabaseErrorType, type DatabaseEvent, type DatabaseEventType, type Document, type DocumentEvent, type DocumentEventType, type DocumentMetadata, IndexedDBStore, type Store, type StoreConfig, type StoreKeyRange, type TelemetryEvent, type TelemetryEventType, createDocument, createEphemeralStore, createIndexedDbStore, openCollection };
package/index.d.ts CHANGED
@@ -278,6 +278,16 @@ interface Collection<T> {
278
278
  * @param tx - Optional transaction to buffer the write into.
279
279
  */
280
280
  create: (initial: T, tx?: TransactionContext) => Promise<Document<T>>;
281
+ /**
282
+ * Updates all documents matching the query with the provided partial data.
283
+ * Returns the number of documents updated.
284
+ */
285
+ update: (query: QueryFilter<T>, data: Partial<T>, tx?: TransactionContext) => Promise<number>;
286
+ /**
287
+ * Deletes all documents matching the query.
288
+ * Returns the number of documents deleted.
289
+ */
290
+ delete: (query: QueryFilter<T>, tx?: TransactionContext) => Promise<number>;
281
291
  /**
282
292
  * Subscribes to collection-level events.
283
293
  */
@@ -366,19 +376,22 @@ type DatabaseEvent = {
366
376
  schema?: SchemaDefinition | Partial<SchemaDefinition>;
367
377
  timestamp: number;
368
378
  };
369
- type Document<T> = {
370
- readonly [K in keyof T]: T[K];
371
- } & {
379
+ type DocumentMetadata = {
372
380
  $id?: string;
373
381
  $created?: string | Date;
374
382
  $updated?: string | Date;
375
383
  $version?: number;
384
+ };
385
+ type Document<T> = {
386
+ readonly [K in keyof T]: T[K];
387
+ } & DocumentMetadata & {
376
388
  read: () => Promise<boolean>;
377
389
  save: (tx?: TransactionContext) => Promise<boolean>;
378
390
  update: (props: Partial<T>, tx?: TransactionContext) => Promise<boolean>;
379
391
  delete: (tx?: TransactionContext) => Promise<boolean>;
380
392
  subscribe: (event: DocumentEventType | TelemetryEventType, callback: (event: DocumentEvent<T> | TelemetryEvent) => void) => () => void;
381
393
  state(): T;
394
+ $metadata(): DocumentMetadata;
382
395
  };
383
396
  type DocumentEventType = "document:create" | "document:write" | "document:update" | "document:delete" | "document:read";
384
397
  type DocumentEvent<T> = {
@@ -727,6 +740,73 @@ declare const createIndexedDbStore: <T extends Record<string, any>>(config: Stor
727
740
 
728
741
  declare function DatabaseConnection(config: Omit<DatabaseConfig, "keyPath">, createStore: <T extends Record<string, any>>(config: StoreConfig, indexes: IndexDefinition[]) => Store<T>): Promise<Database>;
729
742
 
743
+ interface MutexOptions {
744
+ /**
745
+ * Maximum number of pending requests allowed in the queue.
746
+ * If exceeded, tryLock/lock will fail.
747
+ * @default Infinity
748
+ */
749
+ capacity?: number;
750
+ /**
751
+ * Controls how lock handoff is scheduled when a waiter is unblocked.
752
+ *
753
+ * - `"macrotask"` (default): Uses setTimeout(fn, 0) to yield to the event
754
+ * loop between handoffs. Prevents microtask starvation under heavy
755
+ * contention — I/O, rendering, and other macrotasks can run between
756
+ * lock acquisitions. Use for Serializer and other coarse-grained
757
+ * serializers.
758
+ *
759
+ * - `"microtask"`: Uses queueMicrotask(fn) for handoff. Near-zero latency
760
+ * between acquisitions — no macrotask delay. Safe when you need
761
+ * back-to-back operations to complete as fast as possible and starvation
762
+ * is not a concern (e.g. Once, Semaphore where builds are infrequent
763
+ * and latency matters more than fairness).
764
+ */
765
+ yieldMode?: "macrotask" | "microtask";
766
+ }
767
+ /**
768
+ * A mutual exclusion lock.
769
+ * Allows only one execution context to access a resource at a time.
770
+ *
771
+ * Yield mode is configurable per instance:
772
+ * - "macrotask" (default): yields between handoffs, preventing microtask starvation.
773
+ * - "microtask": zero-delay handoff for latency-sensitive paths.
774
+ */
775
+ declare class Mutex {
776
+ private _locked;
777
+ private _capacity;
778
+ private _yieldMode;
779
+ private waiters;
780
+ constructor(options?: MutexOptions);
781
+ /**
782
+ * Acquires the lock. If already held, waits until released or timeout reached.
783
+ *
784
+ * @param timeout - Optional maximum wait time in milliseconds.
785
+ * @throws {TimeoutError} If the lock cannot be acquired within the timeout.
786
+ * @throws {Error} If the wait queue is full (backpressure).
787
+ */
788
+ lock(timeout?: number): Promise<void>;
789
+ /**
790
+ * Attempts to acquire the lock without waiting.
791
+ * @returns `true` if the lock was acquired, `false` otherwise.
792
+ */
793
+ tryLock(): boolean;
794
+ /**
795
+ * Releases the lock, scheduling the next waiter according to yieldMode.
796
+ *
797
+ * When a waiter exists, `_locked` intentionally remains `true` — ownership
798
+ * transfers directly to the next waiter without ever clearing the flag.
799
+ * Only when the queue is empty is `_locked` set to false.
800
+ *
801
+ * @throws {Error} If the mutex is not currently locked.
802
+ */
803
+ unlock(): void;
804
+ /** Returns true if the mutex is currently locked. */
805
+ locked(): boolean;
806
+ /** Returns the number of operations waiting for the lock. */
807
+ pending(): number;
808
+ }
809
+
730
810
  /**
731
811
  * Interface defining the shape of the EventBus.
732
812
  * @template TEventMap - A record mapping event names to their respective payload types.
@@ -797,13 +877,6 @@ declare class Pipeline {
797
877
  wrap<T extends object>(target: T, baseContext: Partial<MiddlewareContext>): T;
798
878
  }
799
879
 
800
- declare class Mutex {
801
- private queue;
802
- private locked;
803
- acquire(): Promise<() => void>;
804
- private release;
805
- }
806
-
807
880
  interface DocumentOptions<T extends Record<string, any>> {
808
881
  /**
809
882
  * The already-persisted initial state of the document.
@@ -812,6 +885,7 @@ interface DocumentOptions<T extends Record<string, any>> {
812
885
  */
813
886
  initial: Partial<T>;
814
887
  collection: string;
888
+ schema: SchemaDefinition;
815
889
  validator?: StandardSchemaV1;
816
890
  store: Store<T>;
817
891
  bus: EventBus<Record<DocumentEventType | TelemetryEventType, DocumentEvent<T> | TelemetryEvent>>;
@@ -832,13 +906,14 @@ interface DocumentOptions<T extends Record<string, any>> {
832
906
  declare function createDocument<T extends Record<string, any>>(opts: DocumentOptions<T>): Promise<Document<T & {
833
907
  $id: string | number;
834
908
  }>>;
835
- declare function openCollection<T extends Record<string, any>>({ collection: schema, validator, bus, store, pipeline, validate, }: {
909
+ declare function openCollection<T extends Record<string, any>>({ collection: schemaName, validator, bus, store, pipeline, validate, schema: schemaDefinition, }: {
836
910
  store: Store<T>;
837
911
  collection: string;
838
912
  validator: StandardSchemaV1;
839
913
  bus: EventBus<any>;
840
914
  pipeline: Pipeline;
841
915
  validate: boolean;
916
+ schema: SchemaDefinition;
842
917
  }): Promise<Collection<T>>;
843
918
 
844
919
  /**
@@ -879,13 +954,17 @@ declare class DatabaseError extends Error {
879
954
  * The schema associated with the error, if applicable.
880
955
  */
881
956
  schema?: SchemaDefinition;
957
+ /**
958
+ * Issues associated with the error
959
+ */
960
+ issues?: any[];
882
961
  /**
883
962
  * Constructs a new DatabaseErrorClass instance.
884
963
  * @param type - The type of error that occurred.
885
964
  * @param message - A human-readable message describing the error.
886
965
  * @param schema - The schema associated with the error, if applicable.
887
966
  */
888
- constructor(type: DatabaseErrorType, message: string, schema?: SchemaDefinition, cause?: unknown);
967
+ constructor(type: DatabaseErrorType, message: string, schema?: SchemaDefinition, cause?: unknown, issues?: any);
889
968
  }
890
969
 
891
- 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, DatabaseError, DatabaseErrorType, 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 };
970
+ 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, DatabaseError, DatabaseErrorType, type DatabaseEvent, type DatabaseEventType, type Document, type DocumentEvent, type DocumentEventType, type DocumentMetadata, IndexedDBStore, type Store, type StoreConfig, type StoreKeyRange, type TelemetryEvent, type TelemetryEventType, createDocument, createEphemeralStore, createIndexedDbStore, openCollection };