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 +198 -74
- package/lib/index.cjs +34 -23
- package/lib/index.d.ts +3 -0
- package/lib/index.esm.js +34 -23
- package/lib/index.js +34 -23
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -55,18 +55,18 @@ yarn add idb-ts
|
|
|
55
55
|
|
|
56
56
|
## Feature Overview
|
|
57
57
|
|
|
58
|
-
| Feature
|
|
59
|
-
|
|
60
|
-
| **Declarative entity definition** | Define stores, keys, and indexes with class decorators
|
|
61
|
-
| **Full CRUD API**
|
|
62
|
-
| **Typed query builder**
|
|
63
|
-
| **Key generation**
|
|
64
|
-
| **Composite keys**
|
|
65
|
-
| **Field validation**
|
|
66
|
-
| **Schema versioning**
|
|
67
|
-
| **Transaction API**
|
|
68
|
-
| **Data retention**
|
|
69
|
-
| **Automatic timestamps**
|
|
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', [
|
|
91
|
+
const db = await Database.build<{ User: EntityRepository<User> }>('mydb', [
|
|
92
|
+
User,
|
|
93
|
+
]);
|
|
92
94
|
|
|
93
|
-
await db.User.create({
|
|
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(
|
|
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
|
|
129
|
-
|
|
130
|
-
| `version` | `number` | `1`
|
|
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
|
|
137
|
-
|
|
138
|
-
| `autoIncrement` | `boolean`
|
|
139
|
-
| `generator`
|
|
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
|
|
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:
|
|
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();
|
|
190
|
-
db.getEntityVersions();
|
|
191
|
-
db.getEntityVersion('User');
|
|
192
|
-
db.getAvailableEntities();
|
|
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');
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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')
|
|
270
|
-
.
|
|
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
|
|
277
|
-
|
|
278
|
-
| `equals`
|
|
279
|
-
| `gt` / `gte` / `lt` / `lte`
|
|
280
|
-
| `between(start, end)`
|
|
281
|
-
| `notBetween(start, end)`
|
|
282
|
-
| `startsWith` / `endsWith`
|
|
283
|
-
| `contains`
|
|
284
|
-
| `matches`
|
|
285
|
-
| `in(values)` / `notIn(values)` | any
|
|
286
|
-
| `containsAny(values)`
|
|
287
|
-
| `containsAll(values)`
|
|
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')
|
|
308
|
+
.where('age')
|
|
309
|
+
.gte(18)
|
|
297
310
|
.or()
|
|
298
|
-
.where('hasParentalConsent')
|
|
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')
|
|
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')
|
|
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;
|
|
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' })
|
|
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' })
|
|
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();
|
|
422
|
+
KeyGenerators.uuid(); // "a1b2c3d4-..."
|
|
410
423
|
KeyGenerators.timestamp(); // 1696118400000
|
|
411
|
-
KeyGenerators.random();
|
|
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(
|
|
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
|
|
518
|
-
|
|
519
|
-
| `seconds` | `number`
|
|
520
|
-
| `enabled` | `boolean` | `true`
|
|
521
|
-
| `field`
|
|
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 })
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|