idb-ts 3.14.0 → 3.15.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
@@ -55,18 +55,18 @@ yarn add idb-ts
55
55
 
56
56
  ## Feature Overview
57
57
 
58
- | Feature | Description |
59
- |---|---|
60
- | **Declarative entity definition** | Define stores, keys, and indexes with class decorators |
61
- | **Full CRUD API** | Create, read, update, delete, list, paginate, and count |
62
- | **Typed query builder** | Chainable, type-checked filter, sort, and aggregation DSL |
63
- | **Key generation** | Auto-increment, UUID v4, timestamp, random, or custom function |
64
- | **Composite keys** | Multi-field primary keys for relational associations |
65
- | **Field validation** | Per-property predicate rules enforced on write |
66
- | **Schema versioning** | Automatic `onupgradeneeded` migration based on entity versions |
67
- | **Transaction API** | Callback-based and explicit commit/rollback patterns |
68
- | **Data retention** | Periodic background cleanup of expired records |
69
- | **Automatic timestamps** | `__idb_createdAt` / `__idb_updatedAt` injected on every write |
58
+ | Feature | Description |
59
+ | --------------------------------- | -------------------------------------------------------------- |
60
+ | **Declarative entity definition** | Define stores, keys, and indexes with class decorators |
61
+ | **Full CRUD API** | Create, read, update, delete, list, paginate, and count |
62
+ | **Typed query builder** | Chainable, type-checked filter, sort, and aggregation DSL |
63
+ | **Key generation** | Auto-increment, UUID v4, timestamp, random, or custom function |
64
+ | **Composite keys** | Multi-field primary keys for relational associations |
65
+ | **Field validation** | Per-property predicate rules enforced on write |
66
+ | **Schema versioning** | Automatic `onupgradeneeded` migration based on entity versions |
67
+ | **Transaction API** | Callback-based and explicit commit/rollback patterns |
68
+ | **Data retention** | Periodic background cleanup of expired records |
69
+ | **Automatic timestamps** | `__idb_createdAt` / `__idb_updatedAt` injected on every write |
70
70
 
71
71
  ---
72
72
 
@@ -88,9 +88,16 @@ class User {
88
88
  age!: number;
89
89
  }
90
90
 
91
- const db = await Database.build<{ User: EntityRepository<User> }>('mydb', [User]);
91
+ const db = await Database.build<{ User: EntityRepository<User> }>('mydb', [
92
+ User,
93
+ ]);
92
94
 
93
- await db.User.create({ id: '', name: 'Alice', age: 30, email: 'alice@example.com' });
95
+ await db.User.create({
96
+ id: '',
97
+ name: 'Alice',
98
+ age: 30,
99
+ email: 'alice@example.com',
100
+ });
94
101
  const alice = await db.User.findOneByIndex('email', 'alice@example.com');
95
102
  ```
96
103
 
@@ -109,7 +116,10 @@ class User {
109
116
  id!: string;
110
117
 
111
118
  @Index({ unique: true })
112
- @Validate((v) => typeof v === 'string' && v.includes('@'), 'must be a valid email')
119
+ @Validate(
120
+ (v) => typeof v === 'string' && v.includes('@'),
121
+ 'must be a valid email',
122
+ )
113
123
  email!: string;
114
124
 
115
125
  @Validate((v) => typeof v === 'number' && v >= 0, 'age must be non-negative')
@@ -125,18 +135,18 @@ class User {
125
135
 
126
136
  Marks a class as a managed entity. Must be applied exactly once per class, after all other idb-ts decorators.
127
137
 
128
- | Option | Type | Default | Description |
129
- |---|---|---|---|
130
- | `version` | `number` | `1` | Schema version. Increment when the entity's store or indexes change. |
138
+ | Option | Type | Default | Description |
139
+ | --------- | -------- | ------- | -------------------------------------------------------------------- |
140
+ | `version` | `number` | `1` | Schema version. Increment when the entity's store or indexes change. |
131
141
 
132
142
  #### `@KeyPath(options?)`
133
143
 
134
144
  Designates the decorated property as the primary key of the object store. Exactly one property per class may carry this decorator. For multi-field keys, use `@CompositeKeyPath` at the class level instead.
135
145
 
136
- | Option | Type | Default | Description |
137
- |---|---|---|---|
138
- | `autoIncrement` | `boolean` | `false` | Delegate key assignment to IndexedDB's auto-increment mechanism. |
139
- | `generator` | `'uuid'` \| `'timestamp'` \| `'random'` \| `(item) => string \| number` | - | Automatic key generator invoked when the key field is absent or empty on `create`. |
146
+ | Option | Type | Default | Description |
147
+ | --------------- | ----------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------- |
148
+ | `autoIncrement` | `boolean` | `false` | Delegate key assignment to IndexedDB's auto-increment mechanism. |
149
+ | `generator` | `'uuid'` \| `'timestamp'` \| `'random'` \| `(item) => string \| number` | - | Automatic key generator invoked when the key field is absent or empty on `create`. |
140
150
 
141
151
  #### `@CompositeKeyPath(fields, options?)`
142
152
 
@@ -156,8 +166,8 @@ class UserProject {
156
166
 
157
167
  Creates an IDB index on the decorated field, enabling efficient lookups via `findByIndex` and `findOneByIndex`.
158
168
 
159
- | Option | Type | Description |
160
- |---|---|---|
169
+ | Option | Type | Description |
170
+ | -------- | --------- | ---------------------------------------- |
161
171
  | `unique` | `boolean` | Enforce uniqueness on the indexed field. |
162
172
 
163
173
  #### `@Validate(predicate, message)`
@@ -174,7 +184,7 @@ Class-level decorator that configures automatic expiry and deletion of records.
174
184
 
175
185
  ```typescript
176
186
  const db = await Database.build<{
177
- User: EntityRepository<User>;
187
+ User: EntityRepository<User>;
178
188
  Order: EntityRepository<Order>;
179
189
  }>('shop', [User, Order]);
180
190
  ```
@@ -186,10 +196,10 @@ The effective database version is the highest `version` value declared across al
186
196
  ### Inspecting database metadata
187
197
 
188
198
  ```typescript
189
- db.getDatabaseVersion(); // number - current IDB version
190
- db.getEntityVersions(); // Map<string, number>
191
- db.getEntityVersion('User'); // number | undefined
192
- db.getAvailableEntities(); // string[]
199
+ db.getDatabaseVersion(); // number - current IDB version
200
+ db.getEntityVersions(); // Map<string, number>
201
+ db.getEntityVersion('User'); // number | undefined
202
+ db.getAvailableEntities(); // string[]
193
203
  ```
194
204
 
195
205
  ### Closing the connection
@@ -210,9 +220,9 @@ await db.User.create(user);
210
220
  await db.User.createMany([alice, bob, charlie]);
211
221
 
212
222
  // Read
213
- const user = await db.User.read('u1'); // by primary key
223
+ const user = await db.User.read('u1'); // by primary key
214
224
  const page = await db.User.listPaginated(1, 20); // 1-based pagination
215
- const all = await db.User.list();
225
+ const all = await db.User.list();
216
226
 
217
227
  // Update
218
228
  await db.User.update(updatedUser);
@@ -224,7 +234,7 @@ await db.User.deleteMany(['u1', 'u2']);
224
234
  await db.User.deleteWhere((q) => q.where('age').lt(18));
225
235
 
226
236
  // Utilities
227
- const count = await db.User.count();
237
+ const count = await db.User.count();
228
238
  const exists = await db.User.exists('u1');
229
239
  await db.User.clear();
230
240
  ```
@@ -232,7 +242,7 @@ await db.User.clear();
232
242
  ### Index lookups
233
243
 
234
244
  ```typescript
235
- const allAdmins = await db.User.findByIndex('role', 'admin');
245
+ const allAdmins = await db.User.findByIndex('role', 'admin');
236
246
  const firstAdmin = await db.User.findOneByIndex('role', 'admin');
237
247
  ```
238
248
 
@@ -244,9 +254,9 @@ Querying a non-existent index throws immediately.
244
254
 
245
255
  Every record written through a repository automatically receives two internal fields:
246
256
 
247
- | Field | Type | Set on |
248
- |---|---|---|
249
- | `__idb_createdAt` | `number` (ms since epoch) | `create` only |
257
+ | Field | Type | Set on |
258
+ | ----------------- | ------------------------- | --------------------- |
259
+ | `__idb_createdAt` | `number` (ms since epoch) | `create` only |
250
260
  | `__idb_updatedAt` | `number` (ms since epoch) | `create` and `update` |
251
261
 
252
262
  `__idb_createdAt` is preserved across updates; `__idb_updatedAt` is refreshed on every write.
@@ -266,25 +276,27 @@ console.log(item.__idb_createdAt, item.__idb_updatedAt);
266
276
 
267
277
  ```typescript
268
278
  const results = await db.User.query()
269
- .where('age').gte(18)
270
- .and('status').equals('active')
279
+ .where('age')
280
+ .gte(18)
281
+ .and('status')
282
+ .equals('active')
271
283
  .execute();
272
284
  ```
273
285
 
274
286
  #### Available operators
275
287
 
276
- | Operator | Field types | Description |
277
- |---|---|---|
278
- | `equals` | any | Strict equality (`===`) |
279
- | `gt` / `gte` / `lt` / `lte` | `ComparableValue` | Comparison |
280
- | `between(start, end)` | `ComparableValue` | Inclusive range |
281
- | `notBetween(start, end)` | `ComparableValue` | Outside range |
282
- | `startsWith` / `endsWith` | `string` | Prefix / suffix match |
283
- | `contains` | `string` \| array | Substring or element membership |
284
- | `matches` | `string` | Regular expression test |
285
- | `in(values)` / `notIn(values)` | any | Membership test |
286
- | `containsAny(values)` | array | At least one element matches |
287
- | `containsAll(values)` | array | All elements present |
288
+ | Operator | Field types | Description |
289
+ | ------------------------------ | ----------------- | ------------------------------- |
290
+ | `equals` | any | Strict equality (`===`) |
291
+ | `gt` / `gte` / `lt` / `lte` | `ComparableValue` | Comparison |
292
+ | `between(start, end)` | `ComparableValue` | Inclusive range |
293
+ | `notBetween(start, end)` | `ComparableValue` | Outside range |
294
+ | `startsWith` / `endsWith` | `string` | Prefix / suffix match |
295
+ | `contains` | `string` \| array | Substring or element membership |
296
+ | `matches` | `string` | Regular expression test |
297
+ | `in(values)` / `notIn(values)` | any | Membership test |
298
+ | `containsAny(values)` | array | At least one element matches |
299
+ | `containsAll(values)` | array | All elements present |
288
300
 
289
301
  TypeScript enforces operator/type compatibility at compile time - string-only operators are not exposed on numeric fields, and so on.
290
302
 
@@ -293,9 +305,11 @@ TypeScript enforces operator/type compatibility at compile time - string-only op
293
305
  ```typescript
294
306
  // OR connector
295
307
  const results = await db.User.query()
296
- .where('age').gte(18)
308
+ .where('age')
309
+ .gte(18)
297
310
  .or()
298
- .where('hasParentalConsent').equals(true)
311
+ .where('hasParentalConsent')
312
+ .equals(true)
299
313
  .execute();
300
314
 
301
315
  // Grouped sub-expression
@@ -304,7 +318,8 @@ const premiumOrTrial = await db.User.query()
304
318
  qb.where('type').equals('premium').and('status').equals('active'),
305
319
  )
306
320
  .or()
307
- .where('isTrial').equals(true)
321
+ .where('isTrial')
322
+ .equals(true)
308
323
  .execute();
309
324
  ```
310
325
 
@@ -312,7 +327,8 @@ const premiumOrTrial = await db.User.query()
312
327
 
313
328
  ```typescript
314
329
  await db.User.query()
315
- .where('status').equals('active')
330
+ .where('status')
331
+ .equals('active')
316
332
  .orderBy('createdAt', 'desc')
317
333
  .offset(20)
318
334
  .limit(10)
@@ -324,10 +340,7 @@ await db.User.query()
324
340
  When a field is indexed, you can constrain the initial IDB candidate set at the storage layer before in-memory filtering begins:
325
341
 
326
342
  ```typescript
327
- await db.Product.query()
328
- .useIndex('price')
329
- .range(10, 100)
330
- .execute();
343
+ await db.Product.query().useIndex('price').range(10, 100).execute();
331
344
  ```
332
345
 
333
346
  ### Aggregations
@@ -356,7 +369,7 @@ const byStatus = await db.Order.query().groupBy('status').count();
356
369
  @DataClass()
357
370
  class Task {
358
371
  @KeyPath({ autoIncrement: true })
359
- id!: number; // Assigned by IndexedDB: 1, 2, 3, …
372
+ id!: number; // Assigned by IndexedDB: 1, 2, 3, …
360
373
 
361
374
  title!: string;
362
375
  }
@@ -367,7 +380,7 @@ class Task {
367
380
  ```typescript
368
381
  @DataClass()
369
382
  class Document {
370
- @KeyPath({ generator: 'uuid' }) // RFC 4122 v4
383
+ @KeyPath({ generator: 'uuid' }) // RFC 4122 v4
371
384
  id!: string;
372
385
  }
373
386
 
@@ -379,7 +392,7 @@ class Event {
379
392
 
380
393
  @DataClass()
381
394
  class Session {
382
- @KeyPath({ generator: 'random' }) // Base-36 random string
395
+ @KeyPath({ generator: 'random' }) // Base-36 random string
383
396
  id!: string;
384
397
  }
385
398
  ```
@@ -406,9 +419,9 @@ class Invoice {
406
419
  ```typescript
407
420
  import { KeyGenerators } from 'idb-ts';
408
421
 
409
- KeyGenerators.uuid(); // "a1b2c3d4-..."
422
+ KeyGenerators.uuid(); // "a1b2c3d4-..."
410
423
  KeyGenerators.timestamp(); // 1696118400000
411
- KeyGenerators.random(); // "xyz789abc"
424
+ KeyGenerators.random(); // "xyz789abc"
412
425
  ```
413
426
 
414
427
  ### Composite keys
@@ -452,7 +465,10 @@ class User {
452
465
  )
453
466
  email!: string;
454
467
 
455
- @Validate((v) => Number.isInteger(v) && v >= 0, 'must be a non-negative integer')
468
+ @Validate(
469
+ (v) => Number.isInteger(v) && v >= 0,
470
+ 'must be a non-negative integer',
471
+ )
456
472
  age!: number;
457
473
  }
458
474
  ```
@@ -514,11 +530,11 @@ class Session {
514
530
  }
515
531
  ```
516
532
 
517
- | Option | Type | Default | Description |
518
- |---|---|---|---|
519
- | `seconds` | `number` | - | **(Required)** Retention window in seconds. Must be a positive integer. |
520
- | `enabled` | `boolean` | `true` | Set to `false` to suspend cleanup without removing the policy. |
521
- | `field` | `string` | `'__idb_createdAt'` | Numeric timestamp field used to compute record age. |
533
+ | Option | Type | Default | Description |
534
+ | --------- | --------- | ------------------- | ----------------------------------------------------------------------- |
535
+ | `seconds` | `number` | - | **(Required)** Retention window in seconds. Must be a positive integer. |
536
+ | `enabled` | `boolean` | `true` | Set to `false` to suspend cleanup without removing the policy. |
537
+ | `field` | `string` | `'__idb_createdAt'` | Numeric timestamp field used to compute record age. |
522
538
 
523
539
  When multiple entities define retention policies, the cleanup interval is set to the GCD of all configured `seconds` values in milliseconds, so a single timer satisfies every policy efficiently. The job runs immediately on database open and then on each interval tick, using cursor-based `readwrite` transactions.
524
540
 
@@ -529,9 +545,18 @@ When multiple entities define retention policies, the cleanup interval is set to
529
545
  Increment an entity's `version` to trigger `onupgradeneeded` and update its object store on the user's next visit. The effective database version is the maximum across all registered entities, so adding a new high-version entity is sufficient to initiate a migration.
530
546
 
531
547
  ```typescript
532
- @DataClass({ version: 1 }) class User { /* ... */ }
533
- @DataClass({ version: 2 }) class Post { /* ... */ }
534
- @DataClass({ version: 3 }) class Comment { /* ... */ }
548
+ @DataClass({ version: 1 })
549
+ class User {
550
+ /* ... */
551
+ }
552
+ @DataClass({ version: 2 })
553
+ class Post {
554
+ /* ... */
555
+ }
556
+ @DataClass({ version: 3 })
557
+ class Comment {
558
+ /* ... */
559
+ }
535
560
 
536
561
  // Database opens at version 3.
537
562
  // If a user was on version 1, only Post (v2) and Comment (v3) stores are
@@ -555,13 +580,112 @@ await db.User.deleteMany(['u1', 'u2', 'u3']);
555
580
 
556
581
  ---
557
582
 
583
+ ## Performance
584
+
585
+ <!-- performance start -->
586
+
587
+ Already up to date
588
+ Done in 443ms using pnpm v11.5.1
589
+
590
+ ### Suite 1: CRUD Operations
591
+
592
+ | Operation | Ops | Total ms | Ops/s | Avg ms | P50 | P95 | P99 | Min | Max |
593
+ |-----------|-----:|---------:|------:|-------:|----:|----:|----:|----:|----:|
594
+ | create (single) | 200 | 15.46 | 12,936.753 | 0.077 | 0.058 | 0.137 | 0.162 | 0.052 | 1.01 |
595
+ | read (by PK) | 200 | 10.987 | 18,203.475 | 0.054 | 0.047 | 0.089 | 0.163 | 0.04 | 0.175 |
596
+ | update (single) | 200 | 121.183 | 1,650.4 | 0.605 | 0.535 | 0.991 | 1.739 | 0.459 | 2.227 |
597
+ | findByIndex (email) | 200 | 11.218 | 17,828.706 | 0.056 | 0.052 | 0.083 | 0.111 | 0.044 | 0.143 |
598
+ | findOneByIndex (email) | 200 | 13.331 | 15,002.256 | 0.066 | 0.057 | 0.081 | 0.103 | 0.048 | 1.11 |
599
+ | count | 200 | 10.209 | 19,589.884 | 0.051 | 0.049 | 0.064 | 0.088 | 0.044 | 0.101 |
600
+ | exists | 200 | 16.586 | 12,058.587 | 0.083 | 0.064 | 0.097 | 0.18 | 0.057 | 2.444 |
601
+ | list (all) | 50 | 97.836 | 511.058 | 1.956 | 1.724 | 4.683 | 7.585 | 1.481 | 7.585 |
602
+ | listPaginated (1, 20) | 200 | 374.357 | 534.25 | 1.871 | 1.692 | 2.005 | 9.138 | 1.447 | 12.441 |
603
+ | query().where().gte().execute() | 100 | 182.997 | 546.458 | 1.829 | 1.542 | 1.975 | 2.457 | 1.485 | 14.388 |
604
+ | delete (single) | 200 | 138.608 | 1,442.92 | 0.693 | 0.658 | 0.764 | 0.857 | 0.556 | 5.689 |
605
+
606
+
607
+ ### Suite 2: Batched CRUD (by batch size)
608
+
609
+ | Operation | Ops | Total ms | Ops/s | Avg ms | P50 | P95 | P99 | Min | Max |
610
+ |-----------|-----:|---------:|------:|-------:|----:|----:|----:|----:|----:|
611
+ | createMany (10) | 3 | 1.482 | 2,023.901 | 0.493 | 0.531 | 0.534 | 0.534 | 0.415 | 0.534 |
612
+ | read batch (10 keys) | 3 | 0.522 | 5,741.792 | 0.174 | 0.169 | 0.191 | 0.191 | 0.161 | 0.191 |
613
+ | updateMany (10) | 3 | 5.066 | 592.172 | 1.688 | 1.488 | 2.093 | 2.093 | 1.482 | 2.093 |
614
+ | deleteMany (10) | 3 | 3.06 | 980.235 | 1.02 | 1.031 | 1.099 | 1.099 | 0.929 | 1.099 |
615
+ | deleteWhere (10+ match) | 3 | 0.984 | 3,048.328 | 0.327 | 0.302 | 0.401 | 0.401 | 0.279 | 0.401 |
616
+ | createMany (50) | 3 | 5.753 | 521.463 | 1.917 | 1.933 | 2.106 | 2.106 | 1.712 | 2.106 |
617
+ | read batch (50 keys) | 3 | 4.12 | 728.179 | 1.372 | 1.386 | 1.468 | 1.468 | 1.263 | 1.468 |
618
+ | updateMany (50) | 3 | 78.35 | 38.29 | 26.116 | 25.63 | 28.291 | 28.291 | 24.426 | 28.291 |
619
+ | deleteMany (50) | 3 | 76.547 | 39.191 | 25.514 | 24.517 | 27.751 | 27.751 | 24.276 | 27.751 |
620
+ | deleteWhere (50+ match) | 3 | 3.43 | 874.636 | 1.143 | 1.069 | 1.294 | 1.294 | 1.065 | 1.294 |
621
+ | createMany (100) | 3 | 11.795 | 254.341 | 3.931 | 4.058 | 4.244 | 4.244 | 3.491 | 4.244 |
622
+ | read batch (100 keys) | 3 | 29.324 | 102.305 | 9.773 | 8.39 | 14.867 | 14.867 | 6.062 | 14.867 |
623
+ | updateMany (100) | 3 | 272.998 | 10.989 | 90.997 | 89.999 | 95.133 | 95.133 | 87.86 | 95.133 |
624
+ | deleteMany (100) | 3 | 270.164 | 11.104 | 90.048 | 88.568 | 98.799 | 98.799 | 82.777 | 98.799 |
625
+ | deleteWhere (100+ match) | 3 | 6.852 | 437.839 | 2.283 | 2.136 | 2.591 | 2.591 | 2.123 | 2.591 |
626
+ | createMany (500) | 3 | 89.252 | 33.613 | 29.749 | 31.124 | 34.061 | 34.061 | 24.062 | 34.061 |
627
+ | read batch (500 keys) | 3 | 163.408 | 18.359 | 54.467 | 59.069 | 60.496 | 60.496 | 43.837 | 60.496 |
628
+ | updateMany (500) | 3 | 7,195.857 | 0.417 | 2,398.617 | 2,397.101 | 2,406.21 | 2,406.21 | 2,392.539 | 2,406.21 |
629
+ | deleteMany (500) | 3 | 7,951.45 | 0.377 | 2,650.481 | 2,603.507 | 2,912.156 | 2,912.156 | 2,435.78 | 2,912.156 |
630
+ | deleteWhere (500+ match) | 3 | 36.772 | 81.584 | 12.256 | 11.967 | 12.995 | 12.995 | 11.806 | 12.995 |
631
+
632
+
633
+ ### Suite 3: Mixed CRUD Operations
634
+
635
+ | Operation | Ops | Total ms | Ops/s | Avg ms | P50 | P95 | P99 | Min | Max |
636
+ |-----------|-----:|---------:|------:|-------:|----:|----:|----:|----:|----:|
637
+ | Read-heavy mix (70R/15U/10C/5D) | 200 | 29.799 | 6,711.698 | 0.149 | 0.029 | 0.606 | 0.619 | 0.016 | 0.652 |
638
+ | Write-heavy mix (20R/15U/50C/15D) | 200 | 41.659 | 4,800.892 | 0.208 | 0.042 | 0.582 | 0.608 | 0.022 | 3.973 |
639
+ | Mixed CRUD + queries | 200 | 118.964 | 1,681.186 | 0.594 | 0.051 | 2.065 | 7.636 | 0.013 | 10.791 |
640
+ | Cross-entity mix (User.read + Order.create) | 200 | 7.316 | 27,336.962 | 0.036 | 0.03 | 0.069 | 0.081 | 0.021 | 0.091 |
641
+
642
+
643
+ ### Suite 4: Mixed Batched CRUD
644
+
645
+ | Operation | Ops | Total ms | Ops/s | Avg ms | P50 | P95 | P99 | Min | Max |
646
+ |-----------|-----:|---------:|------:|-------:|----:|----:|----:|----:|----:|
647
+ | Cycle: createMany -> readAll -> updateMany -> deleteMany (50) | 5 | 66.384 | 75.319 | 13.276 | 13.426 | 17.594 | 17.594 | 10.02 | 17.594 |
648
+ | createMany -> query filter -> deleteMany (50) | 5 | 36.888 | 135.546 | 7.377 | 6.693 | 9.052 | 9.052 | 6.469 | 9.052 |
649
+ | 5 waves × createMany(50) + deleteMany(50) | 3 | 249.923 | 12.004 | 83.307 | 85.413 | 85.568 | 85.568 | 78.939 | 85.568 |
650
+ | Cross-entity batch: createMany(User) + createMany(Order) + deleteMany (×50) | 3 | 140.09 | 21.415 | 46.696 | 47.77 | 51.181 | 51.181 | 41.137 | 51.181 |
651
+
652
+
653
+ ### Suite 5: Transaction Operations
654
+
655
+ | Operation | Ops | Total ms | Ops/s | Avg ms | P50 | P95 | P99 | Min | Max |
656
+ |-----------|-----:|---------:|------:|-------:|----:|----:|----:|----:|----:|
657
+ | tx: single create | 100 | 8.886 | 11,253.788 | 0.089 | 0.073 | 0.178 | 0.228 | 0.07 | 0.253 |
658
+ | tx: create 10 users | 100 | 25.153 | 3,975.637 | 0.251 | 0.238 | 0.288 | 0.489 | 0.223 | 0.517 |
659
+ | tx: read + update | 100 | 159.349 | 627.553 | 1.593 | 1.525 | 1.63 | 2.939 | 1.499 | 5.745 |
660
+ | tx: multi-entity create (User+Order+Session) | 100 | 9.628 | 10,386.133 | 0.096 | 0.087 | 0.128 | 0.189 | 0.081 | 0.23 |
661
+ | tx: 10 reads | 100 | 20.01 | 4,997.52 | 0.2 | 0.169 | 0.343 | 0.399 | 0.153 | 0.441 |
662
+ | tx: batch create 50 users | 20 | 24.816 | 805.936 | 1.24 | 1.019 | 1.284 | 4.769 | 0.976 | 4.769 |
663
+ | tx: query().where().gte() | 100 | 933.212 | 107.157 | 9.331 | 8.393 | 19.189 | 19.988 | 7.73 | 19.998 |
664
+ | tx (explicit): begin -> create -> commit | 100 | 6.974 | 14,337.972 | 0.07 | 0.065 | 0.091 | 0.108 | 0.061 | 0.162 |
665
+
666
+
667
+ ### Suite 6: Mixed Transactions
668
+
669
+ | Operation | Ops | Total ms | Ops/s | Avg ms | P50 | P95 | P99 | Min | Max |
670
+ |-----------|-----:|---------:|------:|-------:|----:|----:|----:|----:|----:|
671
+ | tx mixed: read User -> create Order -> update User | 100 | 51.695 | 1,934.439 | 0.517 | 0.41 | 0.485 | 0.724 | 0.38 | 10.259 |
672
+ | tx mixed: query User + read Order + create Session | 100 | 113.605 | 880.244 | 1.136 | 1.037 | 1.134 | 1.274 | 1.018 | 9.734 |
673
+ | tx multi-entity: create User+Order+Session | 100 | 11.079 | 9,025.916 | 0.111 | 0.106 | 0.138 | 0.143 | 0.1 | 0.166 |
674
+ | tx batched: create 20 Users + 40 Orders + 20 Sessions | 10 | 22.237 | 449.703 | 2.223 | 1.238 | 11.051 | 11.051 | 1.164 | 11.051 |
675
+ | tx mixed: delete old orders -> create new orders | 100 | 104.138 | 960.266 | 1.041 | 0.971 | 1.09 | 1.189 | 0.829 | 8.342 |
676
+ | tx mixed: count Orders -> conditional create | 100 | 9.371 | 10,671.27 | 0.093 | 0.088 | 0.121 | 0.141 | 0.081 | 0.166 |
677
+ | tx complex: read User+Orders -> aggregate -> create Session | 100 | 18.87 | 5,299.428 | 0.188 | 0.15 | 0.223 | 0.288 | 0.135 | 3.002 |
678
+
679
+ <!-- performance end -->
680
+
558
681
  ## Useful Links
559
682
 
560
683
  - **GitHub**: [maifeeulasad/idb-ts](https://github.com/maifeeulasad/idb-ts)
561
684
  - **NPM**: [idb-ts](https://www.npmjs.com/package/idb-ts)
562
- - **Demo**: https://maifeeulasad.github.io/idb-ts/
685
+ - **Demos**: https://maifeeulasad.github.io/idb-ts/
686
+ - **Live Editor**: https://maifeeulasad.github.io/idb-ts/typescript/
563
687
  - **Code Coverage report**: https://maifeeulasad.github.io/idb-ts/coverage/lcov-report/
564
688
 
565
689
  🎉 **Enjoy seamless IndexedDB integration with TypeScript! Happy coding!** 🚀
566
690
 
567
- Made by [Maifee Ulasad](https://github.com/maifeeulasad) with :heart: and :tea:. Licensed under [MIT](./LICENSE).
691
+ Made by [Maifee Ulasad](https://github.com/maifeeulasad) with :heart: and :tea:. Licensed under [MIT](./LICENSE).
package/lib/index.cjs CHANGED
@@ -519,12 +519,23 @@ function DataClass(options = {}) {
519
519
  };
520
520
  }
521
521
  class Database {
522
- constructor(dbName, classes) {
522
+ constructor(dbName, classes, printEnabled = false) {
523
523
  this.db = null;
524
524
  this.entityRepositories = new Map();
525
525
  this.retentionTimer = null;
526
526
  this.retentionCleanupRunning = false;
527
+ this.printDebug = (...data) => {
528
+ if (!this.printEnabled)
529
+ return;
530
+ console.debug('[idb-ts]:DEBUG:', ...data);
531
+ };
532
+ this.printError = (...error) => {
533
+ if (!this.printEnabled)
534
+ return;
535
+ console.error('[idb-ts]:ERROR:', ...error);
536
+ };
527
537
  this.dbName = dbName;
538
+ this.printEnabled = printEnabled;
528
539
  if (!classes.every((cls) => Reflect.getMetadata('dataclass', cls))) {
529
540
  throw new Error('All classes should be decorated with @DataClass.');
530
541
  }
@@ -564,7 +575,7 @@ class Database {
564
575
  const db = request.result;
565
576
  const oldVersion = event.oldVersion;
566
577
  const newVersion = event.newVersion || this.dbVersion;
567
- console.debug(`Database upgrade from version ${oldVersion} to ${newVersion}`);
578
+ this.printDebug(`Database upgrade from version ${oldVersion} to ${newVersion}`);
568
579
  this.classes.forEach((cls) => {
569
580
  var _a;
570
581
  const keyPathMetadata = Reflect.getMetadata('keypath', cls);
@@ -573,7 +584,7 @@ class Database {
573
584
  const storeName = cls.name.toLowerCase();
574
585
  if (classVersion > oldVersion) {
575
586
  if (!db.objectStoreNames.contains(storeName)) {
576
- console.debug(`Creating object store: ${storeName} (version ${classVersion})`);
587
+ this.printDebug(`Creating object store: ${storeName} (version ${classVersion})`);
577
588
  const storeOptions = {};
578
589
  if (keyPathMetadata) {
579
590
  storeOptions.keyPath = keyPathMetadata.fields;
@@ -596,7 +607,7 @@ class Database {
596
607
  });
597
608
  }
598
609
  else {
599
- console.debug(`Updating object store: ${storeName} (version ${classVersion})`);
610
+ this.printDebug(`Updating object store: ${storeName} (version ${classVersion})`);
600
611
  const transaction = request.transaction;
601
612
  if (transaction) {
602
613
  const store = transaction.objectStore(storeName);
@@ -609,7 +620,7 @@ class Database {
609
620
  ? { unique: false }
610
621
  : ((_a = indexField.options) !== null && _a !== void 0 ? _a : { unique: false });
611
622
  if (!store.indexNames.contains(indexName)) {
612
- console.debug(`Adding index: ${indexName} to ${storeName}`);
623
+ this.printDebug(`Adding index: ${indexName} to ${storeName}`);
613
624
  store.createIndex(indexName, indexName, indexOptions);
614
625
  }
615
626
  });
@@ -620,12 +631,12 @@ class Database {
620
631
  };
621
632
  request.onsuccess = () => {
622
633
  this.db = request.result;
623
- console.debug(`Database initialized (version ${this.dbVersion}) with object stores for: ${this.classes.map((cls) => `${cls.name}(v${Reflect.getMetadata('version', cls) || 1})`).join(', ')}`);
634
+ this.printDebug(`Database initialized (version ${this.dbVersion}) with object stores for: ${this.classes.map((cls) => `${cls.name}(v${Reflect.getMetadata('version', cls) || 1})`).join(', ')}`);
624
635
  this.startRetentionCleanup();
625
636
  resolve();
626
637
  };
627
638
  request.onerror = () => {
628
- console.error('Error initializing database:', request.error);
639
+ this.printError('Error initializing database:', request.error);
629
640
  reject(request.error);
630
641
  };
631
642
  });
@@ -666,7 +677,7 @@ class Database {
666
677
  if (!cleanupIntervalMs || !this.db || this.retentionTimer) {
667
678
  return;
668
679
  }
669
- console.debug(`Retention cleanup enabled for ${this.retentionPolicies.length} entities every ${cleanupIntervalMs}ms`);
680
+ this.printDebug(`Retention cleanup enabled for ${this.retentionPolicies.length} entities every ${cleanupIntervalMs}ms`);
670
681
  void this.runRetentionCleanup();
671
682
  this.retentionTimer = setInterval(() => {
672
683
  void this.runRetentionCleanup();
@@ -681,11 +692,11 @@ class Database {
681
692
  }
682
693
  this.retentionCleanupRunning = true;
683
694
  try {
684
- console.debug('Retention cleanup tick started');
695
+ this.printDebug('Retention cleanup tick started');
685
696
  for (const { storeName, className, policy } of this.retentionPolicies) {
686
697
  yield this.cleanupExpiredRecords(storeName, className, policy);
687
698
  }
688
- console.debug('Retention cleanup tick finished');
699
+ this.printDebug('Retention cleanup tick finished');
689
700
  }
690
701
  finally {
691
702
  this.retentionCleanupRunning = false;
@@ -709,11 +720,11 @@ class Database {
709
720
  }
710
721
  const value = cursor.value;
711
722
  const timestamp = value === null || value === void 0 ? void 0 : value[policy.field];
712
- console.debug(`Retention cleanup inspecting ${className}.${policy.field}:`, timestamp, 'cutoff:', cutoff);
723
+ this.printDebug(`Retention cleanup inspecting ${className}.${policy.field}:`, timestamp, 'cutoff:', cutoff);
713
724
  if (typeof timestamp === 'number' && timestamp <= cutoff) {
714
725
  const deleteRequest = cursor.delete();
715
726
  deleteRequest.onsuccess = () => {
716
- console.debug(`Retention cleanup removed expired record from ${className}`);
727
+ this.printDebug(`Retention cleanup removed expired record from ${className}`);
717
728
  cursor.continue();
718
729
  };
719
730
  deleteRequest.onerror = () => {
@@ -874,7 +885,7 @@ class Database {
874
885
  applyTimestampFields(item);
875
886
  return this.performOperation(cls.name, 'readwrite', (store) => {
876
887
  return createStoredItem(store, item).then(() => {
877
- console.debug(`Item added to ${cls.name}:`, item);
888
+ this.printDebug(`Item added to ${cls.name}:`, item);
878
889
  });
879
890
  }, transaction);
880
891
  }),
@@ -889,7 +900,7 @@ class Database {
889
900
  const request = store.get(key);
890
901
  return new Promise((resolve, reject) => {
891
902
  request.onsuccess = () => {
892
- console.debug(`Item read from ${cls.name}:`, request.result);
903
+ this.printDebug(`Item read from ${cls.name}:`, request.result);
893
904
  resolve(request.result);
894
905
  };
895
906
  request.onerror = () => reject(request.error);
@@ -900,7 +911,7 @@ class Database {
900
911
  validateItem(item);
901
912
  return this.performOperation(cls.name, 'readwrite', (store) => {
902
913
  return updateStoredItem(store, item).then(() => {
903
- console.debug(`Item updated in ${cls.name}:`, item);
914
+ this.printDebug(`Item updated in ${cls.name}:`, item);
904
915
  });
905
916
  }, transaction);
906
917
  }),
@@ -913,7 +924,7 @@ class Database {
913
924
  delete: (key) => tslib.__awaiter(this, void 0, void 0, function* () {
914
925
  return this.performOperation(cls.name, 'readwrite', (store) => {
915
926
  return deleteStoredItem(store, key).then(() => {
916
- console.debug(`Item deleted from ${cls.name}:`, key);
927
+ this.printDebug(`Item deleted from ${cls.name}:`, key);
917
928
  });
918
929
  }, transaction);
919
930
  }),
@@ -939,7 +950,7 @@ class Database {
939
950
  const request = store.getAll();
940
951
  return new Promise((resolve, reject) => {
941
952
  request.onsuccess = () => {
942
- console.debug(`All items from ${cls.name}:`, request.result);
953
+ this.printDebug(`All items from ${cls.name}:`, request.result);
943
954
  resolve(request.result);
944
955
  };
945
956
  request.onerror = () => reject(request.error);
@@ -953,7 +964,7 @@ class Database {
953
964
  request.onsuccess = () => {
954
965
  const items = request.result;
955
966
  const paginatedItems = items.slice((page - 1) * pageSize, page * pageSize);
956
- console.debug(`Paginated items from ${cls.name}:`, paginatedItems);
967
+ this.printDebug(`Paginated items from ${cls.name}:`, paginatedItems);
957
968
  resolve(paginatedItems);
958
969
  };
959
970
  request.onerror = () => reject(request.error);
@@ -969,7 +980,7 @@ class Database {
969
980
  const request = index.getAll(value);
970
981
  return new Promise((resolve, reject) => {
971
982
  request.onsuccess = () => {
972
- console.debug(`Items found by index ${indexName} with value ${value}:`, request.result);
983
+ this.printDebug(`Items found by index ${indexName} with value ${value}:`, request.result);
973
984
  resolve(request.result);
974
985
  };
975
986
  request.onerror = () => reject(request.error);
@@ -985,7 +996,7 @@ class Database {
985
996
  const request = index.get(value);
986
997
  return new Promise((resolve, reject) => {
987
998
  request.onsuccess = () => {
988
- console.debug(`Item found by index ${indexName} with value ${value}:`, request.result);
999
+ this.printDebug(`Item found by index ${indexName} with value ${value}:`, request.result);
989
1000
  resolve(request.result);
990
1001
  };
991
1002
  request.onerror = () => reject(request.error);
@@ -997,7 +1008,7 @@ class Database {
997
1008
  const request = store.count();
998
1009
  return new Promise((resolve, reject) => {
999
1010
  request.onsuccess = () => {
1000
- console.debug(`Count for ${cls.name}:`, request.result);
1011
+ this.printDebug(`Count for ${cls.name}:`, request.result);
1001
1012
  resolve(request.result);
1002
1013
  };
1003
1014
  request.onerror = () => reject(request.error);
@@ -1010,7 +1021,7 @@ class Database {
1010
1021
  return new Promise((resolve, reject) => {
1011
1022
  request.onsuccess = () => {
1012
1023
  const exists = request.result > 0;
1013
- console.debug(`Exists check for ${cls.name} with key ${key}:`, exists);
1024
+ this.printDebug(`Exists check for ${cls.name} with key ${key}:`, exists);
1014
1025
  resolve(exists);
1015
1026
  };
1016
1027
  request.onerror = () => reject(request.error);
@@ -1022,7 +1033,7 @@ class Database {
1022
1033
  const request = store.clear();
1023
1034
  return new Promise((resolve, reject) => {
1024
1035
  request.onsuccess = () => {
1025
- console.debug(`Cleared all items from ${cls.name}`);
1036
+ this.printDebug(`Cleared all items from ${cls.name}`);
1026
1037
  resolve();
1027
1038
  };
1028
1039
  request.onerror = () => reject(request.error);
package/lib/index.d.ts CHANGED
@@ -173,6 +173,9 @@ declare class Database {
173
173
  private retentionTimer;
174
174
  private retentionCleanupRunning;
175
175
  private retentionPolicies;
176
+ private printEnabled;
177
+ private printDebug;
178
+ private printError;
176
179
  private constructor();
177
180
  private calculateDatabaseVersion;
178
181
  static build<T extends Record<string, EntityRepository<any>>>(dbName: string, classes: Function[]): Promise<DatabaseWithRepositories<T>>;
package/lib/index.esm.js CHANGED
@@ -517,12 +517,23 @@ function DataClass(options = {}) {
517
517
  };
518
518
  }
519
519
  class Database {
520
- constructor(dbName, classes) {
520
+ constructor(dbName, classes, printEnabled = false) {
521
521
  this.db = null;
522
522
  this.entityRepositories = new Map();
523
523
  this.retentionTimer = null;
524
524
  this.retentionCleanupRunning = false;
525
+ this.printDebug = (...data) => {
526
+ if (!this.printEnabled)
527
+ return;
528
+ console.debug('[idb-ts]:DEBUG:', ...data);
529
+ };
530
+ this.printError = (...error) => {
531
+ if (!this.printEnabled)
532
+ return;
533
+ console.error('[idb-ts]:ERROR:', ...error);
534
+ };
525
535
  this.dbName = dbName;
536
+ this.printEnabled = printEnabled;
526
537
  if (!classes.every((cls) => Reflect.getMetadata('dataclass', cls))) {
527
538
  throw new Error('All classes should be decorated with @DataClass.');
528
539
  }
@@ -562,7 +573,7 @@ class Database {
562
573
  const db = request.result;
563
574
  const oldVersion = event.oldVersion;
564
575
  const newVersion = event.newVersion || this.dbVersion;
565
- console.debug(`Database upgrade from version ${oldVersion} to ${newVersion}`);
576
+ this.printDebug(`Database upgrade from version ${oldVersion} to ${newVersion}`);
566
577
  this.classes.forEach((cls) => {
567
578
  var _a;
568
579
  const keyPathMetadata = Reflect.getMetadata('keypath', cls);
@@ -571,7 +582,7 @@ class Database {
571
582
  const storeName = cls.name.toLowerCase();
572
583
  if (classVersion > oldVersion) {
573
584
  if (!db.objectStoreNames.contains(storeName)) {
574
- console.debug(`Creating object store: ${storeName} (version ${classVersion})`);
585
+ this.printDebug(`Creating object store: ${storeName} (version ${classVersion})`);
575
586
  const storeOptions = {};
576
587
  if (keyPathMetadata) {
577
588
  storeOptions.keyPath = keyPathMetadata.fields;
@@ -594,7 +605,7 @@ class Database {
594
605
  });
595
606
  }
596
607
  else {
597
- console.debug(`Updating object store: ${storeName} (version ${classVersion})`);
608
+ this.printDebug(`Updating object store: ${storeName} (version ${classVersion})`);
598
609
  const transaction = request.transaction;
599
610
  if (transaction) {
600
611
  const store = transaction.objectStore(storeName);
@@ -607,7 +618,7 @@ class Database {
607
618
  ? { unique: false }
608
619
  : ((_a = indexField.options) !== null && _a !== void 0 ? _a : { unique: false });
609
620
  if (!store.indexNames.contains(indexName)) {
610
- console.debug(`Adding index: ${indexName} to ${storeName}`);
621
+ this.printDebug(`Adding index: ${indexName} to ${storeName}`);
611
622
  store.createIndex(indexName, indexName, indexOptions);
612
623
  }
613
624
  });
@@ -618,12 +629,12 @@ class Database {
618
629
  };
619
630
  request.onsuccess = () => {
620
631
  this.db = request.result;
621
- console.debug(`Database initialized (version ${this.dbVersion}) with object stores for: ${this.classes.map((cls) => `${cls.name}(v${Reflect.getMetadata('version', cls) || 1})`).join(', ')}`);
632
+ this.printDebug(`Database initialized (version ${this.dbVersion}) with object stores for: ${this.classes.map((cls) => `${cls.name}(v${Reflect.getMetadata('version', cls) || 1})`).join(', ')}`);
622
633
  this.startRetentionCleanup();
623
634
  resolve();
624
635
  };
625
636
  request.onerror = () => {
626
- console.error('Error initializing database:', request.error);
637
+ this.printError('Error initializing database:', request.error);
627
638
  reject(request.error);
628
639
  };
629
640
  });
@@ -664,7 +675,7 @@ class Database {
664
675
  if (!cleanupIntervalMs || !this.db || this.retentionTimer) {
665
676
  return;
666
677
  }
667
- console.debug(`Retention cleanup enabled for ${this.retentionPolicies.length} entities every ${cleanupIntervalMs}ms`);
678
+ this.printDebug(`Retention cleanup enabled for ${this.retentionPolicies.length} entities every ${cleanupIntervalMs}ms`);
668
679
  void this.runRetentionCleanup();
669
680
  this.retentionTimer = setInterval(() => {
670
681
  void this.runRetentionCleanup();
@@ -679,11 +690,11 @@ class Database {
679
690
  }
680
691
  this.retentionCleanupRunning = true;
681
692
  try {
682
- console.debug('Retention cleanup tick started');
693
+ this.printDebug('Retention cleanup tick started');
683
694
  for (const { storeName, className, policy } of this.retentionPolicies) {
684
695
  yield this.cleanupExpiredRecords(storeName, className, policy);
685
696
  }
686
- console.debug('Retention cleanup tick finished');
697
+ this.printDebug('Retention cleanup tick finished');
687
698
  }
688
699
  finally {
689
700
  this.retentionCleanupRunning = false;
@@ -707,11 +718,11 @@ class Database {
707
718
  }
708
719
  const value = cursor.value;
709
720
  const timestamp = value === null || value === void 0 ? void 0 : value[policy.field];
710
- console.debug(`Retention cleanup inspecting ${className}.${policy.field}:`, timestamp, 'cutoff:', cutoff);
721
+ this.printDebug(`Retention cleanup inspecting ${className}.${policy.field}:`, timestamp, 'cutoff:', cutoff);
711
722
  if (typeof timestamp === 'number' && timestamp <= cutoff) {
712
723
  const deleteRequest = cursor.delete();
713
724
  deleteRequest.onsuccess = () => {
714
- console.debug(`Retention cleanup removed expired record from ${className}`);
725
+ this.printDebug(`Retention cleanup removed expired record from ${className}`);
715
726
  cursor.continue();
716
727
  };
717
728
  deleteRequest.onerror = () => {
@@ -872,7 +883,7 @@ class Database {
872
883
  applyTimestampFields(item);
873
884
  return this.performOperation(cls.name, 'readwrite', (store) => {
874
885
  return createStoredItem(store, item).then(() => {
875
- console.debug(`Item added to ${cls.name}:`, item);
886
+ this.printDebug(`Item added to ${cls.name}:`, item);
876
887
  });
877
888
  }, transaction);
878
889
  }),
@@ -887,7 +898,7 @@ class Database {
887
898
  const request = store.get(key);
888
899
  return new Promise((resolve, reject) => {
889
900
  request.onsuccess = () => {
890
- console.debug(`Item read from ${cls.name}:`, request.result);
901
+ this.printDebug(`Item read from ${cls.name}:`, request.result);
891
902
  resolve(request.result);
892
903
  };
893
904
  request.onerror = () => reject(request.error);
@@ -898,7 +909,7 @@ class Database {
898
909
  validateItem(item);
899
910
  return this.performOperation(cls.name, 'readwrite', (store) => {
900
911
  return updateStoredItem(store, item).then(() => {
901
- console.debug(`Item updated in ${cls.name}:`, item);
912
+ this.printDebug(`Item updated in ${cls.name}:`, item);
902
913
  });
903
914
  }, transaction);
904
915
  }),
@@ -911,7 +922,7 @@ class Database {
911
922
  delete: (key) => __awaiter(this, void 0, void 0, function* () {
912
923
  return this.performOperation(cls.name, 'readwrite', (store) => {
913
924
  return deleteStoredItem(store, key).then(() => {
914
- console.debug(`Item deleted from ${cls.name}:`, key);
925
+ this.printDebug(`Item deleted from ${cls.name}:`, key);
915
926
  });
916
927
  }, transaction);
917
928
  }),
@@ -937,7 +948,7 @@ class Database {
937
948
  const request = store.getAll();
938
949
  return new Promise((resolve, reject) => {
939
950
  request.onsuccess = () => {
940
- console.debug(`All items from ${cls.name}:`, request.result);
951
+ this.printDebug(`All items from ${cls.name}:`, request.result);
941
952
  resolve(request.result);
942
953
  };
943
954
  request.onerror = () => reject(request.error);
@@ -951,7 +962,7 @@ class Database {
951
962
  request.onsuccess = () => {
952
963
  const items = request.result;
953
964
  const paginatedItems = items.slice((page - 1) * pageSize, page * pageSize);
954
- console.debug(`Paginated items from ${cls.name}:`, paginatedItems);
965
+ this.printDebug(`Paginated items from ${cls.name}:`, paginatedItems);
955
966
  resolve(paginatedItems);
956
967
  };
957
968
  request.onerror = () => reject(request.error);
@@ -967,7 +978,7 @@ class Database {
967
978
  const request = index.getAll(value);
968
979
  return new Promise((resolve, reject) => {
969
980
  request.onsuccess = () => {
970
- console.debug(`Items found by index ${indexName} with value ${value}:`, request.result);
981
+ this.printDebug(`Items found by index ${indexName} with value ${value}:`, request.result);
971
982
  resolve(request.result);
972
983
  };
973
984
  request.onerror = () => reject(request.error);
@@ -983,7 +994,7 @@ class Database {
983
994
  const request = index.get(value);
984
995
  return new Promise((resolve, reject) => {
985
996
  request.onsuccess = () => {
986
- console.debug(`Item found by index ${indexName} with value ${value}:`, request.result);
997
+ this.printDebug(`Item found by index ${indexName} with value ${value}:`, request.result);
987
998
  resolve(request.result);
988
999
  };
989
1000
  request.onerror = () => reject(request.error);
@@ -995,7 +1006,7 @@ class Database {
995
1006
  const request = store.count();
996
1007
  return new Promise((resolve, reject) => {
997
1008
  request.onsuccess = () => {
998
- console.debug(`Count for ${cls.name}:`, request.result);
1009
+ this.printDebug(`Count for ${cls.name}:`, request.result);
999
1010
  resolve(request.result);
1000
1011
  };
1001
1012
  request.onerror = () => reject(request.error);
@@ -1008,7 +1019,7 @@ class Database {
1008
1019
  return new Promise((resolve, reject) => {
1009
1020
  request.onsuccess = () => {
1010
1021
  const exists = request.result > 0;
1011
- console.debug(`Exists check for ${cls.name} with key ${key}:`, exists);
1022
+ this.printDebug(`Exists check for ${cls.name} with key ${key}:`, exists);
1012
1023
  resolve(exists);
1013
1024
  };
1014
1025
  request.onerror = () => reject(request.error);
@@ -1020,7 +1031,7 @@ class Database {
1020
1031
  const request = store.clear();
1021
1032
  return new Promise((resolve, reject) => {
1022
1033
  request.onsuccess = () => {
1023
- console.debug(`Cleared all items from ${cls.name}`);
1034
+ this.printDebug(`Cleared all items from ${cls.name}`);
1024
1035
  resolve();
1025
1036
  };
1026
1037
  request.onerror = () => reject(request.error);
package/lib/index.js CHANGED
@@ -517,12 +517,23 @@ function DataClass(options = {}) {
517
517
  };
518
518
  }
519
519
  class Database {
520
- constructor(dbName, classes) {
520
+ constructor(dbName, classes, printEnabled = false) {
521
521
  this.db = null;
522
522
  this.entityRepositories = new Map();
523
523
  this.retentionTimer = null;
524
524
  this.retentionCleanupRunning = false;
525
+ this.printDebug = (...data) => {
526
+ if (!this.printEnabled)
527
+ return;
528
+ console.debug('[idb-ts]:DEBUG:', ...data);
529
+ };
530
+ this.printError = (...error) => {
531
+ if (!this.printEnabled)
532
+ return;
533
+ console.error('[idb-ts]:ERROR:', ...error);
534
+ };
525
535
  this.dbName = dbName;
536
+ this.printEnabled = printEnabled;
526
537
  if (!classes.every((cls) => Reflect.getMetadata('dataclass', cls))) {
527
538
  throw new Error('All classes should be decorated with @DataClass.');
528
539
  }
@@ -562,7 +573,7 @@ class Database {
562
573
  const db = request.result;
563
574
  const oldVersion = event.oldVersion;
564
575
  const newVersion = event.newVersion || this.dbVersion;
565
- console.debug(`Database upgrade from version ${oldVersion} to ${newVersion}`);
576
+ this.printDebug(`Database upgrade from version ${oldVersion} to ${newVersion}`);
566
577
  this.classes.forEach((cls) => {
567
578
  var _a;
568
579
  const keyPathMetadata = Reflect.getMetadata('keypath', cls);
@@ -571,7 +582,7 @@ class Database {
571
582
  const storeName = cls.name.toLowerCase();
572
583
  if (classVersion > oldVersion) {
573
584
  if (!db.objectStoreNames.contains(storeName)) {
574
- console.debug(`Creating object store: ${storeName} (version ${classVersion})`);
585
+ this.printDebug(`Creating object store: ${storeName} (version ${classVersion})`);
575
586
  const storeOptions = {};
576
587
  if (keyPathMetadata) {
577
588
  storeOptions.keyPath = keyPathMetadata.fields;
@@ -594,7 +605,7 @@ class Database {
594
605
  });
595
606
  }
596
607
  else {
597
- console.debug(`Updating object store: ${storeName} (version ${classVersion})`);
608
+ this.printDebug(`Updating object store: ${storeName} (version ${classVersion})`);
598
609
  const transaction = request.transaction;
599
610
  if (transaction) {
600
611
  const store = transaction.objectStore(storeName);
@@ -607,7 +618,7 @@ class Database {
607
618
  ? { unique: false }
608
619
  : ((_a = indexField.options) !== null && _a !== void 0 ? _a : { unique: false });
609
620
  if (!store.indexNames.contains(indexName)) {
610
- console.debug(`Adding index: ${indexName} to ${storeName}`);
621
+ this.printDebug(`Adding index: ${indexName} to ${storeName}`);
611
622
  store.createIndex(indexName, indexName, indexOptions);
612
623
  }
613
624
  });
@@ -618,12 +629,12 @@ class Database {
618
629
  };
619
630
  request.onsuccess = () => {
620
631
  this.db = request.result;
621
- console.debug(`Database initialized (version ${this.dbVersion}) with object stores for: ${this.classes.map((cls) => `${cls.name}(v${Reflect.getMetadata('version', cls) || 1})`).join(', ')}`);
632
+ this.printDebug(`Database initialized (version ${this.dbVersion}) with object stores for: ${this.classes.map((cls) => `${cls.name}(v${Reflect.getMetadata('version', cls) || 1})`).join(', ')}`);
622
633
  this.startRetentionCleanup();
623
634
  resolve();
624
635
  };
625
636
  request.onerror = () => {
626
- console.error('Error initializing database:', request.error);
637
+ this.printError('Error initializing database:', request.error);
627
638
  reject(request.error);
628
639
  };
629
640
  });
@@ -664,7 +675,7 @@ class Database {
664
675
  if (!cleanupIntervalMs || !this.db || this.retentionTimer) {
665
676
  return;
666
677
  }
667
- console.debug(`Retention cleanup enabled for ${this.retentionPolicies.length} entities every ${cleanupIntervalMs}ms`);
678
+ this.printDebug(`Retention cleanup enabled for ${this.retentionPolicies.length} entities every ${cleanupIntervalMs}ms`);
668
679
  void this.runRetentionCleanup();
669
680
  this.retentionTimer = setInterval(() => {
670
681
  void this.runRetentionCleanup();
@@ -679,11 +690,11 @@ class Database {
679
690
  }
680
691
  this.retentionCleanupRunning = true;
681
692
  try {
682
- console.debug('Retention cleanup tick started');
693
+ this.printDebug('Retention cleanup tick started');
683
694
  for (const { storeName, className, policy } of this.retentionPolicies) {
684
695
  yield this.cleanupExpiredRecords(storeName, className, policy);
685
696
  }
686
- console.debug('Retention cleanup tick finished');
697
+ this.printDebug('Retention cleanup tick finished');
687
698
  }
688
699
  finally {
689
700
  this.retentionCleanupRunning = false;
@@ -707,11 +718,11 @@ class Database {
707
718
  }
708
719
  const value = cursor.value;
709
720
  const timestamp = value === null || value === void 0 ? void 0 : value[policy.field];
710
- console.debug(`Retention cleanup inspecting ${className}.${policy.field}:`, timestamp, 'cutoff:', cutoff);
721
+ this.printDebug(`Retention cleanup inspecting ${className}.${policy.field}:`, timestamp, 'cutoff:', cutoff);
711
722
  if (typeof timestamp === 'number' && timestamp <= cutoff) {
712
723
  const deleteRequest = cursor.delete();
713
724
  deleteRequest.onsuccess = () => {
714
- console.debug(`Retention cleanup removed expired record from ${className}`);
725
+ this.printDebug(`Retention cleanup removed expired record from ${className}`);
715
726
  cursor.continue();
716
727
  };
717
728
  deleteRequest.onerror = () => {
@@ -872,7 +883,7 @@ class Database {
872
883
  applyTimestampFields(item);
873
884
  return this.performOperation(cls.name, 'readwrite', (store) => {
874
885
  return createStoredItem(store, item).then(() => {
875
- console.debug(`Item added to ${cls.name}:`, item);
886
+ this.printDebug(`Item added to ${cls.name}:`, item);
876
887
  });
877
888
  }, transaction);
878
889
  }),
@@ -887,7 +898,7 @@ class Database {
887
898
  const request = store.get(key);
888
899
  return new Promise((resolve, reject) => {
889
900
  request.onsuccess = () => {
890
- console.debug(`Item read from ${cls.name}:`, request.result);
901
+ this.printDebug(`Item read from ${cls.name}:`, request.result);
891
902
  resolve(request.result);
892
903
  };
893
904
  request.onerror = () => reject(request.error);
@@ -898,7 +909,7 @@ class Database {
898
909
  validateItem(item);
899
910
  return this.performOperation(cls.name, 'readwrite', (store) => {
900
911
  return updateStoredItem(store, item).then(() => {
901
- console.debug(`Item updated in ${cls.name}:`, item);
912
+ this.printDebug(`Item updated in ${cls.name}:`, item);
902
913
  });
903
914
  }, transaction);
904
915
  }),
@@ -911,7 +922,7 @@ class Database {
911
922
  delete: (key) => __awaiter(this, void 0, void 0, function* () {
912
923
  return this.performOperation(cls.name, 'readwrite', (store) => {
913
924
  return deleteStoredItem(store, key).then(() => {
914
- console.debug(`Item deleted from ${cls.name}:`, key);
925
+ this.printDebug(`Item deleted from ${cls.name}:`, key);
915
926
  });
916
927
  }, transaction);
917
928
  }),
@@ -937,7 +948,7 @@ class Database {
937
948
  const request = store.getAll();
938
949
  return new Promise((resolve, reject) => {
939
950
  request.onsuccess = () => {
940
- console.debug(`All items from ${cls.name}:`, request.result);
951
+ this.printDebug(`All items from ${cls.name}:`, request.result);
941
952
  resolve(request.result);
942
953
  };
943
954
  request.onerror = () => reject(request.error);
@@ -951,7 +962,7 @@ class Database {
951
962
  request.onsuccess = () => {
952
963
  const items = request.result;
953
964
  const paginatedItems = items.slice((page - 1) * pageSize, page * pageSize);
954
- console.debug(`Paginated items from ${cls.name}:`, paginatedItems);
965
+ this.printDebug(`Paginated items from ${cls.name}:`, paginatedItems);
955
966
  resolve(paginatedItems);
956
967
  };
957
968
  request.onerror = () => reject(request.error);
@@ -967,7 +978,7 @@ class Database {
967
978
  const request = index.getAll(value);
968
979
  return new Promise((resolve, reject) => {
969
980
  request.onsuccess = () => {
970
- console.debug(`Items found by index ${indexName} with value ${value}:`, request.result);
981
+ this.printDebug(`Items found by index ${indexName} with value ${value}:`, request.result);
971
982
  resolve(request.result);
972
983
  };
973
984
  request.onerror = () => reject(request.error);
@@ -983,7 +994,7 @@ class Database {
983
994
  const request = index.get(value);
984
995
  return new Promise((resolve, reject) => {
985
996
  request.onsuccess = () => {
986
- console.debug(`Item found by index ${indexName} with value ${value}:`, request.result);
997
+ this.printDebug(`Item found by index ${indexName} with value ${value}:`, request.result);
987
998
  resolve(request.result);
988
999
  };
989
1000
  request.onerror = () => reject(request.error);
@@ -995,7 +1006,7 @@ class Database {
995
1006
  const request = store.count();
996
1007
  return new Promise((resolve, reject) => {
997
1008
  request.onsuccess = () => {
998
- console.debug(`Count for ${cls.name}:`, request.result);
1009
+ this.printDebug(`Count for ${cls.name}:`, request.result);
999
1010
  resolve(request.result);
1000
1011
  };
1001
1012
  request.onerror = () => reject(request.error);
@@ -1008,7 +1019,7 @@ class Database {
1008
1019
  return new Promise((resolve, reject) => {
1009
1020
  request.onsuccess = () => {
1010
1021
  const exists = request.result > 0;
1011
- console.debug(`Exists check for ${cls.name} with key ${key}:`, exists);
1022
+ this.printDebug(`Exists check for ${cls.name} with key ${key}:`, exists);
1012
1023
  resolve(exists);
1013
1024
  };
1014
1025
  request.onerror = () => reject(request.error);
@@ -1020,7 +1031,7 @@ class Database {
1020
1031
  const request = store.clear();
1021
1032
  return new Promise((resolve, reject) => {
1022
1033
  request.onsuccess = () => {
1023
- console.debug(`Cleared all items from ${cls.name}`);
1034
+ this.printDebug(`Cleared all items from ${cls.name}`);
1024
1035
  resolve();
1025
1036
  };
1026
1037
  request.onerror = () => reject(request.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idb-ts",
3
- "version": "3.14.0",
3
+ "version": "3.15.0",
4
4
  "description": "Easy CRUD for indexed-db, written in TypeScript",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/index.esm.js",
@@ -25,7 +25,9 @@
25
25
  "test:types": "tsc -p tsconfig.test.json --noEmit",
26
26
  "test:watch": "jest --watchAll --runInBand --detectOpenHandles",
27
27
  "test:coverage": "jest --coverage --runInBand --detectOpenHandles",
28
- "test:coverage:watch": "jest --coverage --watchAll --runInBand --detectOpenHandles"
28
+ "test:coverage:watch": "jest --coverage --watchAll --runInBand --detectOpenHandles",
29
+ "test:performance": "npx tsx performance.ts",
30
+ "docs:generate": "typedoc"
29
31
  },
30
32
  "repository": {
31
33
  "type": "git",
@@ -70,6 +72,7 @@
70
72
  "prettier": "^3.8.3",
71
73
  "rollup": "^4.60.1",
72
74
  "ts-jest": "^29.4.9",
75
+ "typedoc": "^0.28.19",
73
76
  "typescript": "^6.0.3"
74
77
  }
75
- }
78
+ }