localdb-ces6q 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LocalDB.d.ts +103 -102
- package/LocalDB.js +980 -973
- package/LocalDBDocument.d.ts +18 -18
- package/LocalDBDocument.js +55 -55
- package/LocalDBState.d.ts +14 -14
- package/LocalDBState.js +46 -46
- package/comparators.d.ts +15 -15
- package/comparators.js +105 -105
- package/hooks.d.ts +5 -5
- package/hooks.js +34 -34
- package/package.json +5 -5
- package/types.d.ts +104 -104
- package/types.js +1 -1
package/LocalDB.js
CHANGED
@@ -1,973 +1,980 @@
|
|
1
|
-
import { BTree } from 'bplustree-mq4uj/btree.js';
|
2
|
-
import { Debounce } from 'util-3gcvv/class/Debounce.js';
|
3
|
-
import { deepEqual } from 'util-3gcvv/deepEqual.js';
|
4
|
-
import { objectEmpty, objectKeys } from 'util-3gcvv/object.js';
|
5
|
-
import { randomString } from 'util-3gcvv/string.js';
|
6
|
-
import { defaultComparators } from './comparators.js';
|
7
|
-
import { LocalDBState } from './LocalDBState.js';
|
8
|
-
// TODO
|
9
|
-
// composite index
|
10
|
-
// index filter / sort
|
11
|
-
// watch filtered/sorted collection
|
12
|
-
export class LocalDB {
|
13
|
-
constructor(config, initialData, options) {
|
14
|
-
this._txChangedFields = {};
|
15
|
-
this._txChanges = {};
|
16
|
-
this._txKey = null;
|
17
|
-
this._txOps = [];
|
18
|
-
this._txRollbacks = [];
|
19
|
-
this._txSnapshot = null;
|
20
|
-
this._txOptions = null;
|
21
|
-
this._noEvent = false;
|
22
|
-
this._undoable = false;
|
23
|
-
this._inUndoOrRedo = false;
|
24
|
-
this._opQueue = [];
|
25
|
-
this._commiting = false;
|
26
|
-
this._listenerId = 0;
|
27
|
-
this._dbListeners = [];
|
28
|
-
this._colListeners = [];
|
29
|
-
this._docListeners = [];
|
30
|
-
this._fieldListeners = [];
|
31
|
-
this.state = new LocalDBState({ history: [], historyIndex: 0 }, {});
|
32
|
-
this.saveDebounced = new Debounce(this.saveToStorage, 300, this);
|
33
|
-
this._options = options;
|
34
|
-
this._collectionNames = [];
|
35
|
-
this._collections = {};
|
36
|
-
this._config = {};
|
37
|
-
this._fields = {};
|
38
|
-
this._saveCols = {};
|
39
|
-
this._indexes = {};
|
40
|
-
const colNames = objectKeys(config);
|
41
|
-
for (let i = 0, il = colNames.length; i < il; i += 1) {
|
42
|
-
const col = colNames[i];
|
43
|
-
this.defineCollection(col, config[col], initialData?.[col]);
|
44
|
-
}
|
45
|
-
}
|
46
|
-
destroy() {
|
47
|
-
this.saveDebounced.destroy();
|
48
|
-
this._options = undefined;
|
49
|
-
this._collectionNames = [];
|
50
|
-
this._collections = {};
|
51
|
-
this._config = {};
|
52
|
-
this._fields = {};
|
53
|
-
for (let col in this._saveCols) {
|
54
|
-
this._saveCols[col]?.destroy();
|
55
|
-
}
|
56
|
-
this._saveCols = {};
|
57
|
-
for (let col in this._indexes) {
|
58
|
-
const indexes = this._indexes[col];
|
59
|
-
for (let name in indexes) {
|
60
|
-
indexes[name].clear();
|
61
|
-
}
|
62
|
-
}
|
63
|
-
this._indexes = {};
|
64
|
-
this._txChangedFields = {};
|
65
|
-
this._txChanges = {};
|
66
|
-
this._txKey = null;
|
67
|
-
this._txOps = [];
|
68
|
-
this._txRollbacks = [];
|
69
|
-
this._txSnapshot = null;
|
70
|
-
this._txOptions = null;
|
71
|
-
this.state.$destroy();
|
72
|
-
this._dbListeners = [];
|
73
|
-
this._colListeners = [];
|
74
|
-
this._docListeners = [];
|
75
|
-
this._fieldListeners = [];
|
76
|
-
this._opQueue = [];
|
77
|
-
this._listenerId = 0;
|
78
|
-
}
|
79
|
-
toJSON() {
|
80
|
-
return this._collections;
|
81
|
-
}
|
82
|
-
equals(other) {
|
83
|
-
return deepEqual(this._collections, other);
|
84
|
-
}
|
85
|
-
//
|
86
|
-
// Config
|
87
|
-
//
|
88
|
-
defineCollection(colName, config, initialData) {
|
89
|
-
const col = colName;
|
90
|
-
if (col in this._config) {
|
91
|
-
throw new Error(`Collection, "${col}", is already defined`);
|
92
|
-
}
|
93
|
-
// config
|
94
|
-
this._config[col] = config;
|
95
|
-
// collection name
|
96
|
-
this._collectionNames.push(col);
|
97
|
-
// field names
|
98
|
-
const fieldNames = objectKeys(config.fields);
|
99
|
-
this._fields[col] = fieldNames;
|
100
|
-
// save data to storage
|
101
|
-
this._saveCols[col] = new Debounce(() => this.saveCollectionToStorage(col), config.localStorageSetWait ?? 300);
|
102
|
-
// initial data
|
103
|
-
const { localStorageKey } = config;
|
104
|
-
let colData = {};
|
105
|
-
if (initialData) {
|
106
|
-
colData = initialData;
|
107
|
-
}
|
108
|
-
else if (localStorageKey) {
|
109
|
-
const snapshot = localStorage.getItem(localStorageKey);
|
110
|
-
if (snapshot) {
|
111
|
-
colData = JSON.parse(snapshot);
|
112
|
-
}
|
113
|
-
}
|
114
|
-
this._collections = { ...this._collections, [col]: colData };
|
115
|
-
// indexes
|
116
|
-
const docs = [];
|
117
|
-
if (colData) {
|
118
|
-
for (let id in colData) {
|
119
|
-
const doc = colData[id];
|
120
|
-
docs.push(doc);
|
121
|
-
}
|
122
|
-
}
|
123
|
-
const fieldIndexes = {};
|
124
|
-
for (let fi = 0, fl = fieldNames.length; fi < fl; fi += 1) {
|
125
|
-
const field = fieldNames[fi];
|
126
|
-
const { type, compare, index } = config.fields[field];
|
127
|
-
if (index) {
|
128
|
-
let comparator;
|
129
|
-
if (compare) {
|
130
|
-
comparator = index === 'desc' ? (a, b) => compare(b, a) : compare;
|
131
|
-
}
|
132
|
-
else {
|
133
|
-
comparator = defaultComparators[type]?.[index];
|
134
|
-
}
|
135
|
-
if (!comparator) {
|
136
|
-
throw new Error(`Comparator must be set to index ${col}/${field}`);
|
137
|
-
}
|
138
|
-
fieldIndexes[field] = new BTree(docs.map((d) => [d, d]), (a, b) => comparator(a[field], b[field]));
|
139
|
-
}
|
140
|
-
}
|
141
|
-
const { indexes } = config;
|
142
|
-
if (indexes) {
|
143
|
-
for (const name in indexes) {
|
144
|
-
const { compare } = indexes[name];
|
145
|
-
fieldIndexes[name] = new BTree(docs.map((d) => [d, d]), compare);
|
146
|
-
}
|
147
|
-
}
|
148
|
-
this._indexes[col] = fieldIndexes;
|
149
|
-
}
|
150
|
-
deleteCollection(col) {
|
151
|
-
if (!(col in this._config)) {
|
152
|
-
throw new Error(`Collection, "${col}", does not exist`);
|
153
|
-
}
|
154
|
-
delete this._config[col];
|
155
|
-
this._collectionNames = this._collectionNames.filter((x) => x !== col);
|
156
|
-
delete this._fields[col];
|
157
|
-
delete this._saveCols[col];
|
158
|
-
const next = { ...this._collections };
|
159
|
-
delete next[col];
|
160
|
-
this._collections = next;
|
161
|
-
}
|
162
|
-
existsCollection(col) {
|
163
|
-
return col in this._config;
|
164
|
-
}
|
165
|
-
get collectionNames() {
|
166
|
-
return this._collectionNames.slice();
|
167
|
-
}
|
168
|
-
//
|
169
|
-
// Transaction
|
170
|
-
//
|
171
|
-
beginTx(options = this._options) {
|
172
|
-
if (this._txKey == null) {
|
173
|
-
const key = randomString();
|
174
|
-
this._txChangedFields = {};
|
175
|
-
this._txChanges = {};
|
176
|
-
this._txKey = key;
|
177
|
-
this._txOps = [];
|
178
|
-
this._txRollbacks = [];
|
179
|
-
this._txSnapshot = this._collections;
|
180
|
-
this._txOptions = options || null;
|
181
|
-
this._noEvent = !!options?.noEvent;
|
182
|
-
this._undoable = !!options?.undoable;
|
183
|
-
return key;
|
184
|
-
}
|
185
|
-
return null;
|
186
|
-
}
|
187
|
-
endTx(txKey) {
|
188
|
-
if (txKey != null && this._txKey === txKey) {
|
189
|
-
try {
|
190
|
-
this._commitChanges();
|
191
|
-
}
|
192
|
-
catch (e) {
|
193
|
-
this._rollback();
|
194
|
-
throw e;
|
195
|
-
}
|
196
|
-
this._pushHistory();
|
197
|
-
// reset
|
198
|
-
this._txChangedFields = {};
|
199
|
-
this._txChanges = {};
|
200
|
-
this._txKey = null;
|
201
|
-
this._txOps = [];
|
202
|
-
this._txRollbacks = [];
|
203
|
-
this._txSnapshot = null;
|
204
|
-
this._txOptions = null;
|
205
|
-
this._noEvent = false;
|
206
|
-
this._undoable = false;
|
207
|
-
this._flushQueue();
|
208
|
-
}
|
209
|
-
}
|
210
|
-
_rollback() {
|
211
|
-
if (this._txSnapshot) {
|
212
|
-
this._collections = this._txSnapshot;
|
213
|
-
for (let i = 0, il = this._txRollbacks.length; i < il; i += 1) {
|
214
|
-
this._txRollbacks[i]();
|
215
|
-
}
|
216
|
-
// reset
|
217
|
-
this._txChangedFields = {};
|
218
|
-
this._txChanges = {};
|
219
|
-
this._txKey = null;
|
220
|
-
this._txOps = [];
|
221
|
-
this._txRollbacks = [];
|
222
|
-
this._txSnapshot = null;
|
223
|
-
this._noEvent = false;
|
224
|
-
this._undoable = false;
|
225
|
-
}
|
226
|
-
}
|
227
|
-
tx(fn, options) {
|
228
|
-
const txKey = this.beginTx(options);
|
229
|
-
if (txKey == null) {
|
230
|
-
throw new Error('Already in transaction');
|
231
|
-
}
|
232
|
-
try {
|
233
|
-
fn();
|
234
|
-
}
|
235
|
-
catch (e) {
|
236
|
-
this._rollback();
|
237
|
-
throw e;
|
238
|
-
}
|
239
|
-
this.endTx(txKey);
|
240
|
-
}
|
241
|
-
undoableTx(fn, options) {
|
242
|
-
return this.tx(fn, { ...options, undoable: true });
|
243
|
-
}
|
244
|
-
//
|
245
|
-
// Listeners
|
246
|
-
//
|
247
|
-
subToDB(handler) {
|
248
|
-
const lid = this._listenerId++;
|
249
|
-
this._dbListeners.push({ lid, handler });
|
250
|
-
return () => this.unsubFromDB(lid);
|
251
|
-
}
|
252
|
-
unsubFromDB(lid) {
|
253
|
-
this._dbListeners = this._dbListeners.filter((x) => x.lid !== lid);
|
254
|
-
}
|
255
|
-
subToCol(collection, handler) {
|
256
|
-
const lid = this._listenerId++;
|
257
|
-
this._colListeners.push({ lid, collection, handler });
|
258
|
-
return () => this.unsubFromCol(lid);
|
259
|
-
}
|
260
|
-
unsubFromCol(lid) {
|
261
|
-
this._colListeners = this._colListeners.filter((x) => x.lid !== lid);
|
262
|
-
}
|
263
|
-
subToDoc(collection, id, handler) {
|
264
|
-
const lid = this._listenerId++;
|
265
|
-
this._docListeners.push({ lid, collection, id, handler });
|
266
|
-
return () => this.unsubFromDoc(lid);
|
267
|
-
}
|
268
|
-
unsubFromDoc(lid) {
|
269
|
-
this._docListeners = this._docListeners.filter((x) => x.lid !== lid);
|
270
|
-
}
|
271
|
-
subToField(collection, id, field, handler) {
|
272
|
-
const lid = this._listenerId++;
|
273
|
-
this._fieldListeners.push({ lid, collection, id, field, handler });
|
274
|
-
return () => this.unsubFromField(lid);
|
275
|
-
}
|
276
|
-
unsubFromField(lid) {
|
277
|
-
this._fieldListeners = this._fieldListeners.filter((x) => x.lid !== lid);
|
278
|
-
}
|
279
|
-
//
|
280
|
-
// Get
|
281
|
-
//
|
282
|
-
collection(colName) {
|
283
|
-
return this._collections[colName];
|
284
|
-
}
|
285
|
-
doc(colName, id) {
|
286
|
-
return this._collections[colName][id];
|
287
|
-
}
|
288
|
-
docs(colName, ids) {
|
289
|
-
const col = this._collections[colName];
|
290
|
-
return ids.map((id) => col[id]);
|
291
|
-
}
|
292
|
-
|
293
|
-
const col = this._indexes[colName];
|
294
|
-
if (!(index in col)) {
|
295
|
-
throw new Error(`index ${colName}/${index} not found`);
|
296
|
-
}
|
297
|
-
return col[index]
|
298
|
-
}
|
299
|
-
|
300
|
-
const col = this._indexes[
|
301
|
-
if (!(
|
302
|
-
throw new Error(`index ${
|
303
|
-
}
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
this.
|
374
|
-
|
375
|
-
|
376
|
-
}
|
377
|
-
}
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
this.
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
if (
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
//
|
447
|
-
let
|
448
|
-
|
449
|
-
let
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
if (
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
}
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
const
|
531
|
-
if (prev
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
const
|
551
|
-
|
552
|
-
const
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
[
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
}
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
}
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
}
|
582
|
-
}
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
if (
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
const
|
603
|
-
const
|
604
|
-
const
|
605
|
-
|
606
|
-
|
607
|
-
const
|
608
|
-
|
609
|
-
const
|
610
|
-
|
611
|
-
const
|
612
|
-
const
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
const
|
623
|
-
if (
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
const
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
const
|
641
|
-
|
642
|
-
const
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
};
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
}
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
}
|
678
|
-
}
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
// update
|
723
|
-
const
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
this.
|
736
|
-
|
737
|
-
|
738
|
-
}
|
739
|
-
}
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
const
|
762
|
-
const
|
763
|
-
const
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
[
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
this.
|
807
|
-
|
808
|
-
|
809
|
-
}
|
810
|
-
}
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
const
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
const
|
842
|
-
if (
|
843
|
-
handler(
|
844
|
-
}
|
845
|
-
}
|
846
|
-
const
|
847
|
-
for (let i = 0, il =
|
848
|
-
const { collection, handler } =
|
849
|
-
if (next[collection] !== prev[collection]) {
|
850
|
-
handler(next[collection], prev[collection], _txChanges[collection]
|
851
|
-
}
|
852
|
-
}
|
853
|
-
const
|
854
|
-
for (let i = 0, il =
|
855
|
-
const { handler } =
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
this.
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
this.
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
this.
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
1
|
+
import { BTree } from 'bplustree-mq4uj/btree.js';
|
2
|
+
import { Debounce } from 'util-3gcvv/class/Debounce.js';
|
3
|
+
import { deepEqual } from 'util-3gcvv/deepEqual.js';
|
4
|
+
import { objectEmpty, objectKeys } from 'util-3gcvv/object.js';
|
5
|
+
import { randomString } from 'util-3gcvv/string.js';
|
6
|
+
import { defaultComparators } from './comparators.js';
|
7
|
+
import { LocalDBState } from './LocalDBState.js';
|
8
|
+
// TODO
|
9
|
+
// composite index
|
10
|
+
// index filter / sort
|
11
|
+
// watch filtered/sorted collection
|
12
|
+
export class LocalDB {
|
13
|
+
constructor(config, initialData, options) {
|
14
|
+
this._txChangedFields = {};
|
15
|
+
this._txChanges = {};
|
16
|
+
this._txKey = null;
|
17
|
+
this._txOps = [];
|
18
|
+
this._txRollbacks = [];
|
19
|
+
this._txSnapshot = null;
|
20
|
+
this._txOptions = null;
|
21
|
+
this._noEvent = false;
|
22
|
+
this._undoable = false;
|
23
|
+
this._inUndoOrRedo = false;
|
24
|
+
this._opQueue = [];
|
25
|
+
this._commiting = false;
|
26
|
+
this._listenerId = 0;
|
27
|
+
this._dbListeners = [];
|
28
|
+
this._colListeners = [];
|
29
|
+
this._docListeners = [];
|
30
|
+
this._fieldListeners = [];
|
31
|
+
this.state = new LocalDBState({ history: [], historyIndex: 0 }, {});
|
32
|
+
this.saveDebounced = new Debounce(this.saveToStorage, 300, this);
|
33
|
+
this._options = options;
|
34
|
+
this._collectionNames = [];
|
35
|
+
this._collections = {};
|
36
|
+
this._config = {};
|
37
|
+
this._fields = {};
|
38
|
+
this._saveCols = {};
|
39
|
+
this._indexes = {};
|
40
|
+
const colNames = objectKeys(config);
|
41
|
+
for (let i = 0, il = colNames.length; i < il; i += 1) {
|
42
|
+
const col = colNames[i];
|
43
|
+
this.defineCollection(col, config[col], initialData?.[col]);
|
44
|
+
}
|
45
|
+
}
|
46
|
+
destroy() {
|
47
|
+
this.saveDebounced.destroy();
|
48
|
+
this._options = undefined;
|
49
|
+
this._collectionNames = [];
|
50
|
+
this._collections = {};
|
51
|
+
this._config = {};
|
52
|
+
this._fields = {};
|
53
|
+
for (let col in this._saveCols) {
|
54
|
+
this._saveCols[col]?.destroy();
|
55
|
+
}
|
56
|
+
this._saveCols = {};
|
57
|
+
for (let col in this._indexes) {
|
58
|
+
const indexes = this._indexes[col];
|
59
|
+
for (let name in indexes) {
|
60
|
+
indexes[name].clear();
|
61
|
+
}
|
62
|
+
}
|
63
|
+
this._indexes = {};
|
64
|
+
this._txChangedFields = {};
|
65
|
+
this._txChanges = {};
|
66
|
+
this._txKey = null;
|
67
|
+
this._txOps = [];
|
68
|
+
this._txRollbacks = [];
|
69
|
+
this._txSnapshot = null;
|
70
|
+
this._txOptions = null;
|
71
|
+
this.state.$destroy();
|
72
|
+
this._dbListeners = [];
|
73
|
+
this._colListeners = [];
|
74
|
+
this._docListeners = [];
|
75
|
+
this._fieldListeners = [];
|
76
|
+
this._opQueue = [];
|
77
|
+
this._listenerId = 0;
|
78
|
+
}
|
79
|
+
toJSON() {
|
80
|
+
return this._collections;
|
81
|
+
}
|
82
|
+
equals(other) {
|
83
|
+
return deepEqual(this._collections, other);
|
84
|
+
}
|
85
|
+
//
|
86
|
+
// Config
|
87
|
+
//
|
88
|
+
defineCollection(colName, config, initialData) {
|
89
|
+
const col = colName;
|
90
|
+
if (col in this._config) {
|
91
|
+
throw new Error(`Collection, "${col}", is already defined`);
|
92
|
+
}
|
93
|
+
// config
|
94
|
+
this._config[col] = config;
|
95
|
+
// collection name
|
96
|
+
this._collectionNames.push(col);
|
97
|
+
// field names
|
98
|
+
const fieldNames = objectKeys(config.fields);
|
99
|
+
this._fields[col] = fieldNames;
|
100
|
+
// save data to storage
|
101
|
+
this._saveCols[col] = new Debounce(() => this.saveCollectionToStorage(col), config.localStorageSetWait ?? 300);
|
102
|
+
// initial data
|
103
|
+
const { localStorageKey } = config;
|
104
|
+
let colData = {};
|
105
|
+
if (initialData) {
|
106
|
+
colData = initialData;
|
107
|
+
}
|
108
|
+
else if (localStorageKey) {
|
109
|
+
const snapshot = localStorage.getItem(localStorageKey);
|
110
|
+
if (snapshot) {
|
111
|
+
colData = JSON.parse(snapshot);
|
112
|
+
}
|
113
|
+
}
|
114
|
+
this._collections = { ...this._collections, [col]: colData };
|
115
|
+
// indexes
|
116
|
+
const docs = [];
|
117
|
+
if (colData) {
|
118
|
+
for (let id in colData) {
|
119
|
+
const doc = colData[id];
|
120
|
+
docs.push(doc);
|
121
|
+
}
|
122
|
+
}
|
123
|
+
const fieldIndexes = {};
|
124
|
+
for (let fi = 0, fl = fieldNames.length; fi < fl; fi += 1) {
|
125
|
+
const field = fieldNames[fi];
|
126
|
+
const { type, compare, index } = config.fields[field];
|
127
|
+
if (index) {
|
128
|
+
let comparator;
|
129
|
+
if (compare) {
|
130
|
+
comparator = index === 'desc' ? (a, b) => compare(b, a) : compare;
|
131
|
+
}
|
132
|
+
else {
|
133
|
+
comparator = defaultComparators[type]?.[index];
|
134
|
+
}
|
135
|
+
if (!comparator) {
|
136
|
+
throw new Error(`Comparator must be set to index ${col}/${field}`);
|
137
|
+
}
|
138
|
+
fieldIndexes[field] = new BTree(docs.map((d) => [d, d]), (a, b) => comparator(a[field], b[field]));
|
139
|
+
}
|
140
|
+
}
|
141
|
+
const { indexes } = config;
|
142
|
+
if (indexes) {
|
143
|
+
for (const name in indexes) {
|
144
|
+
const { compare } = indexes[name];
|
145
|
+
fieldIndexes[name] = new BTree(docs.map((d) => [d, d]), compare);
|
146
|
+
}
|
147
|
+
}
|
148
|
+
this._indexes[col] = fieldIndexes;
|
149
|
+
}
|
150
|
+
deleteCollection(col) {
|
151
|
+
if (!(col in this._config)) {
|
152
|
+
throw new Error(`Collection, "${col}", does not exist`);
|
153
|
+
}
|
154
|
+
delete this._config[col];
|
155
|
+
this._collectionNames = this._collectionNames.filter((x) => x !== col);
|
156
|
+
delete this._fields[col];
|
157
|
+
delete this._saveCols[col];
|
158
|
+
const next = { ...this._collections };
|
159
|
+
delete next[col];
|
160
|
+
this._collections = next;
|
161
|
+
}
|
162
|
+
existsCollection(col) {
|
163
|
+
return col in this._config;
|
164
|
+
}
|
165
|
+
get collectionNames() {
|
166
|
+
return this._collectionNames.slice();
|
167
|
+
}
|
168
|
+
//
|
169
|
+
// Transaction
|
170
|
+
//
|
171
|
+
beginTx(options = this._options) {
|
172
|
+
if (this._txKey == null) {
|
173
|
+
const key = randomString();
|
174
|
+
this._txChangedFields = {};
|
175
|
+
this._txChanges = {};
|
176
|
+
this._txKey = key;
|
177
|
+
this._txOps = [];
|
178
|
+
this._txRollbacks = [];
|
179
|
+
this._txSnapshot = this._collections;
|
180
|
+
this._txOptions = options || null;
|
181
|
+
this._noEvent = !!options?.noEvent;
|
182
|
+
this._undoable = !!options?.undoable;
|
183
|
+
return key;
|
184
|
+
}
|
185
|
+
return null;
|
186
|
+
}
|
187
|
+
endTx(txKey) {
|
188
|
+
if (txKey != null && this._txKey === txKey) {
|
189
|
+
try {
|
190
|
+
this._commitChanges();
|
191
|
+
}
|
192
|
+
catch (e) {
|
193
|
+
this._rollback();
|
194
|
+
throw e;
|
195
|
+
}
|
196
|
+
this._pushHistory();
|
197
|
+
// reset
|
198
|
+
this._txChangedFields = {};
|
199
|
+
this._txChanges = {};
|
200
|
+
this._txKey = null;
|
201
|
+
this._txOps = [];
|
202
|
+
this._txRollbacks = [];
|
203
|
+
this._txSnapshot = null;
|
204
|
+
this._txOptions = null;
|
205
|
+
this._noEvent = false;
|
206
|
+
this._undoable = false;
|
207
|
+
this._flushQueue();
|
208
|
+
}
|
209
|
+
}
|
210
|
+
_rollback() {
|
211
|
+
if (this._txSnapshot) {
|
212
|
+
this._collections = this._txSnapshot;
|
213
|
+
for (let i = 0, il = this._txRollbacks.length; i < il; i += 1) {
|
214
|
+
this._txRollbacks[i]();
|
215
|
+
}
|
216
|
+
// reset
|
217
|
+
this._txChangedFields = {};
|
218
|
+
this._txChanges = {};
|
219
|
+
this._txKey = null;
|
220
|
+
this._txOps = [];
|
221
|
+
this._txRollbacks = [];
|
222
|
+
this._txSnapshot = null;
|
223
|
+
this._noEvent = false;
|
224
|
+
this._undoable = false;
|
225
|
+
}
|
226
|
+
}
|
227
|
+
tx(fn, options) {
|
228
|
+
const txKey = this.beginTx(options);
|
229
|
+
if (txKey == null) {
|
230
|
+
throw new Error('Already in transaction');
|
231
|
+
}
|
232
|
+
try {
|
233
|
+
fn();
|
234
|
+
}
|
235
|
+
catch (e) {
|
236
|
+
this._rollback();
|
237
|
+
throw e;
|
238
|
+
}
|
239
|
+
this.endTx(txKey);
|
240
|
+
}
|
241
|
+
undoableTx(fn, options) {
|
242
|
+
return this.tx(fn, { ...options, undoable: true });
|
243
|
+
}
|
244
|
+
//
|
245
|
+
// Listeners
|
246
|
+
//
|
247
|
+
subToDB(handler) {
|
248
|
+
const lid = this._listenerId++;
|
249
|
+
this._dbListeners.push({ lid, handler });
|
250
|
+
return () => this.unsubFromDB(lid);
|
251
|
+
}
|
252
|
+
unsubFromDB(lid) {
|
253
|
+
this._dbListeners = this._dbListeners.filter((x) => x.lid !== lid);
|
254
|
+
}
|
255
|
+
subToCol(collection, handler) {
|
256
|
+
const lid = this._listenerId++;
|
257
|
+
this._colListeners.push({ lid, collection, handler });
|
258
|
+
return () => this.unsubFromCol(lid);
|
259
|
+
}
|
260
|
+
unsubFromCol(lid) {
|
261
|
+
this._colListeners = this._colListeners.filter((x) => x.lid !== lid);
|
262
|
+
}
|
263
|
+
subToDoc(collection, id, handler) {
|
264
|
+
const lid = this._listenerId++;
|
265
|
+
this._docListeners.push({ lid, collection, id, handler });
|
266
|
+
return () => this.unsubFromDoc(lid);
|
267
|
+
}
|
268
|
+
unsubFromDoc(lid) {
|
269
|
+
this._docListeners = this._docListeners.filter((x) => x.lid !== lid);
|
270
|
+
}
|
271
|
+
subToField(collection, id, field, handler) {
|
272
|
+
const lid = this._listenerId++;
|
273
|
+
this._fieldListeners.push({ lid, collection, id, field, handler });
|
274
|
+
return () => this.unsubFromField(lid);
|
275
|
+
}
|
276
|
+
unsubFromField(lid) {
|
277
|
+
this._fieldListeners = this._fieldListeners.filter((x) => x.lid !== lid);
|
278
|
+
}
|
279
|
+
//
|
280
|
+
// Get
|
281
|
+
//
|
282
|
+
collection(colName) {
|
283
|
+
return this._collections[colName];
|
284
|
+
}
|
285
|
+
doc(colName, id) {
|
286
|
+
return this._collections[colName][id];
|
287
|
+
}
|
288
|
+
docs(colName, ids) {
|
289
|
+
const col = this._collections[colName];
|
290
|
+
return ids.map((id) => col[id]);
|
291
|
+
}
|
292
|
+
getIndex(colName, index) {
|
293
|
+
const col = this._indexes[colName];
|
294
|
+
if (!(index in col)) {
|
295
|
+
throw new Error(`index ${colName}/${index} not found`);
|
296
|
+
}
|
297
|
+
return col[index];
|
298
|
+
}
|
299
|
+
docsOrderBy(colName, index) {
|
300
|
+
const col = this._indexes[colName];
|
301
|
+
if (!(index in col)) {
|
302
|
+
throw new Error(`index ${colName}/${index} not found`);
|
303
|
+
}
|
304
|
+
return col[index].valuesArray();
|
305
|
+
}
|
306
|
+
query({ collection, orderBy, limit, startAfter, startAt, endAfter, endAt, }) {
|
307
|
+
const col = this._indexes[collection];
|
308
|
+
if (!(orderBy in col)) {
|
309
|
+
throw new Error(`index ${collection}/${orderBy} not found`);
|
310
|
+
}
|
311
|
+
const index = col[orderBy];
|
312
|
+
// TODO
|
313
|
+
return index.valuesArray();
|
314
|
+
}
|
315
|
+
//
|
316
|
+
// Set / Update
|
317
|
+
//
|
318
|
+
setDoc(colName, doc, options) {
|
319
|
+
if (this._commiting) {
|
320
|
+
this._opQueue.push({ op: 'setDoc', args: [colName, doc] });
|
321
|
+
return;
|
322
|
+
}
|
323
|
+
const { id } = doc;
|
324
|
+
const keys = this._fields[colName];
|
325
|
+
const collections = this._collections;
|
326
|
+
const prev = collections[colName][id] || null;
|
327
|
+
if (prev != null) {
|
328
|
+
const copy = {};
|
329
|
+
// delete unset fields
|
330
|
+
for (let i = 0, il = keys.length; i < il; i += 1) {
|
331
|
+
const key = keys[i];
|
332
|
+
if (key in doc) {
|
333
|
+
copy[key] = doc[key];
|
334
|
+
}
|
335
|
+
else {
|
336
|
+
// undefined -> delete field
|
337
|
+
copy[key] = undefined;
|
338
|
+
}
|
339
|
+
}
|
340
|
+
return this.updateDoc(colName, id, copy, options);
|
341
|
+
}
|
342
|
+
// begin tx
|
343
|
+
const txKey = this.beginTx(options);
|
344
|
+
const _txChange = this._txChanges;
|
345
|
+
const _txChangedFields = this._txChangedFields;
|
346
|
+
const next = {};
|
347
|
+
for (let i = 0, il = keys.length; i < il; i += 1) {
|
348
|
+
const key = keys[i];
|
349
|
+
if (key in doc && doc[key] !== undefined) {
|
350
|
+
next[key] = doc[key];
|
351
|
+
// set changes
|
352
|
+
const changeMapCol = _txChangedFields[colName] || (_txChangedFields[colName] = {});
|
353
|
+
changeMapCol[key] = true;
|
354
|
+
const changeCol = _txChange[colName] || (_txChange[colName] = {});
|
355
|
+
const changeDoc = changeCol[id] || (changeCol[id] = {});
|
356
|
+
changeDoc[key] = doc[key];
|
357
|
+
}
|
358
|
+
}
|
359
|
+
// update data
|
360
|
+
this._collections = {
|
361
|
+
...collections,
|
362
|
+
[colName]: {
|
363
|
+
...collections[colName],
|
364
|
+
[id]: next,
|
365
|
+
},
|
366
|
+
};
|
367
|
+
// update indexes
|
368
|
+
const indexes = this._indexes[colName];
|
369
|
+
for (let indexName in indexes) {
|
370
|
+
indexes[indexName].set(next, next, true);
|
371
|
+
}
|
372
|
+
this._txRollbacks.push(() => {
|
373
|
+
const indexes = this._indexes[colName];
|
374
|
+
for (let indexName in indexes) {
|
375
|
+
indexes[indexName].delete(next);
|
376
|
+
}
|
377
|
+
});
|
378
|
+
// update undo history
|
379
|
+
if (this._undoable && !this._inUndoOrRedo) {
|
380
|
+
this._txOps.push({
|
381
|
+
undo: { op: 'deleteDoc', args: [colName, id] },
|
382
|
+
redo: { op: 'setDoc', args: [colName, doc] },
|
383
|
+
});
|
384
|
+
}
|
385
|
+
this._config[colName].foreignComputes?.forEach((compute) => {
|
386
|
+
compute.compute(this, [{ next, prev }]);
|
387
|
+
});
|
388
|
+
// end tx
|
389
|
+
this.endTx(txKey);
|
390
|
+
}
|
391
|
+
setDocs(colName, docs, options) {
|
392
|
+
if (!docs.length)
|
393
|
+
return;
|
394
|
+
if (this._commiting) {
|
395
|
+
this._opQueue.push({ op: 'setDocs', args: [colName, docs] });
|
396
|
+
return;
|
397
|
+
}
|
398
|
+
// begin tx
|
399
|
+
const txKey = this.beginTx(options);
|
400
|
+
// TODO undo, computes
|
401
|
+
for (let i = 0, il = docs.length; i < il; i += 1) {
|
402
|
+
this.setDoc(colName, docs[i], options);
|
403
|
+
}
|
404
|
+
// end tx
|
405
|
+
this.endTx(txKey);
|
406
|
+
}
|
407
|
+
_updateDoc(colName, prev, update) {
|
408
|
+
//
|
409
|
+
// Normalize Update
|
410
|
+
//
|
411
|
+
const config = this._config[colName];
|
412
|
+
const keys = this._fields[colName];
|
413
|
+
let next = { ...prev };
|
414
|
+
let changed = false;
|
415
|
+
let updateNormalized = {};
|
416
|
+
for (let ki = 0, kl = keys.length; ki < kl; ki += 1) {
|
417
|
+
const key = keys[ki];
|
418
|
+
if (!(key in update))
|
419
|
+
continue;
|
420
|
+
let nextVal = update[key];
|
421
|
+
const { normalize, equals } = config.fields[key];
|
422
|
+
if (normalize) {
|
423
|
+
nextVal = normalize(nextVal, next);
|
424
|
+
}
|
425
|
+
if (equals ? equals(nextVal, prev[key]) : nextVal === prev[key]) {
|
426
|
+
// noop
|
427
|
+
}
|
428
|
+
else {
|
429
|
+
changed = true;
|
430
|
+
updateNormalized[key] = nextVal;
|
431
|
+
if (nextVal === undefined) {
|
432
|
+
delete next[key];
|
433
|
+
}
|
434
|
+
else {
|
435
|
+
next[key] = nextVal;
|
436
|
+
}
|
437
|
+
}
|
438
|
+
}
|
439
|
+
if (!changed)
|
440
|
+
return null;
|
441
|
+
//
|
442
|
+
// Computes
|
443
|
+
//
|
444
|
+
const { computes } = config;
|
445
|
+
if (computes) {
|
446
|
+
// previous doc state before update before this iteration
|
447
|
+
let docBeforeUpdate = prev;
|
448
|
+
// update before this iteration
|
449
|
+
let updateBeforeIter = updateNormalized;
|
450
|
+
// docBeforeUpdate + updateBeforeIter
|
451
|
+
let docBeforeIter = next;
|
452
|
+
let updateByThisIter = {};
|
453
|
+
// docBeforeUpdate + updateBeforeIter + updateByThisIter
|
454
|
+
let docAfterIter = { ...next };
|
455
|
+
let changedByIter = false;
|
456
|
+
let count = 0;
|
457
|
+
while (true) {
|
458
|
+
for (let ci = 0, cl = computes.length; ci < cl; ci += 1) {
|
459
|
+
const { compute } = computes[ci];
|
460
|
+
// check deps has changed
|
461
|
+
let depsChanged = false;
|
462
|
+
const deps = computes[ci].deps;
|
463
|
+
for (let di = 0, dl = deps.length; di < dl; di += 1) {
|
464
|
+
const dep = deps[di];
|
465
|
+
if (dep in updateBeforeIter) {
|
466
|
+
depsChanged = true;
|
467
|
+
}
|
468
|
+
}
|
469
|
+
if (!depsChanged)
|
470
|
+
continue;
|
471
|
+
const computedUpdate = compute(docBeforeIter, docBeforeUpdate);
|
472
|
+
if (!computedUpdate)
|
473
|
+
continue;
|
474
|
+
for (let ki = 0, kl = keys.length; ki < kl; ki += 1) {
|
475
|
+
const key = keys[ki];
|
476
|
+
if (!(key in computedUpdate))
|
477
|
+
continue;
|
478
|
+
let nextVal = computedUpdate[key];
|
479
|
+
const { normalize, equals } = config.fields[key];
|
480
|
+
if (normalize) {
|
481
|
+
nextVal = normalize(nextVal, docAfterIter);
|
482
|
+
}
|
483
|
+
if (equals
|
484
|
+
? equals(nextVal, docBeforeIter[key])
|
485
|
+
: nextVal === docBeforeIter[key]) {
|
486
|
+
// noop
|
487
|
+
}
|
488
|
+
else {
|
489
|
+
changedByIter = true;
|
490
|
+
updateByThisIter[key] = nextVal;
|
491
|
+
if (nextVal === undefined) {
|
492
|
+
delete docAfterIter[key];
|
493
|
+
}
|
494
|
+
else {
|
495
|
+
docAfterIter[key] = nextVal;
|
496
|
+
}
|
497
|
+
}
|
498
|
+
}
|
499
|
+
}
|
500
|
+
if (changedByIter) {
|
501
|
+
docBeforeUpdate = docBeforeIter;
|
502
|
+
updateBeforeIter = updateByThisIter;
|
503
|
+
docBeforeIter = docAfterIter;
|
504
|
+
updateByThisIter = {};
|
505
|
+
docAfterIter = { ...docAfterIter };
|
506
|
+
changedByIter = false;
|
507
|
+
}
|
508
|
+
else {
|
509
|
+
next = docAfterIter;
|
510
|
+
break;
|
511
|
+
}
|
512
|
+
if (count++ > 100) {
|
513
|
+
throw new Error('too many compute loops');
|
514
|
+
}
|
515
|
+
}
|
516
|
+
}
|
517
|
+
if (!changed)
|
518
|
+
return null;
|
519
|
+
const changedKeys = keys.filter((key) => next[key] !== prev[key]);
|
520
|
+
if (!changedKeys.length)
|
521
|
+
return null;
|
522
|
+
return { next, changedKeys };
|
523
|
+
}
|
524
|
+
updateDoc(colName, id, updater, options) {
|
525
|
+
if (this._commiting) {
|
526
|
+
this._opQueue.push({ op: 'updateDoc', args: [colName, id, updater] });
|
527
|
+
return;
|
528
|
+
}
|
529
|
+
const collections = this._collections;
|
530
|
+
const prev = collections[colName][id];
|
531
|
+
if (prev == null) {
|
532
|
+
if (options?.ignoreNotFound || this._txOptions?.ignoreNotFound)
|
533
|
+
return;
|
534
|
+
this._rollback();
|
535
|
+
throw new Error(`Cannot update non-existing document, ${colName}/${id}`);
|
536
|
+
}
|
537
|
+
const update = typeof updater === 'function' ? updater(prev) : updater;
|
538
|
+
if (prev === update || update == null || objectEmpty(update))
|
539
|
+
return;
|
540
|
+
// begin tx
|
541
|
+
const txKey = this.beginTx(options);
|
542
|
+
const result = this._updateDoc(colName, prev, update);
|
543
|
+
if (result) {
|
544
|
+
const { next, changedKeys } = result;
|
545
|
+
const _txChange = this._txChanges;
|
546
|
+
const _txChangedFields = this._txChangedFields;
|
547
|
+
const prevFields = {};
|
548
|
+
const nextFields = {};
|
549
|
+
for (let ki = 0, kl = changedKeys.length; ki < kl; ki += 1) {
|
550
|
+
const key = changedKeys[ki];
|
551
|
+
const nextVal = next[key];
|
552
|
+
const prevVal = prev[key];
|
553
|
+
// update field
|
554
|
+
prevFields[key] = prevVal;
|
555
|
+
nextFields[key] = nextVal;
|
556
|
+
// set changes
|
557
|
+
const changeMapCol = _txChangedFields[colName] || (_txChangedFields[colName] = {});
|
558
|
+
changeMapCol[key] = true;
|
559
|
+
const changeCol = _txChange[colName] || (_txChange[colName] = {});
|
560
|
+
const changeDoc = changeCol[id] || (changeCol[id] = {});
|
561
|
+
changeDoc[key] = nextVal;
|
562
|
+
}
|
563
|
+
this._collections = {
|
564
|
+
...collections,
|
565
|
+
[colName]: {
|
566
|
+
...collections[colName],
|
567
|
+
[id]: next,
|
568
|
+
},
|
569
|
+
};
|
570
|
+
// update indexes
|
571
|
+
const indexes = this._indexes[colName];
|
572
|
+
for (let indexName in indexes) {
|
573
|
+
indexes[indexName].delete(prev);
|
574
|
+
indexes[indexName].set(next, next, true);
|
575
|
+
}
|
576
|
+
this._txRollbacks.push(() => {
|
577
|
+
const indexes = this._indexes[colName];
|
578
|
+
for (let indexName in indexes) {
|
579
|
+
indexes[indexName].delete(next);
|
580
|
+
indexes[indexName].set(prev, prev, true);
|
581
|
+
}
|
582
|
+
});
|
583
|
+
// update undo history
|
584
|
+
if (this._undoable && !this._inUndoOrRedo) {
|
585
|
+
this._txOps.push({
|
586
|
+
undo: { op: 'updateDoc', args: [colName, id, prevFields] },
|
587
|
+
redo: { op: 'updateDoc', args: [colName, id, nextFields] },
|
588
|
+
});
|
589
|
+
}
|
590
|
+
this._config[colName].foreignComputes?.forEach((compute) => {
|
591
|
+
compute.compute(this, [{ next, prev }]);
|
592
|
+
});
|
593
|
+
}
|
594
|
+
// end tx
|
595
|
+
this.endTx(txKey);
|
596
|
+
}
|
597
|
+
updateDocs(colName, updater, options) {
|
598
|
+
if (this._commiting) {
|
599
|
+
this._opQueue.push({ op: 'updateDocs', args: [colName, updater] });
|
600
|
+
return;
|
601
|
+
}
|
602
|
+
const collections = this._collections;
|
603
|
+
const prevCol = collections[colName];
|
604
|
+
const update = typeof updater === 'function' ? updater(prevCol) : updater;
|
605
|
+
if (prevCol === update || update == null || objectEmpty(update))
|
606
|
+
return;
|
607
|
+
const nextCol = { ...prevCol };
|
608
|
+
// begin tx
|
609
|
+
const txKey = this.beginTx(options);
|
610
|
+
const _txChange = this._txChanges;
|
611
|
+
const _txChangedFields = this._txChangedFields;
|
612
|
+
const prevUpdateMap = {};
|
613
|
+
const nextUpdateMap = {};
|
614
|
+
const prevDocs = [];
|
615
|
+
const nextDocs = [];
|
616
|
+
const foreignComputeArgs = [];
|
617
|
+
let changed = false;
|
618
|
+
const ignoreNotFound = !!(options?.ignoreNotFound || this._txOptions?.ignoreNotFound);
|
619
|
+
const ids = objectKeys(update);
|
620
|
+
for (let i = 0, il = ids.length; i < il; i += 1) {
|
621
|
+
const id = ids[i];
|
622
|
+
const prevDoc = prevCol[id];
|
623
|
+
if (prevDoc == null) {
|
624
|
+
if (ignoreNotFound)
|
625
|
+
continue;
|
626
|
+
this._rollback();
|
627
|
+
throw new Error(`Cannot update non-existing document, ${colName}/${id}`);
|
628
|
+
}
|
629
|
+
const updateDoc = update[id];
|
630
|
+
if (updateDoc == null || objectEmpty(updateDoc))
|
631
|
+
continue;
|
632
|
+
const result = this._updateDoc(colName, prevDoc, updateDoc);
|
633
|
+
if (!result)
|
634
|
+
continue;
|
635
|
+
changed = true;
|
636
|
+
const { next: nextDoc, changedKeys } = result;
|
637
|
+
const prevFields = {};
|
638
|
+
const nextFields = {};
|
639
|
+
for (let ki = 0, kl = changedKeys.length; ki < kl; ki += 1) {
|
640
|
+
const key = changedKeys[ki];
|
641
|
+
const nextVal = nextDoc[key];
|
642
|
+
const prevVal = prevDoc[key];
|
643
|
+
// update field
|
644
|
+
prevFields[key] = prevVal;
|
645
|
+
nextFields[key] = nextVal;
|
646
|
+
// set changes
|
647
|
+
const changeMapCol = _txChangedFields[colName] || (_txChangedFields[colName] = {});
|
648
|
+
changeMapCol[key] = true;
|
649
|
+
const changeCol = _txChange[colName] || (_txChange[colName] = {});
|
650
|
+
const changeDoc = changeCol[id] || (changeCol[id] = {});
|
651
|
+
changeDoc[key] = nextVal;
|
652
|
+
}
|
653
|
+
nextCol[id] = nextDoc;
|
654
|
+
prevUpdateMap[id] = prevFields;
|
655
|
+
nextUpdateMap[id] = nextFields;
|
656
|
+
prevDocs.push(prevDoc);
|
657
|
+
nextDocs.push(nextDoc);
|
658
|
+
foreignComputeArgs.push({ next: nextDoc, prev: prevDoc });
|
659
|
+
}
|
660
|
+
// update data
|
661
|
+
if (changed) {
|
662
|
+
this._collections = {
|
663
|
+
...collections,
|
664
|
+
[colName]: nextCol,
|
665
|
+
};
|
666
|
+
// update indexes
|
667
|
+
const indexes = this._indexes[colName];
|
668
|
+
for (let indexName in indexes) {
|
669
|
+
indexes[indexName].deleteKeys(prevDocs);
|
670
|
+
indexes[indexName].setPairs(nextDocs.map((doc) => [doc, doc]), true);
|
671
|
+
}
|
672
|
+
this._txRollbacks.push(() => {
|
673
|
+
const indexes = this._indexes[colName];
|
674
|
+
for (let indexName in indexes) {
|
675
|
+
indexes[indexName].deleteKeys(nextDocs);
|
676
|
+
indexes[indexName].setPairs(prevDocs.map((doc) => [doc, doc]), true);
|
677
|
+
}
|
678
|
+
});
|
679
|
+
// update undo history
|
680
|
+
if (this._undoable && !this._inUndoOrRedo) {
|
681
|
+
this._txOps.push({
|
682
|
+
undo: { op: 'updateDocs', args: [colName, prevUpdateMap] },
|
683
|
+
redo: { op: 'updateDocs', args: [colName, nextUpdateMap] },
|
684
|
+
});
|
685
|
+
}
|
686
|
+
this._config[colName].foreignComputes?.forEach((compute) => {
|
687
|
+
compute.compute(this, foreignComputeArgs);
|
688
|
+
});
|
689
|
+
}
|
690
|
+
// end tx
|
691
|
+
this.endTx(txKey);
|
692
|
+
}
|
693
|
+
deleteDoc(colName, id, options) {
|
694
|
+
if (this._commiting) {
|
695
|
+
this._opQueue.push({ op: 'deleteDoc', args: [colName, id] });
|
696
|
+
return;
|
697
|
+
}
|
698
|
+
const collections = this._collections;
|
699
|
+
const prev = collections[colName][id];
|
700
|
+
if (prev == null) {
|
701
|
+
if (options?.idempotent || this._txOptions?.idempotent)
|
702
|
+
return;
|
703
|
+
this._rollback();
|
704
|
+
throw new Error(`Cannot delete non-existing document, ${colName}/${id}`);
|
705
|
+
}
|
706
|
+
// begin tx
|
707
|
+
const txKey = this.beginTx(options);
|
708
|
+
// changes
|
709
|
+
const _txChange = this._txChanges;
|
710
|
+
const _txChangedFields = this._txChangedFields;
|
711
|
+
const keys = this._fields[colName];
|
712
|
+
for (let i = 0, il = keys.length; i < il; i += 1) {
|
713
|
+
const key = keys[i];
|
714
|
+
if (key in prev && prev[key] !== undefined) {
|
715
|
+
// set changes
|
716
|
+
const changeMapCol = _txChangedFields[colName] || (_txChangedFields[colName] = {});
|
717
|
+
changeMapCol[key] = true;
|
718
|
+
}
|
719
|
+
}
|
720
|
+
const changeCol = _txChange[colName] || (_txChange[colName] = {});
|
721
|
+
changeCol[id] = null;
|
722
|
+
// update data
|
723
|
+
const nextCol = { ...collections[colName] };
|
724
|
+
delete nextCol[id];
|
725
|
+
this._collections = {
|
726
|
+
...collections,
|
727
|
+
[colName]: nextCol,
|
728
|
+
};
|
729
|
+
// update indexes
|
730
|
+
const indexes = this._indexes[colName];
|
731
|
+
for (let indexName in indexes) {
|
732
|
+
indexes[indexName].delete(prev);
|
733
|
+
}
|
734
|
+
this._txRollbacks.push(() => {
|
735
|
+
const indexes = this._indexes[colName];
|
736
|
+
for (let indexName in indexes) {
|
737
|
+
indexes[indexName].set(prev, prev, true);
|
738
|
+
}
|
739
|
+
});
|
740
|
+
// update undo history
|
741
|
+
if (this._undoable && !this._inUndoOrRedo) {
|
742
|
+
this._txOps.push({
|
743
|
+
undo: { op: 'setDoc', args: [colName, prev] },
|
744
|
+
redo: { op: 'deleteDoc', args: [colName, id] },
|
745
|
+
});
|
746
|
+
}
|
747
|
+
this._config[colName].foreignComputes?.forEach((compute) => {
|
748
|
+
compute.compute(this, [{ next: null, prev }]);
|
749
|
+
});
|
750
|
+
// end tx
|
751
|
+
this.endTx(txKey);
|
752
|
+
}
|
753
|
+
deleteDocs(colName, ids, options) {
|
754
|
+
if (!ids?.length)
|
755
|
+
return;
|
756
|
+
if (this._commiting) {
|
757
|
+
this._opQueue.push({ op: 'deleteDocs', args: [colName, ids] });
|
758
|
+
return;
|
759
|
+
}
|
760
|
+
// begin tx
|
761
|
+
const txKey = this.beginTx(options);
|
762
|
+
const idempotent = !!(options?.idempotent || this._txOptions?.idempotent);
|
763
|
+
const _txChange = this._txChanges;
|
764
|
+
const _txChangedFields = this._txChangedFields;
|
765
|
+
const collections = this._collections;
|
766
|
+
const prevCol = collections[colName];
|
767
|
+
const nextCol = { ...prevCol };
|
768
|
+
const fields = this._fields[colName];
|
769
|
+
const prevDocs = [];
|
770
|
+
const foreignComputeArgs = [];
|
771
|
+
for (let i = 0, il = ids.length; i < il; i += 1) {
|
772
|
+
const id = ids[i];
|
773
|
+
const prev = prevCol[id];
|
774
|
+
if (prev == null) {
|
775
|
+
if (idempotent)
|
776
|
+
continue;
|
777
|
+
this._rollback();
|
778
|
+
throw new Error(`Cannot delete non-existing document, ${colName}/${id}`);
|
779
|
+
}
|
780
|
+
// changes
|
781
|
+
for (let fi = 0, fl = fields.length; fi < fl; fi += 1) {
|
782
|
+
const key = fields[fi];
|
783
|
+
if (key in prev && prev[key] !== undefined) {
|
784
|
+
// set changes
|
785
|
+
const changeMapCol = _txChangedFields[colName] || (_txChangedFields[colName] = {});
|
786
|
+
changeMapCol[key] = true;
|
787
|
+
}
|
788
|
+
}
|
789
|
+
const changeCol = _txChange[colName] || (_txChange[colName] = {});
|
790
|
+
changeCol[id] = null;
|
791
|
+
delete nextCol[id];
|
792
|
+
prevDocs.push(prev);
|
793
|
+
foreignComputeArgs.push({ next: null, prev });
|
794
|
+
}
|
795
|
+
// update data
|
796
|
+
this._collections = {
|
797
|
+
...collections,
|
798
|
+
[colName]: nextCol,
|
799
|
+
};
|
800
|
+
// update indexes
|
801
|
+
const indexes = this._indexes[colName];
|
802
|
+
for (let indexName in indexes) {
|
803
|
+
indexes[indexName].deleteKeys(prevDocs);
|
804
|
+
}
|
805
|
+
this._txRollbacks.push(() => {
|
806
|
+
const indexes = this._indexes[colName];
|
807
|
+
for (let indexName in indexes) {
|
808
|
+
indexes[indexName].setPairs(prevDocs.map((d) => [d, d]), true);
|
809
|
+
}
|
810
|
+
});
|
811
|
+
// update undo history
|
812
|
+
if (this._undoable && !this._inUndoOrRedo) {
|
813
|
+
this._txOps.push({
|
814
|
+
undo: { op: 'setDocs', args: [colName, prevDocs] },
|
815
|
+
redo: { op: 'deleteDocs', args: [colName, ids] },
|
816
|
+
});
|
817
|
+
}
|
818
|
+
this._config[colName].foreignComputes?.forEach((compute) => {
|
819
|
+
compute.compute(this, foreignComputeArgs);
|
820
|
+
});
|
821
|
+
// end tx
|
822
|
+
this.endTx(txKey);
|
823
|
+
}
|
824
|
+
_commitChanges() {
|
825
|
+
if (this._commiting)
|
826
|
+
return;
|
827
|
+
const prev = this._txSnapshot;
|
828
|
+
const next = this._collections;
|
829
|
+
if (prev === next || prev == null)
|
830
|
+
return;
|
831
|
+
this._commiting = true;
|
832
|
+
const _txChanges = this._txChanges;
|
833
|
+
const _txChangedFields = this._txChangedFields;
|
834
|
+
const txContext = this._txOptions?.context || {};
|
835
|
+
// listeners
|
836
|
+
const fieldListeners = this._fieldListeners;
|
837
|
+
for (let i = 0, il = fieldListeners.length; i < il; i += 1) {
|
838
|
+
const { collection, id, field, handler } = fieldListeners[i];
|
839
|
+
const field2 = field;
|
840
|
+
const nextDoc = next[collection][id];
|
841
|
+
const prevDoc = prev[collection][id];
|
842
|
+
if (nextDoc?.[field2] !== prevDoc?.[field2]) {
|
843
|
+
handler(nextDoc?.[field2], prevDoc?.[field2], nextDoc, prevDoc, txContext);
|
844
|
+
}
|
845
|
+
}
|
846
|
+
const docListeners = this._docListeners;
|
847
|
+
for (let i = 0, il = docListeners.length; i < il; i += 1) {
|
848
|
+
const { collection, id, handler } = docListeners[i];
|
849
|
+
if (next[collection][id] !== prev[collection][id]) {
|
850
|
+
handler(next[collection][id], prev[collection][id], _txChanges[collection][id], txContext);
|
851
|
+
}
|
852
|
+
}
|
853
|
+
const colListeners = this._colListeners;
|
854
|
+
for (let i = 0, il = colListeners.length; i < il; i += 1) {
|
855
|
+
const { collection, handler } = colListeners[i];
|
856
|
+
if (next[collection] !== prev[collection]) {
|
857
|
+
handler(next[collection], prev[collection], _txChanges[collection], _txChangedFields[collection], txContext);
|
858
|
+
}
|
859
|
+
}
|
860
|
+
const dbListeners = this._dbListeners;
|
861
|
+
for (let i = 0, il = dbListeners.length; i < il; i += 1) {
|
862
|
+
const { handler } = dbListeners[i];
|
863
|
+
handler(next, prev, _txChanges, _txChangedFields, txContext);
|
864
|
+
}
|
865
|
+
// save to storage
|
866
|
+
const cols = this._collectionNames;
|
867
|
+
for (let i = 0, il = cols.length; i < il; i += 1) {
|
868
|
+
const col = cols[i];
|
869
|
+
if (next[col] !== prev[col]) {
|
870
|
+
this._saveCols[col].debounced();
|
871
|
+
}
|
872
|
+
}
|
873
|
+
this._commiting = false;
|
874
|
+
}
|
875
|
+
//
|
876
|
+
// Queue
|
877
|
+
//
|
878
|
+
_flushQueue() {
|
879
|
+
let count = 0;
|
880
|
+
while (this._opQueue.length) {
|
881
|
+
const { op, args } = this._opQueue.shift();
|
882
|
+
this[op].apply(this, args);
|
883
|
+
if (count++ > 100) {
|
884
|
+
throw new Error('too much');
|
885
|
+
}
|
886
|
+
}
|
887
|
+
}
|
888
|
+
//
|
889
|
+
// History
|
890
|
+
//
|
891
|
+
_pushHistory() {
|
892
|
+
if (!this._inUndoOrRedo && this._txOps.length) {
|
893
|
+
this.state.$update(({ history, historyIndex }) => {
|
894
|
+
const nextHistory = [...history.slice(0, historyIndex), this._txOps];
|
895
|
+
return {
|
896
|
+
history: nextHistory,
|
897
|
+
historyIndex: nextHistory.length,
|
898
|
+
};
|
899
|
+
});
|
900
|
+
this._txOps = [];
|
901
|
+
}
|
902
|
+
}
|
903
|
+
undo() {
|
904
|
+
if (this._txKey) {
|
905
|
+
throw new Error('Cannot undo during transaction');
|
906
|
+
}
|
907
|
+
const { undoItem } = this.state;
|
908
|
+
if (undoItem) {
|
909
|
+
this._inUndoOrRedo = true;
|
910
|
+
this.tx(() => {
|
911
|
+
for (let i = undoItem.length - 1; i >= 0; i -= 1) {
|
912
|
+
const { undo } = undoItem[i];
|
913
|
+
this[undo.op].apply(this, undo.args);
|
914
|
+
}
|
915
|
+
this.state.historyIndex--;
|
916
|
+
});
|
917
|
+
this._inUndoOrRedo = false;
|
918
|
+
}
|
919
|
+
}
|
920
|
+
redo() {
|
921
|
+
if (this._txKey) {
|
922
|
+
throw new Error('Cannot redo during transaction');
|
923
|
+
}
|
924
|
+
const { redoItem } = this.state;
|
925
|
+
if (redoItem) {
|
926
|
+
this._inUndoOrRedo = true;
|
927
|
+
this.tx(() => {
|
928
|
+
for (let i = 0, il = redoItem.length; i < il; i += 1) {
|
929
|
+
const { redo } = redoItem[i];
|
930
|
+
this[redo.op].apply(this, redo.args);
|
931
|
+
}
|
932
|
+
this.state.historyIndex++;
|
933
|
+
});
|
934
|
+
this._inUndoOrRedo = false;
|
935
|
+
}
|
936
|
+
}
|
937
|
+
// Local Storage
|
938
|
+
loadCollectionFromStorage(collection) {
|
939
|
+
if (collection in this._config) {
|
940
|
+
const { localStorageKey } = this._config[collection];
|
941
|
+
if (localStorageKey) {
|
942
|
+
const snapshot = localStorage.getItem(localStorageKey);
|
943
|
+
if (snapshot) {
|
944
|
+
return JSON.parse(snapshot);
|
945
|
+
}
|
946
|
+
}
|
947
|
+
return undefined;
|
948
|
+
}
|
949
|
+
}
|
950
|
+
saveCollectionToStorage(collection) {
|
951
|
+
if (collection in this._config) {
|
952
|
+
const { localStorageKey } = this._config[collection];
|
953
|
+
if (localStorageKey) {
|
954
|
+
localStorage.setItem(localStorageKey, JSON.stringify(this._collections[collection]));
|
955
|
+
}
|
956
|
+
}
|
957
|
+
}
|
958
|
+
loadFromStorage() {
|
959
|
+
const data = this._collections;
|
960
|
+
const cols = this._collectionNames;
|
961
|
+
for (let i = 0, il = cols.length; i < il; i += 1) {
|
962
|
+
const col = cols[i];
|
963
|
+
const colData = this.loadCollectionFromStorage(col);
|
964
|
+
if (colData) {
|
965
|
+
data[col] = colData;
|
966
|
+
}
|
967
|
+
}
|
968
|
+
}
|
969
|
+
saveToStorage() {
|
970
|
+
const data = this._collections;
|
971
|
+
const cols = this._collectionNames;
|
972
|
+
for (let i = 0, il = cols.length; i < il; i += 1) {
|
973
|
+
const col = cols[i];
|
974
|
+
const { localStorageKey } = this._config[col];
|
975
|
+
if (localStorageKey) {
|
976
|
+
localStorage.setItem(localStorageKey, JSON.stringify(data[col]));
|
977
|
+
}
|
978
|
+
}
|
979
|
+
}
|
980
|
+
}
|