@incoqnito.io/bajadab 1.2.1 → 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 -153
- package/dist/base.d.ts +50 -3
- package/dist/base.js.map +1 -1
- package/dist/basics.d.ts +44 -3
- package/dist/basics.js +456 -211
- 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 -186
- package/src/main/basics.ts +1058 -787
- 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,14 +87,73 @@ 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
|
}
|
|
88
94
|
return this.io.snapshot(targetDir, options);
|
|
89
95
|
});
|
|
90
96
|
}
|
|
97
|
+
async transaction(collectionNames, fn) {
|
|
98
|
+
if (this.transactionScope.getStore() !== undefined) {
|
|
99
|
+
throw new Error("nested transactions are not supported");
|
|
100
|
+
}
|
|
101
|
+
const canonicalNames = Array.from(new Set(collectionNames.map(canonicalName)));
|
|
102
|
+
return this.runExclusive(async () => {
|
|
103
|
+
const originals = new Map();
|
|
104
|
+
const clones = new Map();
|
|
105
|
+
for (const name of canonicalNames) {
|
|
106
|
+
const original = this.collections.get(name);
|
|
107
|
+
if (!original) {
|
|
108
|
+
throw new Error(`no such collection ${name}`);
|
|
109
|
+
}
|
|
110
|
+
originals.set(name, original);
|
|
111
|
+
}
|
|
112
|
+
for (const [name, original] of originals) {
|
|
113
|
+
clones.set(name, await original.cloneForTransaction());
|
|
114
|
+
}
|
|
115
|
+
const tx = {
|
|
116
|
+
getCollection: async (collectionName) => {
|
|
117
|
+
const clone = clones.get(canonicalName(collectionName));
|
|
118
|
+
if (!clone) {
|
|
119
|
+
throw new Error(`collection "${collectionName}" was not declared to this transaction`);
|
|
120
|
+
}
|
|
121
|
+
return clone;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
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());
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
91
157
|
}
|
|
92
158
|
export class BCollection {
|
|
93
159
|
database;
|
|
@@ -99,26 +165,19 @@ export class BCollection {
|
|
|
99
165
|
loaded = false;
|
|
100
166
|
dirty = false;
|
|
101
167
|
dropped = false;
|
|
168
|
+
transactionClone = false;
|
|
102
169
|
addHandlers = [];
|
|
103
170
|
updateHandlers = [];
|
|
104
171
|
deleteHandlers = [];
|
|
105
172
|
indexDefinitions = new Map();
|
|
106
173
|
indexRuntime = new Map();
|
|
174
|
+
indexedKeys = new Map();
|
|
175
|
+
txChanges;
|
|
107
176
|
constructor(database, collectionID, collectionName, collectionConfig) {
|
|
108
177
|
this.database = database;
|
|
109
178
|
this.collectionID = collectionID;
|
|
110
179
|
this.collectionName = canonicalName(collectionName);
|
|
111
180
|
this.collectionConfig = collectionConfig;
|
|
112
|
-
switch (collectionConfig.memStorageStrategy) {
|
|
113
|
-
case EBMemStorageStrategy.MAP:
|
|
114
|
-
// if (!collectionConfig.getID || !collectionConfig.setID) {
|
|
115
|
-
// throw new Error("need id accessors configured when using map mem storage");
|
|
116
|
-
// }
|
|
117
|
-
break;
|
|
118
|
-
default:
|
|
119
|
-
case EBMemStorageStrategy.ARRAY:
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
181
|
}
|
|
123
182
|
async checkLoaded() {
|
|
124
183
|
if (!this.loaded) {
|
|
@@ -126,7 +185,7 @@ export class BCollection {
|
|
|
126
185
|
}
|
|
127
186
|
}
|
|
128
187
|
async runExclusive(fn) {
|
|
129
|
-
return
|
|
188
|
+
return this.database.runExclusive(fn);
|
|
130
189
|
}
|
|
131
190
|
async runProtected(fn) {
|
|
132
191
|
return this.runExclusive(async () => {
|
|
@@ -136,6 +195,14 @@ export class BCollection {
|
|
|
136
195
|
return fn();
|
|
137
196
|
});
|
|
138
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
|
+
}
|
|
139
206
|
/**
|
|
140
207
|
* call this only within the protecting mutex
|
|
141
208
|
*/
|
|
@@ -181,7 +248,7 @@ export class BCollection {
|
|
|
181
248
|
if (!byKey) {
|
|
182
249
|
throw new Error(`index "${indexKey}" is not configured`);
|
|
183
250
|
}
|
|
184
|
-
return byKey.get(key)
|
|
251
|
+
return Array.from(byKey.get(key) ?? []);
|
|
185
252
|
});
|
|
186
253
|
}
|
|
187
254
|
async invokeHandler(fn, modificationType) {
|
|
@@ -189,7 +256,8 @@ export class BCollection {
|
|
|
189
256
|
await fn();
|
|
190
257
|
}
|
|
191
258
|
catch (error) {
|
|
192
|
-
|
|
259
|
+
// TODO switch logging to ILogging where necessary
|
|
260
|
+
console.error(`bajadab: collection "${this.collectionName}" ${modificationType} handler failed`, error);
|
|
193
261
|
}
|
|
194
262
|
}
|
|
195
263
|
async fireAdd(added) {
|
|
@@ -246,10 +314,11 @@ export class BCollection {
|
|
|
246
314
|
});
|
|
247
315
|
}
|
|
248
316
|
async delete(predicate) {
|
|
249
|
-
return this.
|
|
317
|
+
return this.runMutation(async () => {
|
|
250
318
|
await this.checkLoaded();
|
|
251
319
|
let ret = 0;
|
|
252
320
|
const trackDeletes = this.deleteHandlers.length > 0;
|
|
321
|
+
const collectDeleted = trackDeletes || this.txChanges !== undefined;
|
|
253
322
|
const indexDeletes = this.indexDefinitions.size > 0;
|
|
254
323
|
const deleted = [];
|
|
255
324
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
@@ -258,10 +327,10 @@ export class BCollection {
|
|
|
258
327
|
if (predicate(value)) {
|
|
259
328
|
ret++;
|
|
260
329
|
if (indexDeletes) {
|
|
261
|
-
|
|
330
|
+
this.removeFromIndexes(value);
|
|
262
331
|
}
|
|
263
332
|
this.mapData.delete(key);
|
|
264
|
-
if (
|
|
333
|
+
if (collectDeleted) {
|
|
265
334
|
deleted.push(value);
|
|
266
335
|
}
|
|
267
336
|
}
|
|
@@ -281,9 +350,9 @@ export class BCollection {
|
|
|
281
350
|
else {
|
|
282
351
|
ret++;
|
|
283
352
|
if (indexDeletes) {
|
|
284
|
-
|
|
353
|
+
this.removeFromIndexes(item);
|
|
285
354
|
}
|
|
286
|
-
if (
|
|
355
|
+
if (collectDeleted) {
|
|
287
356
|
deleted.push(item);
|
|
288
357
|
}
|
|
289
358
|
}
|
|
@@ -292,20 +361,29 @@ export class BCollection {
|
|
|
292
361
|
break;
|
|
293
362
|
default:
|
|
294
363
|
case EBArrayDeletionStrategy.NEW_ARRAY:
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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);
|
|
304
378
|
}
|
|
305
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);
|
|
306
386
|
}
|
|
307
|
-
this.arrayData = this.arrayData.filter(v => !predicate(v));
|
|
308
|
-
ret += (ol - this.arrayData.length);
|
|
309
387
|
break;
|
|
310
388
|
}
|
|
311
389
|
break;
|
|
@@ -313,7 +391,8 @@ export class BCollection {
|
|
|
313
391
|
if (ret > 0) {
|
|
314
392
|
await this.doOnDirty();
|
|
315
393
|
}
|
|
316
|
-
|
|
394
|
+
await this.recordTxChanges(EBModificationType.DELETE, deleted);
|
|
395
|
+
if (trackDeletes && deleted.length > 0) {
|
|
317
396
|
await this.fireDelete(deleted);
|
|
318
397
|
}
|
|
319
398
|
return ret;
|
|
@@ -324,10 +403,10 @@ export class BCollection {
|
|
|
324
403
|
await this.checkLoaded();
|
|
325
404
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
326
405
|
case EBMemStorageStrategy.MAP:
|
|
327
|
-
return this.mapData.values().
|
|
406
|
+
return this.mapData.values().some(predicate);
|
|
328
407
|
default:
|
|
329
408
|
case EBMemStorageStrategy.ARRAY:
|
|
330
|
-
return this.arrayData.
|
|
409
|
+
return this.arrayData.some(predicate);
|
|
331
410
|
}
|
|
332
411
|
});
|
|
333
412
|
}
|
|
@@ -356,31 +435,46 @@ export class BCollection {
|
|
|
356
435
|
return result;
|
|
357
436
|
}
|
|
358
437
|
async addToIndexes(v) {
|
|
359
|
-
|
|
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) {
|
|
360
449
|
const byKey = this.indexRuntime.get(indexKey);
|
|
361
450
|
for (const key of keys) {
|
|
362
451
|
let bucket = byKey.get(key);
|
|
363
452
|
if (!bucket) {
|
|
364
|
-
bucket =
|
|
453
|
+
bucket = new Set();
|
|
365
454
|
byKey.set(key, bucket);
|
|
366
455
|
}
|
|
367
|
-
bucket.
|
|
456
|
+
bucket.add(v);
|
|
368
457
|
}
|
|
369
458
|
}
|
|
459
|
+
if (indexKeys.length > 0) {
|
|
460
|
+
this.indexedKeys.set(v, indexKeys);
|
|
461
|
+
}
|
|
370
462
|
}
|
|
371
|
-
|
|
372
|
-
|
|
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) {
|
|
373
470
|
const byKey = this.indexRuntime.get(indexKey);
|
|
374
471
|
for (const key of keys) {
|
|
375
472
|
const bucket = byKey.get(key);
|
|
376
473
|
if (!bucket) {
|
|
377
474
|
continue;
|
|
378
475
|
}
|
|
379
|
-
|
|
380
|
-
if (
|
|
381
|
-
bucket.splice(at, 1);
|
|
382
|
-
}
|
|
383
|
-
if (bucket.length === 0) {
|
|
476
|
+
bucket.delete(v);
|
|
477
|
+
if (bucket.size === 0) {
|
|
384
478
|
byKey.delete(key);
|
|
385
479
|
}
|
|
386
480
|
}
|
|
@@ -394,6 +488,7 @@ export class BCollection {
|
|
|
394
488
|
// every configured index gets an entry up front, even if empty, so getByIndex()/
|
|
395
489
|
// addToIndexes()/removeFromIndexes() can assume it's there instead of checking twice
|
|
396
490
|
this.indexRuntime = new Map(Array.from(this.indexDefinitions.keys(), (indexKey) => [indexKey, new Map()]));
|
|
491
|
+
this.indexedKeys = new Map();
|
|
397
492
|
if (this.indexDefinitions.size === 0) {
|
|
398
493
|
return;
|
|
399
494
|
}
|
|
@@ -403,54 +498,122 @@ export class BCollection {
|
|
|
403
498
|
await this.addToIndexes(v);
|
|
404
499
|
}
|
|
405
500
|
}
|
|
406
|
-
|
|
407
|
-
let getter;
|
|
501
|
+
resolveIDGetter() {
|
|
408
502
|
if (this.collectionConfig.getID) {
|
|
409
503
|
const found = this.database.functionRegistry?.fetch(this.collectionConfig.getID);
|
|
410
504
|
if (!found) {
|
|
411
505
|
throw new Error(`getID function "${this.collectionConfig.getID}" is not registered`);
|
|
412
506
|
}
|
|
413
|
-
|
|
507
|
+
return found;
|
|
414
508
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
else {
|
|
435
|
-
setter = DEFAULT_SET_ID;
|
|
509
|
+
return DEFAULT_GET_ID;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* id -> value for every stored item, resolved via the plain id getter (no auto-id assignment - every
|
|
513
|
+
* stored item already has one). Used to diff a transaction's working copy against the pre-transaction
|
|
514
|
+
* state in {@link prepareTransactionCommit}.
|
|
515
|
+
*/
|
|
516
|
+
async idKeyedSnapshot() {
|
|
517
|
+
switch (this.collectionConfig.memStorageStrategy) {
|
|
518
|
+
case EBMemStorageStrategy.MAP:
|
|
519
|
+
return this.mapData;
|
|
520
|
+
default:
|
|
521
|
+
case EBMemStorageStrategy.ARRAY:
|
|
522
|
+
const getter = this.resolveIDGetter();
|
|
523
|
+
const ret = new Map();
|
|
524
|
+
for (const v of this.arrayData) {
|
|
525
|
+
const id = normalizeID(await getter(v));
|
|
526
|
+
if (id) {
|
|
527
|
+
ret.set(id, v);
|
|
436
528
|
}
|
|
437
|
-
|
|
438
|
-
|
|
529
|
+
}
|
|
530
|
+
return ret;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
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);
|
|
439
586
|
}
|
|
440
587
|
}
|
|
441
|
-
return id;
|
|
442
588
|
}
|
|
443
589
|
async add(...vs) {
|
|
444
|
-
return this.
|
|
590
|
+
return this.runMutation(async () => {
|
|
445
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;
|
|
446
598
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
447
|
-
case EBMemStorageStrategy.MAP:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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];
|
|
454
617
|
if (this.mapData.has(id)) {
|
|
455
618
|
switch (this.collectionConfig.duplicateIDStrategy) {
|
|
456
619
|
case EBDuplicateIDStrategy.ERROR:
|
|
@@ -462,15 +625,15 @@ export class BCollection {
|
|
|
462
625
|
case EBDuplicateIDStrategy.OVERWRITE:
|
|
463
626
|
// replace the new value
|
|
464
627
|
ret++;
|
|
465
|
-
if (
|
|
466
|
-
|
|
628
|
+
if (indexAdds) {
|
|
629
|
+
this.removeFromIndexes(this.mapData.get(id));
|
|
467
630
|
}
|
|
468
631
|
this.mapData.set(id, v);
|
|
469
|
-
if (
|
|
470
|
-
|
|
632
|
+
if (indexAdds) {
|
|
633
|
+
this.addKeysToIndexes(v, indexKeys[i]);
|
|
471
634
|
}
|
|
472
|
-
if (
|
|
473
|
-
|
|
635
|
+
if (collectAdded) {
|
|
636
|
+
added.push(v);
|
|
474
637
|
}
|
|
475
638
|
break;
|
|
476
639
|
}
|
|
@@ -478,169 +641,164 @@ export class BCollection {
|
|
|
478
641
|
else {
|
|
479
642
|
ret++;
|
|
480
643
|
this.mapData.set(id, v);
|
|
481
|
-
if (
|
|
482
|
-
|
|
644
|
+
if (indexAdds) {
|
|
645
|
+
this.addKeysToIndexes(v, indexKeys[i]);
|
|
483
646
|
}
|
|
484
|
-
if (
|
|
485
|
-
|
|
647
|
+
if (collectAdded) {
|
|
648
|
+
added.push(v);
|
|
486
649
|
}
|
|
487
650
|
}
|
|
488
651
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
if (addedMap.length > 0) {
|
|
493
|
-
await this.fireAdd(addedMap);
|
|
494
|
-
}
|
|
495
|
-
return ret;
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
496
654
|
default:
|
|
497
|
-
case EBMemStorageStrategy.ARRAY:
|
|
498
|
-
const
|
|
499
|
-
const indexAddsArray = this.indexDefinitions.size > 0;
|
|
500
|
-
const addedArray = [];
|
|
655
|
+
case EBMemStorageStrategy.ARRAY: {
|
|
656
|
+
const storedIDs = await this.storedArrayIDs();
|
|
501
657
|
switch (this.collectionConfig.duplicateIDStrategy) {
|
|
502
|
-
case EBDuplicateIDStrategy.ERROR:
|
|
503
|
-
const haveIDs = new Set();
|
|
504
|
-
for (const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
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
|
+
}
|
|
509
664
|
if (haveIDs.has(id)) {
|
|
510
665
|
throw new Error(`duplicate id ${id}`);
|
|
511
666
|
}
|
|
512
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++;
|
|
513
673
|
this.arrayData.push(v);
|
|
514
|
-
if (
|
|
515
|
-
|
|
674
|
+
if (indexAdds) {
|
|
675
|
+
this.addKeysToIndexes(v, indexKeys[i]);
|
|
516
676
|
}
|
|
517
|
-
if (
|
|
518
|
-
|
|
677
|
+
if (collectAdded) {
|
|
678
|
+
added.push(v);
|
|
519
679
|
}
|
|
520
|
-
}
|
|
680
|
+
});
|
|
521
681
|
break;
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
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) => {
|
|
529
688
|
if (!seenIDs.has(id)) {
|
|
530
689
|
seenIDs.add(id);
|
|
531
|
-
|
|
532
|
-
if (indexAddsArray) {
|
|
533
|
-
await this.addToIndexes(v);
|
|
534
|
-
}
|
|
535
|
-
if (trackAddsArray) {
|
|
536
|
-
addedArray.push(v);
|
|
537
|
-
}
|
|
690
|
+
toAdd.push(vs[i]);
|
|
538
691
|
}
|
|
539
|
-
}
|
|
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
|
+
});
|
|
540
704
|
break;
|
|
705
|
+
}
|
|
541
706
|
default:
|
|
542
|
-
case EBDuplicateIDStrategy.OVERWRITE:
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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++;
|
|
550
720
|
if (existingIndex !== undefined) {
|
|
551
|
-
if (
|
|
552
|
-
|
|
721
|
+
if (indexAdds) {
|
|
722
|
+
this.removeFromIndexes(this.arrayData[existingIndex]);
|
|
553
723
|
}
|
|
554
724
|
this.arrayData[existingIndex] = v;
|
|
555
725
|
}
|
|
556
726
|
else {
|
|
557
|
-
|
|
558
|
-
indexMap.set(id, this.arrayData.length);
|
|
727
|
+
positions.set(id, this.arrayData.length);
|
|
559
728
|
this.arrayData.push(v);
|
|
560
729
|
}
|
|
561
|
-
if (
|
|
562
|
-
|
|
730
|
+
if (indexAdds) {
|
|
731
|
+
this.addKeysToIndexes(v, indexKeys[i]);
|
|
563
732
|
}
|
|
564
|
-
if (
|
|
565
|
-
|
|
733
|
+
if (collectAdded) {
|
|
734
|
+
added.push(v);
|
|
566
735
|
}
|
|
567
|
-
}
|
|
736
|
+
});
|
|
568
737
|
break;
|
|
738
|
+
}
|
|
569
739
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (ret > 0) {
|
|
744
|
+
await this.doOnDirty();
|
|
745
|
+
}
|
|
746
|
+
await this.recordTxChanges(EBModificationType.ADD, added);
|
|
747
|
+
if (trackAdds && added.length > 0) {
|
|
748
|
+
await this.fireAdd(added);
|
|
577
749
|
}
|
|
750
|
+
return ret;
|
|
578
751
|
});
|
|
579
752
|
}
|
|
580
753
|
async update(predicate, updater) {
|
|
581
|
-
return this.
|
|
754
|
+
return this.runMutation(async () => {
|
|
582
755
|
await this.checkLoaded();
|
|
583
|
-
let ret = 0;
|
|
584
756
|
const trackUpdates = this.updateHandlers.length > 0;
|
|
585
757
|
const indexUpdates = this.indexDefinitions.size > 0;
|
|
586
|
-
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
|
+
};
|
|
587
767
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
588
768
|
case EBMemStorageStrategy.MAP:
|
|
589
769
|
for (const [key, value] of this.mapData) {
|
|
590
770
|
if (predicate(value)) {
|
|
591
|
-
|
|
592
|
-
if (replacement !== undefined) {
|
|
593
|
-
ret++;
|
|
594
|
-
if (indexUpdates) {
|
|
595
|
-
await this.removeFromIndexes(value);
|
|
596
|
-
}
|
|
597
|
-
this.mapData.set(key, replacement);
|
|
598
|
-
if (indexUpdates) {
|
|
599
|
-
await this.addToIndexes(replacement);
|
|
600
|
-
}
|
|
601
|
-
if (trackUpdates) {
|
|
602
|
-
updated.push([value, replacement]);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
else {
|
|
606
|
-
throw new Error("updater gave undefined");
|
|
607
|
-
}
|
|
771
|
+
await plan(value, (replacement) => { this.mapData.set(key, replacement); });
|
|
608
772
|
}
|
|
609
773
|
}
|
|
610
774
|
break;
|
|
611
775
|
default:
|
|
612
776
|
case EBMemStorageStrategy.ARRAY:
|
|
613
777
|
for (let i = 0; i < this.arrayData.length; i++) {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (replacement !== undefined) {
|
|
618
|
-
ret++;
|
|
619
|
-
if (indexUpdates) {
|
|
620
|
-
await this.removeFromIndexes(previous);
|
|
621
|
-
}
|
|
622
|
-
this.arrayData[i] = replacement;
|
|
623
|
-
if (indexUpdates) {
|
|
624
|
-
await this.addToIndexes(replacement);
|
|
625
|
-
}
|
|
626
|
-
if (trackUpdates) {
|
|
627
|
-
updated.push([previous, replacement]);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
else {
|
|
631
|
-
throw new Error("updater gave undefined");
|
|
632
|
-
}
|
|
778
|
+
const previous = this.arrayData[i];
|
|
779
|
+
if (predicate(previous)) {
|
|
780
|
+
await plan(previous, (replacement) => { this.arrayData[i] = replacement; });
|
|
633
781
|
}
|
|
634
782
|
}
|
|
635
783
|
break;
|
|
636
784
|
}
|
|
637
|
-
|
|
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) {
|
|
638
795
|
await this.doOnDirty();
|
|
639
796
|
}
|
|
640
|
-
|
|
641
|
-
|
|
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]));
|
|
642
800
|
}
|
|
643
|
-
return
|
|
801
|
+
return planned.length;
|
|
644
802
|
});
|
|
645
803
|
}
|
|
646
804
|
async doOnDirty() {
|
|
@@ -676,10 +834,10 @@ export class BCollection {
|
|
|
676
834
|
this.arrayData = await this.database.io.loadArrayData(this.collectionID);
|
|
677
835
|
break;
|
|
678
836
|
}
|
|
679
|
-
this.loaded = true;
|
|
680
|
-
this.dirty = false;
|
|
681
837
|
this.resolveIndexDefinitions();
|
|
682
838
|
await this.rebuildIndexRuntime();
|
|
839
|
+
this.loaded = true;
|
|
840
|
+
this.dirty = false;
|
|
683
841
|
});
|
|
684
842
|
}
|
|
685
843
|
async unload() {
|
|
@@ -706,9 +864,96 @@ export class BCollection {
|
|
|
706
864
|
this.arrayData = [];
|
|
707
865
|
break;
|
|
708
866
|
}
|
|
867
|
+
this.indexRuntime.clear();
|
|
868
|
+
this.indexedKeys.clear();
|
|
709
869
|
this.loaded = false;
|
|
710
870
|
this.dirty = false;
|
|
711
871
|
});
|
|
712
872
|
}
|
|
873
|
+
/**
|
|
874
|
+
* Creates a transaction-scoped working copy: same identity/config as this collection, but with
|
|
875
|
+
* independently mutable in-memory state (auto-flush forced off, regardless of this collection's own
|
|
876
|
+
* `autoFlushStrategy`) and no change-event handlers of its own. Call only while already holding this
|
|
877
|
+
* collection's mutex for the whole transaction (see {@link BDatabase.transaction}).
|
|
878
|
+
*/
|
|
879
|
+
async cloneForTransaction() {
|
|
880
|
+
return this.runProtected(async () => {
|
|
881
|
+
await this.checkLoaded();
|
|
882
|
+
const txConfig = { ...this.collectionConfig, autoFlushStrategy: EBAutoFlushStrategy.NO_AUTO_FLUSH };
|
|
883
|
+
const clone = new BCollection(this.database, this.collectionID, this.collectionName, txConfig);
|
|
884
|
+
clone.mapData = new Map(this.mapData);
|
|
885
|
+
clone.arrayData = this.arrayData.slice();
|
|
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
|
+
}
|
|
891
|
+
clone.resolveIndexDefinitions();
|
|
892
|
+
await clone.rebuildIndexRuntime();
|
|
893
|
+
return clone;
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
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.
|
|
901
|
+
*/
|
|
902
|
+
async prepareTransactionCommit(clone) {
|
|
903
|
+
return this.runProtected(async () => {
|
|
904
|
+
await this.checkLoaded();
|
|
905
|
+
const added = [];
|
|
906
|
+
const updated = [];
|
|
907
|
+
const deleted = [];
|
|
908
|
+
if (clone.txChanges && clone.txChanges.size > 0) {
|
|
909
|
+
const before = await this.idKeyedSnapshot();
|
|
910
|
+
const after = await clone.idKeyedSnapshot();
|
|
911
|
+
for (const [id, modificationType] of clone.txChanges) {
|
|
912
|
+
if (!after.has(id)) {
|
|
913
|
+
if (before.has(id)) {
|
|
914
|
+
deleted.push(before.get(id));
|
|
915
|
+
}
|
|
916
|
+
}
|
|
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)]);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
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;
|
|
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
|
+
};
|
|
956
|
+
});
|
|
957
|
+
}
|
|
713
958
|
}
|
|
714
959
|
//# sourceMappingURL=basics.js.map
|