@incoqnito.io/bajadab 1.2.2 → 1.3.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 +192 -174
- package/dist/base.d.ts +13 -9
- package/dist/base.js.map +1 -1
- package/dist/basics.d.ts +27 -10
- package/dist/basics.js +378 -251
- package/dist/basics.js.map +1 -1
- package/dist/fileio.d.ts +7 -0
- package/dist/fileio.js +65 -14
- package/dist/fileio.js.map +1 -1
- package/package.json +2 -2
- package/src/main/base.ts +235 -231
- package/src/main/basics.ts +1058 -912
- package/src/main/fileio.ts +270 -207
package/dist/basics.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { uuidv7, } from "uuidv7";
|
|
3
|
-
import {
|
|
3
|
+
import { runReentrantProtected } from "@incoqnito.io/ts-iq-core";
|
|
4
4
|
import { EBArrayDeletionStrategy, EBAutoFlushStrategy, EBAutoIDStrategy, EBDirtyUnloadStrategy, EBDuplicateIDStrategy, EBHandlerMode, EBMemStorageStrategy, EBModificationType, } from "./base.js";
|
|
5
5
|
import { canonicalName } from "./utils.js";
|
|
6
6
|
/** Default {@link TGetIDFunction}: reads `v.id`, `undefined` if absent. */
|
|
7
|
-
export const DEFAULT_GET_ID = (v) => ("id" in v) ? v.id : undefined;
|
|
7
|
+
export const DEFAULT_GET_ID = (v) => (v !== null && typeof v === 'object' && "id" in v) ? v.id : undefined;
|
|
8
8
|
/** Default {@link TSetIDFunction}: assigns to `v.id`. */
|
|
9
9
|
export const DEFAULT_SET_ID = async (v, id) => { v.id = id; };
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
MutexKeys["DATABASE"] = " database ";
|
|
14
|
-
})(MutexKeys || (MutexKeys = {}));
|
|
10
|
+
function normalizeID(id) {
|
|
11
|
+
return id === undefined || id === null || id === "" ? undefined : String(id);
|
|
12
|
+
}
|
|
15
13
|
/** {@link IBCollectionConfig} used when a collection is created without an explicit one. */
|
|
16
14
|
export const DEFAULT_COLLECTION_CONFIG = {
|
|
17
15
|
arrayDeletionStrategy: EBArrayDeletionStrategy.IN_PLACE,
|
|
@@ -30,16 +28,24 @@ export class BDatabase {
|
|
|
30
28
|
collections = new Map();
|
|
31
29
|
io;
|
|
32
30
|
functionRegistry;
|
|
31
|
+
iid = uuidv7();
|
|
32
|
+
transactionScope = new AsyncLocalStorage();
|
|
33
33
|
constructor(io, config, functionRegistry) {
|
|
34
|
-
this.config = config ?? DEFAULT_DATABASE_CONFIG;
|
|
34
|
+
this.config = { ...(config ?? DEFAULT_DATABASE_CONFIG) };
|
|
35
35
|
this.io = io;
|
|
36
36
|
this.functionRegistry = functionRegistry;
|
|
37
37
|
}
|
|
38
|
+
async runExclusive(fn) {
|
|
39
|
+
return runReentrantProtected(this.iid, fn);
|
|
40
|
+
}
|
|
41
|
+
isInTransaction(collectionName) {
|
|
42
|
+
return this.transactionScope.getStore()?.has(collectionName) ?? false;
|
|
43
|
+
}
|
|
38
44
|
addCollection(collection) {
|
|
39
45
|
this.collections.set(collection.collectionName, collection);
|
|
40
46
|
}
|
|
41
47
|
async getCollection(collectionName, collectionConfig) {
|
|
42
|
-
return
|
|
48
|
+
return this.runExclusive(async () => {
|
|
43
49
|
const cname = canonicalName(collectionName);
|
|
44
50
|
let ret = this.collections.get(cname);
|
|
45
51
|
if (!ret) {
|
|
@@ -49,25 +55,26 @@ export class BDatabase {
|
|
|
49
55
|
});
|
|
50
56
|
}
|
|
51
57
|
async createCollection(collectionName, collectionConfig) {
|
|
52
|
-
return
|
|
58
|
+
return this.runExclusive(async () => {
|
|
53
59
|
const cname = canonicalName(collectionName);
|
|
54
60
|
if (this.collections.has(cname)) {
|
|
55
61
|
throw new Error(`cannot create already existing collection ${collectionName}`);
|
|
56
62
|
}
|
|
57
63
|
const id = uuidv7();
|
|
58
|
-
const ret = new BCollection(this, id, collectionName, collectionConfig ?? this.config.defaultCollectionConfig);
|
|
64
|
+
const ret = new BCollection(this, id, collectionName, { ...(collectionConfig ?? this.config.defaultCollectionConfig) });
|
|
65
|
+
await this.io.createCollection(ret);
|
|
59
66
|
this.addCollection(ret);
|
|
60
|
-
return
|
|
67
|
+
return ret;
|
|
61
68
|
});
|
|
62
69
|
}
|
|
63
70
|
async hasCollection(collectionName) {
|
|
64
|
-
return
|
|
71
|
+
return this.runExclusive(async () => {
|
|
65
72
|
return this.collections.has(canonicalName(collectionName));
|
|
66
73
|
});
|
|
67
74
|
}
|
|
68
75
|
async dropCollection(collectionName) {
|
|
69
76
|
const cname = canonicalName(collectionName);
|
|
70
|
-
return
|
|
77
|
+
return this.runExclusive(async () => {
|
|
71
78
|
const coll = this.collections.get(cname);
|
|
72
79
|
if (coll) {
|
|
73
80
|
coll.markDropped();
|
|
@@ -80,8 +87,7 @@ export class BDatabase {
|
|
|
80
87
|
});
|
|
81
88
|
}
|
|
82
89
|
async snapshot(targetDir, options) {
|
|
83
|
-
|
|
84
|
-
return runMultiReentrantProtected([MutexKeys.DATABASE, ...collectionNames], async () => {
|
|
90
|
+
return this.runExclusive(async () => {
|
|
85
91
|
for (const collection of this.collections.values()) {
|
|
86
92
|
await collection.flush();
|
|
87
93
|
}
|
|
@@ -89,13 +95,21 @@ export class BDatabase {
|
|
|
89
95
|
});
|
|
90
96
|
}
|
|
91
97
|
async transaction(collectionNames, fn) {
|
|
98
|
+
if (this.transactionScope.getStore() !== undefined) {
|
|
99
|
+
throw new Error("nested transactions are not supported");
|
|
100
|
+
}
|
|
92
101
|
const canonicalNames = Array.from(new Set(collectionNames.map(canonicalName)));
|
|
93
|
-
return
|
|
102
|
+
return this.runExclusive(async () => {
|
|
94
103
|
const originals = new Map();
|
|
95
104
|
const clones = new Map();
|
|
96
105
|
for (const name of canonicalNames) {
|
|
97
|
-
const original =
|
|
106
|
+
const original = this.collections.get(name);
|
|
107
|
+
if (!original) {
|
|
108
|
+
throw new Error(`no such collection ${name}`);
|
|
109
|
+
}
|
|
98
110
|
originals.set(name, original);
|
|
111
|
+
}
|
|
112
|
+
for (const [name, original] of originals) {
|
|
99
113
|
clones.set(name, await original.cloneForTransaction());
|
|
100
114
|
}
|
|
101
115
|
const tx = {
|
|
@@ -107,9 +121,36 @@ export class BDatabase {
|
|
|
107
121
|
return clone;
|
|
108
122
|
},
|
|
109
123
|
};
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
124
|
+
try {
|
|
125
|
+
await this.transactionScope.run(new Set(canonicalNames), () => fn(tx));
|
|
126
|
+
const prepared = [];
|
|
127
|
+
try {
|
|
128
|
+
for (const name of canonicalNames) {
|
|
129
|
+
prepared.push(await originals.get(name).prepareTransactionCommit(clones.get(name)));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
await Promise.all(prepared.map(p => p.write.discard()));
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
let renamed = 0;
|
|
137
|
+
try {
|
|
138
|
+
for (const p of prepared) {
|
|
139
|
+
await p.write.commit();
|
|
140
|
+
renamed++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
await Promise.all(prepared.slice(renamed).map(p => p.write.discard()));
|
|
145
|
+
const applied = prepared.slice(0, renamed);
|
|
146
|
+
applied.forEach(p => p.apply());
|
|
147
|
+
for (const p of applied) {
|
|
148
|
+
await p.fire();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
clones.forEach(clone => clone.markDropped());
|
|
113
154
|
}
|
|
114
155
|
});
|
|
115
156
|
}
|
|
@@ -124,26 +165,19 @@ export class BCollection {
|
|
|
124
165
|
loaded = false;
|
|
125
166
|
dirty = false;
|
|
126
167
|
dropped = false;
|
|
168
|
+
transactionClone = false;
|
|
127
169
|
addHandlers = [];
|
|
128
170
|
updateHandlers = [];
|
|
129
171
|
deleteHandlers = [];
|
|
130
172
|
indexDefinitions = new Map();
|
|
131
173
|
indexRuntime = new Map();
|
|
174
|
+
indexedKeys = new Map();
|
|
175
|
+
txChanges;
|
|
132
176
|
constructor(database, collectionID, collectionName, collectionConfig) {
|
|
133
177
|
this.database = database;
|
|
134
178
|
this.collectionID = collectionID;
|
|
135
179
|
this.collectionName = canonicalName(collectionName);
|
|
136
180
|
this.collectionConfig = collectionConfig;
|
|
137
|
-
switch (collectionConfig.memStorageStrategy) {
|
|
138
|
-
case EBMemStorageStrategy.MAP:
|
|
139
|
-
// if (!collectionConfig.getID || !collectionConfig.setID) {
|
|
140
|
-
// throw new Error("need id accessors configured when using map mem storage");
|
|
141
|
-
// }
|
|
142
|
-
break;
|
|
143
|
-
default:
|
|
144
|
-
case EBMemStorageStrategy.ARRAY:
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
181
|
}
|
|
148
182
|
async checkLoaded() {
|
|
149
183
|
if (!this.loaded) {
|
|
@@ -151,7 +185,7 @@ export class BCollection {
|
|
|
151
185
|
}
|
|
152
186
|
}
|
|
153
187
|
async runExclusive(fn) {
|
|
154
|
-
return
|
|
188
|
+
return this.database.runExclusive(fn);
|
|
155
189
|
}
|
|
156
190
|
async runProtected(fn) {
|
|
157
191
|
return this.runExclusive(async () => {
|
|
@@ -161,6 +195,14 @@ export class BCollection {
|
|
|
161
195
|
return fn();
|
|
162
196
|
});
|
|
163
197
|
}
|
|
198
|
+
async runMutation(fn) {
|
|
199
|
+
return this.runProtected(async () => {
|
|
200
|
+
if (!this.transactionClone && this.database.isInTransaction(this.collectionName)) {
|
|
201
|
+
throw new Error(`collection "${this.collectionName}" is part of a running transaction; write through tx.getCollection() instead`);
|
|
202
|
+
}
|
|
203
|
+
return fn();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
164
206
|
/**
|
|
165
207
|
* call this only within the protecting mutex
|
|
166
208
|
*/
|
|
@@ -206,7 +248,7 @@ export class BCollection {
|
|
|
206
248
|
if (!byKey) {
|
|
207
249
|
throw new Error(`index "${indexKey}" is not configured`);
|
|
208
250
|
}
|
|
209
|
-
return byKey.get(key)
|
|
251
|
+
return Array.from(byKey.get(key) ?? []);
|
|
210
252
|
});
|
|
211
253
|
}
|
|
212
254
|
async invokeHandler(fn, modificationType) {
|
|
@@ -214,7 +256,8 @@ export class BCollection {
|
|
|
214
256
|
await fn();
|
|
215
257
|
}
|
|
216
258
|
catch (error) {
|
|
217
|
-
|
|
259
|
+
// TODO switch logging to ILogging where necessary
|
|
260
|
+
console.error(`bajadab: collection "${this.collectionName}" ${modificationType} handler failed`, error);
|
|
218
261
|
}
|
|
219
262
|
}
|
|
220
263
|
async fireAdd(added) {
|
|
@@ -271,10 +314,11 @@ export class BCollection {
|
|
|
271
314
|
});
|
|
272
315
|
}
|
|
273
316
|
async delete(predicate) {
|
|
274
|
-
return this.
|
|
317
|
+
return this.runMutation(async () => {
|
|
275
318
|
await this.checkLoaded();
|
|
276
319
|
let ret = 0;
|
|
277
320
|
const trackDeletes = this.deleteHandlers.length > 0;
|
|
321
|
+
const collectDeleted = trackDeletes || this.txChanges !== undefined;
|
|
278
322
|
const indexDeletes = this.indexDefinitions.size > 0;
|
|
279
323
|
const deleted = [];
|
|
280
324
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
@@ -283,10 +327,10 @@ export class BCollection {
|
|
|
283
327
|
if (predicate(value)) {
|
|
284
328
|
ret++;
|
|
285
329
|
if (indexDeletes) {
|
|
286
|
-
|
|
330
|
+
this.removeFromIndexes(value);
|
|
287
331
|
}
|
|
288
332
|
this.mapData.delete(key);
|
|
289
|
-
if (
|
|
333
|
+
if (collectDeleted) {
|
|
290
334
|
deleted.push(value);
|
|
291
335
|
}
|
|
292
336
|
}
|
|
@@ -306,9 +350,9 @@ export class BCollection {
|
|
|
306
350
|
else {
|
|
307
351
|
ret++;
|
|
308
352
|
if (indexDeletes) {
|
|
309
|
-
|
|
353
|
+
this.removeFromIndexes(item);
|
|
310
354
|
}
|
|
311
|
-
if (
|
|
355
|
+
if (collectDeleted) {
|
|
312
356
|
deleted.push(item);
|
|
313
357
|
}
|
|
314
358
|
}
|
|
@@ -317,20 +361,29 @@ export class BCollection {
|
|
|
317
361
|
break;
|
|
318
362
|
default:
|
|
319
363
|
case EBArrayDeletionStrategy.NEW_ARRAY:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
364
|
+
if (collectDeleted || indexDeletes) {
|
|
365
|
+
const kept = [];
|
|
366
|
+
for (const v of this.arrayData) {
|
|
367
|
+
if (predicate(v)) {
|
|
368
|
+
ret++;
|
|
369
|
+
if (indexDeletes) {
|
|
370
|
+
this.removeFromIndexes(v);
|
|
371
|
+
}
|
|
372
|
+
if (collectDeleted) {
|
|
373
|
+
deleted.push(v);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
kept.push(v);
|
|
329
378
|
}
|
|
330
379
|
}
|
|
380
|
+
this.arrayData = kept;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
const ol = this.arrayData.length;
|
|
384
|
+
this.arrayData = this.arrayData.filter(v => !predicate(v));
|
|
385
|
+
ret += (ol - this.arrayData.length);
|
|
331
386
|
}
|
|
332
|
-
this.arrayData = this.arrayData.filter(v => !predicate(v));
|
|
333
|
-
ret += (ol - this.arrayData.length);
|
|
334
387
|
break;
|
|
335
388
|
}
|
|
336
389
|
break;
|
|
@@ -338,7 +391,8 @@ export class BCollection {
|
|
|
338
391
|
if (ret > 0) {
|
|
339
392
|
await this.doOnDirty();
|
|
340
393
|
}
|
|
341
|
-
|
|
394
|
+
await this.recordTxChanges(EBModificationType.DELETE, deleted);
|
|
395
|
+
if (trackDeletes && deleted.length > 0) {
|
|
342
396
|
await this.fireDelete(deleted);
|
|
343
397
|
}
|
|
344
398
|
return ret;
|
|
@@ -349,10 +403,10 @@ export class BCollection {
|
|
|
349
403
|
await this.checkLoaded();
|
|
350
404
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
351
405
|
case EBMemStorageStrategy.MAP:
|
|
352
|
-
return this.mapData.values().
|
|
406
|
+
return this.mapData.values().some(predicate);
|
|
353
407
|
default:
|
|
354
408
|
case EBMemStorageStrategy.ARRAY:
|
|
355
|
-
return this.arrayData.
|
|
409
|
+
return this.arrayData.some(predicate);
|
|
356
410
|
}
|
|
357
411
|
});
|
|
358
412
|
}
|
|
@@ -381,31 +435,46 @@ export class BCollection {
|
|
|
381
435
|
return result;
|
|
382
436
|
}
|
|
383
437
|
async addToIndexes(v) {
|
|
384
|
-
|
|
438
|
+
this.addKeysToIndexes(v, await this.indexKeysFor(v));
|
|
439
|
+
}
|
|
440
|
+
async indexKeysForAll(vs) {
|
|
441
|
+
const ret = [];
|
|
442
|
+
for (const v of vs) {
|
|
443
|
+
ret.push(await this.indexKeysFor(v));
|
|
444
|
+
}
|
|
445
|
+
return ret;
|
|
446
|
+
}
|
|
447
|
+
addKeysToIndexes(v, indexKeys) {
|
|
448
|
+
for (const [indexKey, keys] of indexKeys) {
|
|
385
449
|
const byKey = this.indexRuntime.get(indexKey);
|
|
386
450
|
for (const key of keys) {
|
|
387
451
|
let bucket = byKey.get(key);
|
|
388
452
|
if (!bucket) {
|
|
389
|
-
bucket =
|
|
453
|
+
bucket = new Set();
|
|
390
454
|
byKey.set(key, bucket);
|
|
391
455
|
}
|
|
392
|
-
bucket.
|
|
456
|
+
bucket.add(v);
|
|
393
457
|
}
|
|
394
458
|
}
|
|
459
|
+
if (indexKeys.length > 0) {
|
|
460
|
+
this.indexedKeys.set(v, indexKeys);
|
|
461
|
+
}
|
|
395
462
|
}
|
|
396
|
-
|
|
397
|
-
|
|
463
|
+
removeFromIndexes(v) {
|
|
464
|
+
const indexKeys = this.indexedKeys.get(v);
|
|
465
|
+
if (!indexKeys) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
this.indexedKeys.delete(v);
|
|
469
|
+
for (const [indexKey, keys] of indexKeys) {
|
|
398
470
|
const byKey = this.indexRuntime.get(indexKey);
|
|
399
471
|
for (const key of keys) {
|
|
400
472
|
const bucket = byKey.get(key);
|
|
401
473
|
if (!bucket) {
|
|
402
474
|
continue;
|
|
403
475
|
}
|
|
404
|
-
|
|
405
|
-
if (
|
|
406
|
-
bucket.splice(at, 1);
|
|
407
|
-
}
|
|
408
|
-
if (bucket.length === 0) {
|
|
476
|
+
bucket.delete(v);
|
|
477
|
+
if (bucket.size === 0) {
|
|
409
478
|
byKey.delete(key);
|
|
410
479
|
}
|
|
411
480
|
}
|
|
@@ -419,6 +488,7 @@ export class BCollection {
|
|
|
419
488
|
// every configured index gets an entry up front, even if empty, so getByIndex()/
|
|
420
489
|
// addToIndexes()/removeFromIndexes() can assume it's there instead of checking twice
|
|
421
490
|
this.indexRuntime = new Map(Array.from(this.indexDefinitions.keys(), (indexKey) => [indexKey, new Map()]));
|
|
491
|
+
this.indexedKeys = new Map();
|
|
422
492
|
if (this.indexDefinitions.size === 0) {
|
|
423
493
|
return;
|
|
424
494
|
}
|
|
@@ -441,18 +511,18 @@ export class BCollection {
|
|
|
441
511
|
/**
|
|
442
512
|
* id -> value for every stored item, resolved via the plain id getter (no auto-id assignment - every
|
|
443
513
|
* stored item already has one). Used to diff a transaction's working copy against the pre-transaction
|
|
444
|
-
* state in {@link
|
|
514
|
+
* state in {@link prepareTransactionCommit}.
|
|
445
515
|
*/
|
|
446
516
|
async idKeyedSnapshot() {
|
|
447
517
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
448
518
|
case EBMemStorageStrategy.MAP:
|
|
449
|
-
return
|
|
519
|
+
return this.mapData;
|
|
450
520
|
default:
|
|
451
521
|
case EBMemStorageStrategy.ARRAY:
|
|
452
522
|
const getter = this.resolveIDGetter();
|
|
453
523
|
const ret = new Map();
|
|
454
524
|
for (const v of this.arrayData) {
|
|
455
|
-
const id = await getter(v);
|
|
525
|
+
const id = normalizeID(await getter(v));
|
|
456
526
|
if (id) {
|
|
457
527
|
ret.set(id, v);
|
|
458
528
|
}
|
|
@@ -460,43 +530,90 @@ export class BCollection {
|
|
|
460
530
|
return ret;
|
|
461
531
|
}
|
|
462
532
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
533
|
+
resolveIDSetter() {
|
|
534
|
+
if (this.collectionConfig.setID) {
|
|
535
|
+
const found = this.database.functionRegistry?.fetch(this.collectionConfig.setID);
|
|
536
|
+
if (!found) {
|
|
537
|
+
throw new Error(`setID function "${this.collectionConfig.setID}" is not registered`);
|
|
538
|
+
}
|
|
539
|
+
return found;
|
|
540
|
+
}
|
|
541
|
+
return DEFAULT_SET_ID;
|
|
542
|
+
}
|
|
543
|
+
async resolveIncomingIDs(vs) {
|
|
544
|
+
const getter = this.resolveIDGetter();
|
|
545
|
+
const ids = [];
|
|
546
|
+
for (const v of vs) {
|
|
547
|
+
const id = normalizeID(await getter(v));
|
|
548
|
+
if (!id && this.collectionConfig.autoIDStrategy === EBAutoIDStrategy.NO_AUTO_ID) {
|
|
549
|
+
throw new Error("cannot omit auto-ids and than trying to add a value without an id");
|
|
550
|
+
}
|
|
551
|
+
ids.push(id);
|
|
552
|
+
}
|
|
553
|
+
return ids;
|
|
554
|
+
}
|
|
555
|
+
async assignMissingIDs(vs, ids) {
|
|
556
|
+
let setter;
|
|
557
|
+
const ret = [];
|
|
558
|
+
for (let i = 0; i < vs.length; i++) {
|
|
559
|
+
let id = ids[i];
|
|
560
|
+
if (!id) {
|
|
561
|
+
setter ??= this.resolveIDSetter();
|
|
562
|
+
id = uuidv7();
|
|
563
|
+
await setter(vs[i], id);
|
|
564
|
+
}
|
|
565
|
+
ret.push(id);
|
|
566
|
+
}
|
|
567
|
+
return ret;
|
|
568
|
+
}
|
|
569
|
+
async storedArrayIDs() {
|
|
570
|
+
const getter = this.resolveIDGetter();
|
|
571
|
+
const ids = [];
|
|
572
|
+
for (const v of this.arrayData) {
|
|
573
|
+
ids.push(normalizeID(await getter(v)));
|
|
574
|
+
}
|
|
575
|
+
return ids;
|
|
576
|
+
}
|
|
577
|
+
async recordTxChanges(modificationType, values) {
|
|
578
|
+
if (!this.txChanges || values.length === 0) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const getter = this.resolveIDGetter();
|
|
582
|
+
for (const v of values) {
|
|
583
|
+
const id = normalizeID(await getter(v));
|
|
584
|
+
if (id) {
|
|
585
|
+
this.txChanges.set(id, modificationType);
|
|
485
586
|
}
|
|
486
587
|
}
|
|
487
|
-
return id;
|
|
488
588
|
}
|
|
489
589
|
async add(...vs) {
|
|
490
|
-
return this.
|
|
590
|
+
return this.runMutation(async () => {
|
|
491
591
|
await this.checkLoaded();
|
|
592
|
+
const trackAdds = this.addHandlers.length > 0;
|
|
593
|
+
const collectAdded = trackAdds || this.txChanges !== undefined;
|
|
594
|
+
const indexAdds = this.indexDefinitions.size > 0;
|
|
595
|
+
const incomingIDs = await this.resolveIncomingIDs(vs);
|
|
596
|
+
const added = [];
|
|
597
|
+
let ret = 0;
|
|
492
598
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
493
|
-
case EBMemStorageStrategy.MAP:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
599
|
+
case EBMemStorageStrategy.MAP: {
|
|
600
|
+
if (this.collectionConfig.duplicateIDStrategy === EBDuplicateIDStrategy.ERROR) {
|
|
601
|
+
const batchIDs = new Set();
|
|
602
|
+
for (const id of incomingIDs) {
|
|
603
|
+
if (id === undefined) {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (this.mapData.has(id) || batchIDs.has(id)) {
|
|
607
|
+
throw new Error(`duplicate id ${id}`);
|
|
608
|
+
}
|
|
609
|
+
batchIDs.add(id);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const ids = await this.assignMissingIDs(vs, incomingIDs);
|
|
613
|
+
const indexKeys = indexAdds ? await this.indexKeysForAll(vs) : [];
|
|
614
|
+
for (let i = 0; i < vs.length; i++) {
|
|
615
|
+
const id = ids[i];
|
|
616
|
+
const v = vs[i];
|
|
500
617
|
if (this.mapData.has(id)) {
|
|
501
618
|
switch (this.collectionConfig.duplicateIDStrategy) {
|
|
502
619
|
case EBDuplicateIDStrategy.ERROR:
|
|
@@ -508,15 +625,15 @@ export class BCollection {
|
|
|
508
625
|
case EBDuplicateIDStrategy.OVERWRITE:
|
|
509
626
|
// replace the new value
|
|
510
627
|
ret++;
|
|
511
|
-
if (
|
|
512
|
-
|
|
628
|
+
if (indexAdds) {
|
|
629
|
+
this.removeFromIndexes(this.mapData.get(id));
|
|
513
630
|
}
|
|
514
631
|
this.mapData.set(id, v);
|
|
515
|
-
if (
|
|
516
|
-
|
|
632
|
+
if (indexAdds) {
|
|
633
|
+
this.addKeysToIndexes(v, indexKeys[i]);
|
|
517
634
|
}
|
|
518
|
-
if (
|
|
519
|
-
|
|
635
|
+
if (collectAdded) {
|
|
636
|
+
added.push(v);
|
|
520
637
|
}
|
|
521
638
|
break;
|
|
522
639
|
}
|
|
@@ -524,169 +641,164 @@ export class BCollection {
|
|
|
524
641
|
else {
|
|
525
642
|
ret++;
|
|
526
643
|
this.mapData.set(id, v);
|
|
527
|
-
if (
|
|
528
|
-
|
|
644
|
+
if (indexAdds) {
|
|
645
|
+
this.addKeysToIndexes(v, indexKeys[i]);
|
|
529
646
|
}
|
|
530
|
-
if (
|
|
531
|
-
|
|
647
|
+
if (collectAdded) {
|
|
648
|
+
added.push(v);
|
|
532
649
|
}
|
|
533
650
|
}
|
|
534
651
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
if (addedMap.length > 0) {
|
|
539
|
-
await this.fireAdd(addedMap);
|
|
540
|
-
}
|
|
541
|
-
return ret;
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
542
654
|
default:
|
|
543
|
-
case EBMemStorageStrategy.ARRAY:
|
|
544
|
-
const
|
|
545
|
-
const indexAddsArray = this.indexDefinitions.size > 0;
|
|
546
|
-
const addedArray = [];
|
|
655
|
+
case EBMemStorageStrategy.ARRAY: {
|
|
656
|
+
const storedIDs = await this.storedArrayIDs();
|
|
547
657
|
switch (this.collectionConfig.duplicateIDStrategy) {
|
|
548
|
-
case EBDuplicateIDStrategy.ERROR:
|
|
549
|
-
const haveIDs = new Set();
|
|
550
|
-
for (const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const id = await this.getID(v);
|
|
658
|
+
case EBDuplicateIDStrategy.ERROR: {
|
|
659
|
+
const haveIDs = new Set(storedIDs.filter((id) => id !== undefined));
|
|
660
|
+
for (const id of incomingIDs) {
|
|
661
|
+
if (id === undefined) {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
555
664
|
if (haveIDs.has(id)) {
|
|
556
665
|
throw new Error(`duplicate id ${id}`);
|
|
557
666
|
}
|
|
558
667
|
haveIDs.add(id);
|
|
668
|
+
}
|
|
669
|
+
await this.assignMissingIDs(vs, incomingIDs);
|
|
670
|
+
const indexKeys = indexAdds ? await this.indexKeysForAll(vs) : [];
|
|
671
|
+
vs.forEach((v, i) => {
|
|
672
|
+
ret++;
|
|
559
673
|
this.arrayData.push(v);
|
|
560
|
-
if (
|
|
561
|
-
|
|
674
|
+
if (indexAdds) {
|
|
675
|
+
this.addKeysToIndexes(v, indexKeys[i]);
|
|
562
676
|
}
|
|
563
|
-
if (
|
|
564
|
-
|
|
677
|
+
if (collectAdded) {
|
|
678
|
+
added.push(v);
|
|
565
679
|
}
|
|
566
|
-
}
|
|
680
|
+
});
|
|
567
681
|
break;
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const id = await this.getID(v);
|
|
682
|
+
}
|
|
683
|
+
case EBDuplicateIDStrategy.IGNORE: {
|
|
684
|
+
const ids = await this.assignMissingIDs(vs, incomingIDs);
|
|
685
|
+
const seenIDs = new Set(storedIDs.filter((id) => id !== undefined));
|
|
686
|
+
const toAdd = [];
|
|
687
|
+
ids.forEach((id, i) => {
|
|
575
688
|
if (!seenIDs.has(id)) {
|
|
576
689
|
seenIDs.add(id);
|
|
577
|
-
|
|
578
|
-
if (indexAddsArray) {
|
|
579
|
-
await this.addToIndexes(v);
|
|
580
|
-
}
|
|
581
|
-
if (trackAddsArray) {
|
|
582
|
-
addedArray.push(v);
|
|
583
|
-
}
|
|
690
|
+
toAdd.push(vs[i]);
|
|
584
691
|
}
|
|
585
|
-
}
|
|
692
|
+
});
|
|
693
|
+
const indexKeys = indexAdds ? await this.indexKeysForAll(toAdd) : [];
|
|
694
|
+
toAdd.forEach((v, i) => {
|
|
695
|
+
ret++;
|
|
696
|
+
this.arrayData.push(v);
|
|
697
|
+
if (indexAdds) {
|
|
698
|
+
this.addKeysToIndexes(v, indexKeys[i]);
|
|
699
|
+
}
|
|
700
|
+
if (collectAdded) {
|
|
701
|
+
added.push(v);
|
|
702
|
+
}
|
|
703
|
+
});
|
|
586
704
|
break;
|
|
705
|
+
}
|
|
587
706
|
default:
|
|
588
|
-
case EBDuplicateIDStrategy.OVERWRITE:
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
707
|
+
case EBDuplicateIDStrategy.OVERWRITE: {
|
|
708
|
+
const ids = await this.assignMissingIDs(vs, incomingIDs);
|
|
709
|
+
const indexKeys = indexAdds ? await this.indexKeysForAll(vs) : [];
|
|
710
|
+
const positions = new Map();
|
|
711
|
+
storedIDs.forEach((id, i) => {
|
|
712
|
+
if (id !== undefined) {
|
|
713
|
+
positions.set(id, i);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
vs.forEach((v, i) => {
|
|
717
|
+
const id = ids[i];
|
|
718
|
+
const existingIndex = positions.get(id);
|
|
719
|
+
ret++;
|
|
596
720
|
if (existingIndex !== undefined) {
|
|
597
|
-
if (
|
|
598
|
-
|
|
721
|
+
if (indexAdds) {
|
|
722
|
+
this.removeFromIndexes(this.arrayData[existingIndex]);
|
|
599
723
|
}
|
|
600
724
|
this.arrayData[existingIndex] = v;
|
|
601
725
|
}
|
|
602
726
|
else {
|
|
603
|
-
|
|
604
|
-
indexMap.set(id, this.arrayData.length);
|
|
727
|
+
positions.set(id, this.arrayData.length);
|
|
605
728
|
this.arrayData.push(v);
|
|
606
729
|
}
|
|
607
|
-
if (
|
|
608
|
-
|
|
730
|
+
if (indexAdds) {
|
|
731
|
+
this.addKeysToIndexes(v, indexKeys[i]);
|
|
609
732
|
}
|
|
610
|
-
if (
|
|
611
|
-
|
|
733
|
+
if (collectAdded) {
|
|
734
|
+
added.push(v);
|
|
612
735
|
}
|
|
613
|
-
}
|
|
736
|
+
});
|
|
614
737
|
break;
|
|
738
|
+
}
|
|
615
739
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
}
|
|
622
|
-
return vs.length;
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (ret > 0) {
|
|
744
|
+
await this.doOnDirty();
|
|
623
745
|
}
|
|
746
|
+
await this.recordTxChanges(EBModificationType.ADD, added);
|
|
747
|
+
if (trackAdds && added.length > 0) {
|
|
748
|
+
await this.fireAdd(added);
|
|
749
|
+
}
|
|
750
|
+
return ret;
|
|
624
751
|
});
|
|
625
752
|
}
|
|
626
753
|
async update(predicate, updater) {
|
|
627
|
-
return this.
|
|
754
|
+
return this.runMutation(async () => {
|
|
628
755
|
await this.checkLoaded();
|
|
629
|
-
let ret = 0;
|
|
630
756
|
const trackUpdates = this.updateHandlers.length > 0;
|
|
631
757
|
const indexUpdates = this.indexDefinitions.size > 0;
|
|
632
|
-
const
|
|
758
|
+
const planned = [];
|
|
759
|
+
const plan = async (previous, store) => {
|
|
760
|
+
const replacement = await updater(previous);
|
|
761
|
+
if (replacement === undefined) {
|
|
762
|
+
throw new Error("updater gave undefined");
|
|
763
|
+
}
|
|
764
|
+
const replacementKeys = indexUpdates ? await this.indexKeysFor(replacement) : [];
|
|
765
|
+
planned.push({ previous, replacement, replacementKeys, store });
|
|
766
|
+
};
|
|
633
767
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
634
768
|
case EBMemStorageStrategy.MAP:
|
|
635
769
|
for (const [key, value] of this.mapData) {
|
|
636
770
|
if (predicate(value)) {
|
|
637
|
-
|
|
638
|
-
if (replacement !== undefined) {
|
|
639
|
-
ret++;
|
|
640
|
-
if (indexUpdates) {
|
|
641
|
-
await this.removeFromIndexes(value);
|
|
642
|
-
}
|
|
643
|
-
this.mapData.set(key, replacement);
|
|
644
|
-
if (indexUpdates) {
|
|
645
|
-
await this.addToIndexes(replacement);
|
|
646
|
-
}
|
|
647
|
-
if (trackUpdates) {
|
|
648
|
-
updated.push([value, replacement]);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
else {
|
|
652
|
-
throw new Error("updater gave undefined");
|
|
653
|
-
}
|
|
771
|
+
await plan(value, (replacement) => { this.mapData.set(key, replacement); });
|
|
654
772
|
}
|
|
655
773
|
}
|
|
656
774
|
break;
|
|
657
775
|
default:
|
|
658
776
|
case EBMemStorageStrategy.ARRAY:
|
|
659
777
|
for (let i = 0; i < this.arrayData.length; i++) {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if (replacement !== undefined) {
|
|
664
|
-
ret++;
|
|
665
|
-
if (indexUpdates) {
|
|
666
|
-
await this.removeFromIndexes(previous);
|
|
667
|
-
}
|
|
668
|
-
this.arrayData[i] = replacement;
|
|
669
|
-
if (indexUpdates) {
|
|
670
|
-
await this.addToIndexes(replacement);
|
|
671
|
-
}
|
|
672
|
-
if (trackUpdates) {
|
|
673
|
-
updated.push([previous, replacement]);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
else {
|
|
677
|
-
throw new Error("updater gave undefined");
|
|
678
|
-
}
|
|
778
|
+
const previous = this.arrayData[i];
|
|
779
|
+
if (predicate(previous)) {
|
|
780
|
+
await plan(previous, (replacement) => { this.arrayData[i] = replacement; });
|
|
679
781
|
}
|
|
680
782
|
}
|
|
681
783
|
break;
|
|
682
784
|
}
|
|
683
|
-
|
|
785
|
+
for (const { previous, replacement, replacementKeys, store } of planned) {
|
|
786
|
+
if (indexUpdates) {
|
|
787
|
+
this.removeFromIndexes(previous);
|
|
788
|
+
}
|
|
789
|
+
store(replacement);
|
|
790
|
+
if (indexUpdates) {
|
|
791
|
+
this.addKeysToIndexes(replacement, replacementKeys);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (planned.length > 0) {
|
|
684
795
|
await this.doOnDirty();
|
|
685
796
|
}
|
|
686
|
-
|
|
687
|
-
|
|
797
|
+
await this.recordTxChanges(EBModificationType.UPDATE, planned.map(({ replacement }) => replacement));
|
|
798
|
+
if (trackUpdates && planned.length > 0) {
|
|
799
|
+
await this.fireUpdate(planned.map(({ previous, replacement }) => [previous, replacement]));
|
|
688
800
|
}
|
|
689
|
-
return
|
|
801
|
+
return planned.length;
|
|
690
802
|
});
|
|
691
803
|
}
|
|
692
804
|
async doOnDirty() {
|
|
@@ -722,10 +834,10 @@ export class BCollection {
|
|
|
722
834
|
this.arrayData = await this.database.io.loadArrayData(this.collectionID);
|
|
723
835
|
break;
|
|
724
836
|
}
|
|
725
|
-
this.loaded = true;
|
|
726
|
-
this.dirty = false;
|
|
727
837
|
this.resolveIndexDefinitions();
|
|
728
838
|
await this.rebuildIndexRuntime();
|
|
839
|
+
this.loaded = true;
|
|
840
|
+
this.dirty = false;
|
|
729
841
|
});
|
|
730
842
|
}
|
|
731
843
|
async unload() {
|
|
@@ -752,6 +864,8 @@ export class BCollection {
|
|
|
752
864
|
this.arrayData = [];
|
|
753
865
|
break;
|
|
754
866
|
}
|
|
867
|
+
this.indexRuntime.clear();
|
|
868
|
+
this.indexedKeys.clear();
|
|
755
869
|
this.loaded = false;
|
|
756
870
|
this.dirty = false;
|
|
757
871
|
});
|
|
@@ -770,62 +884,75 @@ export class BCollection {
|
|
|
770
884
|
clone.mapData = new Map(this.mapData);
|
|
771
885
|
clone.arrayData = this.arrayData.slice();
|
|
772
886
|
clone.loaded = true;
|
|
887
|
+
clone.transactionClone = true;
|
|
888
|
+
if (this.addHandlers.length > 0 || this.updateHandlers.length > 0 || this.deleteHandlers.length > 0) {
|
|
889
|
+
clone.txChanges = new Map();
|
|
890
|
+
}
|
|
773
891
|
clone.resolveIndexDefinitions();
|
|
774
892
|
await clone.rebuildIndexRuntime();
|
|
775
893
|
return clone;
|
|
776
894
|
});
|
|
777
895
|
}
|
|
778
896
|
/**
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
* least one handler is registered on this collection - a transaction on a collection nobody listens to
|
|
784
|
-
* skips it entirely. Call only while already holding this collection's mutex for the whole transaction.
|
|
897
|
+
* Writes `clone`'s state to a temp file (see {@link IPreparedWrite}) and computes the handler events for the
|
|
898
|
+
* net changes against this collection's current state: one per id the transaction touched, typed by the last
|
|
899
|
+
* operation on it (`DELETE` if gone, `ADD` if new or last stored via `add()`, `UPDATE` otherwise). Nothing is
|
|
900
|
+
* applied until `apply()`; call only while already holding this collection's mutex for the whole transaction.
|
|
785
901
|
*/
|
|
786
|
-
async
|
|
902
|
+
async prepareTransactionCommit(clone) {
|
|
787
903
|
return this.runProtected(async () => {
|
|
788
904
|
await this.checkLoaded();
|
|
789
905
|
const added = [];
|
|
790
906
|
const updated = [];
|
|
791
907
|
const deleted = [];
|
|
792
|
-
|
|
793
|
-
// entirely (incl. idKeyedSnapshot()'s id-getter calls) when nothing is listening
|
|
794
|
-
const trackChanges = this.addHandlers.length > 0 || this.updateHandlers.length > 0 || this.deleteHandlers.length > 0;
|
|
795
|
-
if (trackChanges) {
|
|
908
|
+
if (clone.txChanges && clone.txChanges.size > 0) {
|
|
796
909
|
const before = await this.idKeyedSnapshot();
|
|
797
910
|
const after = await clone.idKeyedSnapshot();
|
|
798
|
-
for (const [id,
|
|
799
|
-
if (!
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
else {
|
|
803
|
-
const previous = before.get(id);
|
|
804
|
-
if (JSON.stringify(previous) !== JSON.stringify(value)) {
|
|
805
|
-
updated.push([previous, value]);
|
|
911
|
+
for (const [id, modificationType] of clone.txChanges) {
|
|
912
|
+
if (!after.has(id)) {
|
|
913
|
+
if (before.has(id)) {
|
|
914
|
+
deleted.push(before.get(id));
|
|
806
915
|
}
|
|
807
916
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
917
|
+
else if (!before.has(id) || modificationType === EBModificationType.ADD) {
|
|
918
|
+
added.push(after.get(id));
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
updated.push([before.get(id), after.get(id)]);
|
|
812
922
|
}
|
|
813
923
|
}
|
|
814
924
|
}
|
|
815
|
-
|
|
816
|
-
this.
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
await this.fireUpdate(updated);
|
|
825
|
-
}
|
|
826
|
-
if (deleted.length > 0) {
|
|
827
|
-
await this.fireDelete(deleted);
|
|
925
|
+
let write;
|
|
926
|
+
switch (this.collectionConfig.memStorageStrategy) {
|
|
927
|
+
case EBMemStorageStrategy.MAP:
|
|
928
|
+
write = await this.database.io.prepareMapData(this.collectionID, clone.mapData);
|
|
929
|
+
break;
|
|
930
|
+
default:
|
|
931
|
+
case EBMemStorageStrategy.ARRAY:
|
|
932
|
+
write = await this.database.io.prepareArrayData(this.collectionID, clone.arrayData);
|
|
933
|
+
break;
|
|
828
934
|
}
|
|
935
|
+
return {
|
|
936
|
+
write,
|
|
937
|
+
apply: () => {
|
|
938
|
+
this.mapData = clone.mapData;
|
|
939
|
+
this.arrayData = clone.arrayData;
|
|
940
|
+
this.indexRuntime = clone.indexRuntime;
|
|
941
|
+
this.indexedKeys = clone.indexedKeys;
|
|
942
|
+
this.dirty = false;
|
|
943
|
+
},
|
|
944
|
+
fire: () => this.runProtected(async () => {
|
|
945
|
+
if (added.length > 0) {
|
|
946
|
+
await this.fireAdd(added);
|
|
947
|
+
}
|
|
948
|
+
if (updated.length > 0) {
|
|
949
|
+
await this.fireUpdate(updated);
|
|
950
|
+
}
|
|
951
|
+
if (deleted.length > 0) {
|
|
952
|
+
await this.fireDelete(deleted);
|
|
953
|
+
}
|
|
954
|
+
}),
|
|
955
|
+
};
|
|
829
956
|
});
|
|
830
957
|
}
|
|
831
958
|
}
|