localdb-ces6q 0.2.23 → 0.2.24

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.js DELETED
@@ -1,980 +0,0 @@
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
- }