idb-ts 3.13.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 :love: and :tea:. Licensed under [MIT](./LICENSE).
691
+ Made by [Maifee Ulasad](https://github.com/maifeeulasad) with :heart: and :tea:. Licensed under [MIT](./LICENSE).