@incoqnito.io/bajadab 1.2.2 → 1.4.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 +18 -13
- package/dist/base.js +2 -2
- package/dist/base.js.map +1 -1
- package/dist/basics.d.ts +30 -10
- package/dist/basics.js +466 -279
- package/dist/basics.js.map +1 -1
- package/dist/fileio.d.ts +7 -0
- package/dist/fileio.js +104 -22
- package/dist/fileio.js.map +1 -1
- package/package.json +5 -9
- package/src/main/base.ts +236 -231
- package/src/main/basics.ts +1135 -913
- package/src/main/fileio.ts +305 -207
package/dist/basics.js
CHANGED
|
@@ -1,45 +1,60 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { uuidv7, } from "uuidv7";
|
|
3
|
-
import {
|
|
3
|
+
import { ReentrantMutex } 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
|
-
|
|
14
|
-
|
|
10
|
+
function normalizeID(id) {
|
|
11
|
+
return id === undefined || id === null || id === "" ? undefined : String(id);
|
|
12
|
+
}
|
|
13
|
+
const UNCHANGED_TRANSACTION_COMMIT = Object.freeze({
|
|
14
|
+
write: Object.freeze({ commit: async () => { }, discard: async () => { } }),
|
|
15
|
+
apply: () => { },
|
|
16
|
+
fire: async () => { },
|
|
17
|
+
});
|
|
15
18
|
/** {@link IBCollectionConfig} used when a collection is created without an explicit one. */
|
|
16
|
-
export const DEFAULT_COLLECTION_CONFIG = {
|
|
19
|
+
export const DEFAULT_COLLECTION_CONFIG = Object.freeze({
|
|
17
20
|
arrayDeletionStrategy: EBArrayDeletionStrategy.IN_PLACE,
|
|
18
21
|
autoIDStrategy: EBAutoIDStrategy.AUTO_ID_UUID,
|
|
19
22
|
duplicateIDStrategy: EBDuplicateIDStrategy.OVERWRITE,
|
|
20
23
|
memStorageStrategy: EBMemStorageStrategy.MAP,
|
|
21
24
|
dirtyUnloadStrategy: EBDirtyUnloadStrategy.FLUSH,
|
|
22
25
|
autoFlushStrategy: EBAutoFlushStrategy.ALWAYS_AUTO_FLUSH,
|
|
23
|
-
};
|
|
26
|
+
});
|
|
24
27
|
/** {@link IBDatabaseConfig} used when `getDatabase()` creates a fresh database. */
|
|
25
|
-
export const DEFAULT_DATABASE_CONFIG = {
|
|
28
|
+
export const DEFAULT_DATABASE_CONFIG = Object.freeze({
|
|
26
29
|
defaultCollectionConfig: DEFAULT_COLLECTION_CONFIG,
|
|
27
|
-
};
|
|
30
|
+
});
|
|
28
31
|
export class BDatabase {
|
|
29
32
|
config;
|
|
30
33
|
collections = new Map();
|
|
31
34
|
io;
|
|
32
35
|
functionRegistry;
|
|
36
|
+
iid = uuidv7();
|
|
37
|
+
transactionScope = new AsyncLocalStorage();
|
|
38
|
+
mutex = new ReentrantMutex(this.iid);
|
|
33
39
|
constructor(io, config, functionRegistry) {
|
|
34
|
-
this.config = config ?? DEFAULT_DATABASE_CONFIG;
|
|
40
|
+
this.config = { ...(config ?? DEFAULT_DATABASE_CONFIG) };
|
|
35
41
|
this.io = io;
|
|
36
42
|
this.functionRegistry = functionRegistry;
|
|
37
43
|
}
|
|
44
|
+
runExclusive(fn) {
|
|
45
|
+
return this.mutex.run(fn);
|
|
46
|
+
}
|
|
47
|
+
runDetached(task) {
|
|
48
|
+
this.mutex.runAfterRelease(() => { void this.transactionScope.exit(task); });
|
|
49
|
+
}
|
|
50
|
+
isInTransaction(collectionName) {
|
|
51
|
+
return this.transactionScope.getStore()?.has(collectionName) ?? false;
|
|
52
|
+
}
|
|
38
53
|
addCollection(collection) {
|
|
39
54
|
this.collections.set(collection.collectionName, collection);
|
|
40
55
|
}
|
|
41
56
|
async getCollection(collectionName, collectionConfig) {
|
|
42
|
-
return
|
|
57
|
+
return this.runExclusive(async () => {
|
|
43
58
|
const cname = canonicalName(collectionName);
|
|
44
59
|
let ret = this.collections.get(cname);
|
|
45
60
|
if (!ret) {
|
|
@@ -49,30 +64,31 @@ export class BDatabase {
|
|
|
49
64
|
});
|
|
50
65
|
}
|
|
51
66
|
async createCollection(collectionName, collectionConfig) {
|
|
52
|
-
return
|
|
67
|
+
return this.runExclusive(async () => {
|
|
53
68
|
const cname = canonicalName(collectionName);
|
|
54
69
|
if (this.collections.has(cname)) {
|
|
55
70
|
throw new Error(`cannot create already existing collection ${collectionName}`);
|
|
56
71
|
}
|
|
57
72
|
const id = uuidv7();
|
|
58
|
-
const ret = new BCollection(this, id, collectionName, collectionConfig ?? this.config.defaultCollectionConfig);
|
|
73
|
+
const ret = new BCollection(this, id, collectionName, { ...(collectionConfig ?? this.config.defaultCollectionConfig) });
|
|
74
|
+
await this.io.createCollection(ret);
|
|
59
75
|
this.addCollection(ret);
|
|
60
|
-
return
|
|
76
|
+
return ret;
|
|
61
77
|
});
|
|
62
78
|
}
|
|
63
79
|
async hasCollection(collectionName) {
|
|
64
|
-
return
|
|
80
|
+
return this.runExclusive(async () => {
|
|
65
81
|
return this.collections.has(canonicalName(collectionName));
|
|
66
82
|
});
|
|
67
83
|
}
|
|
68
84
|
async dropCollection(collectionName) {
|
|
69
85
|
const cname = canonicalName(collectionName);
|
|
70
|
-
return
|
|
86
|
+
return this.runExclusive(async () => {
|
|
71
87
|
const coll = this.collections.get(cname);
|
|
72
88
|
if (coll) {
|
|
73
89
|
coll.markDropped();
|
|
90
|
+
await this.io.deleteCollection(cname);
|
|
74
91
|
this.collections.delete(cname);
|
|
75
|
-
return this.io.deleteCollection(cname);
|
|
76
92
|
}
|
|
77
93
|
else {
|
|
78
94
|
throw new Error(`no such collection ${collectionName}`);
|
|
@@ -80,8 +96,7 @@ export class BDatabase {
|
|
|
80
96
|
});
|
|
81
97
|
}
|
|
82
98
|
async snapshot(targetDir, options) {
|
|
83
|
-
|
|
84
|
-
return runMultiReentrantProtected([MutexKeys.DATABASE, ...collectionNames], async () => {
|
|
99
|
+
return this.runExclusive(async () => {
|
|
85
100
|
for (const collection of this.collections.values()) {
|
|
86
101
|
await collection.flush();
|
|
87
102
|
}
|
|
@@ -89,27 +104,64 @@ export class BDatabase {
|
|
|
89
104
|
});
|
|
90
105
|
}
|
|
91
106
|
async transaction(collectionNames, fn) {
|
|
107
|
+
if (this.transactionScope.getStore() !== undefined) {
|
|
108
|
+
throw new Error("nested transactions are not supported");
|
|
109
|
+
}
|
|
92
110
|
const canonicalNames = Array.from(new Set(collectionNames.map(canonicalName)));
|
|
93
|
-
return
|
|
111
|
+
return this.runExclusive(async () => {
|
|
94
112
|
const originals = new Map();
|
|
95
113
|
const clones = new Map();
|
|
96
114
|
for (const name of canonicalNames) {
|
|
97
|
-
const original =
|
|
115
|
+
const original = this.collections.get(name);
|
|
116
|
+
if (!original) {
|
|
117
|
+
throw new Error(`no such collection ${name}`);
|
|
118
|
+
}
|
|
98
119
|
originals.set(name, original);
|
|
120
|
+
}
|
|
121
|
+
for (const [name, original] of originals) {
|
|
99
122
|
clones.set(name, await original.cloneForTransaction());
|
|
100
123
|
}
|
|
124
|
+
const views = new Map(Array.from(clones, ([name, clone]) => [name, new BTransactionCollection(clone)]));
|
|
101
125
|
const tx = {
|
|
102
126
|
getCollection: async (collectionName) => {
|
|
103
|
-
const
|
|
104
|
-
if (!
|
|
127
|
+
const view = views.get(canonicalName(collectionName));
|
|
128
|
+
if (!view) {
|
|
105
129
|
throw new Error(`collection "${collectionName}" was not declared to this transaction`);
|
|
106
130
|
}
|
|
107
|
-
return
|
|
131
|
+
return view;
|
|
108
132
|
},
|
|
109
133
|
};
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
134
|
+
try {
|
|
135
|
+
await this.transactionScope.run(new Set(canonicalNames), () => fn(tx));
|
|
136
|
+
const prepared = [];
|
|
137
|
+
try {
|
|
138
|
+
for (const name of canonicalNames) {
|
|
139
|
+
prepared.push(await originals.get(name).prepareTransactionCommit(clones.get(name)));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
await Promise.all(prepared.map(p => p.write.discard()));
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
let renamed = 0;
|
|
147
|
+
try {
|
|
148
|
+
for (const p of prepared) {
|
|
149
|
+
await p.write.commit();
|
|
150
|
+
renamed++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
await Promise.all(prepared.slice(renamed).map(p => p.write.discard()));
|
|
155
|
+
const applied = prepared.slice(0, renamed);
|
|
156
|
+
applied.forEach(p => p.apply());
|
|
157
|
+
for (const p of applied) {
|
|
158
|
+
await p.fire();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
views.forEach(view => view.end());
|
|
164
|
+
clones.forEach(clone => clone.markDropped());
|
|
113
165
|
}
|
|
114
166
|
});
|
|
115
167
|
}
|
|
@@ -124,26 +176,19 @@ export class BCollection {
|
|
|
124
176
|
loaded = false;
|
|
125
177
|
dirty = false;
|
|
126
178
|
dropped = false;
|
|
179
|
+
transactionClone = false;
|
|
127
180
|
addHandlers = [];
|
|
128
181
|
updateHandlers = [];
|
|
129
182
|
deleteHandlers = [];
|
|
130
183
|
indexDefinitions = new Map();
|
|
131
184
|
indexRuntime = new Map();
|
|
185
|
+
indexedKeys = new Map();
|
|
186
|
+
txChanges;
|
|
132
187
|
constructor(database, collectionID, collectionName, collectionConfig) {
|
|
133
188
|
this.database = database;
|
|
134
189
|
this.collectionID = collectionID;
|
|
135
190
|
this.collectionName = canonicalName(collectionName);
|
|
136
191
|
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
192
|
}
|
|
148
193
|
async checkLoaded() {
|
|
149
194
|
if (!this.loaded) {
|
|
@@ -151,7 +196,7 @@ export class BCollection {
|
|
|
151
196
|
}
|
|
152
197
|
}
|
|
153
198
|
async runExclusive(fn) {
|
|
154
|
-
return
|
|
199
|
+
return this.database.runExclusive(fn);
|
|
155
200
|
}
|
|
156
201
|
async runProtected(fn) {
|
|
157
202
|
return this.runExclusive(async () => {
|
|
@@ -161,6 +206,14 @@ export class BCollection {
|
|
|
161
206
|
return fn();
|
|
162
207
|
});
|
|
163
208
|
}
|
|
209
|
+
async runMutation(fn) {
|
|
210
|
+
return this.runProtected(async () => {
|
|
211
|
+
if (!this.transactionClone && this.database.isInTransaction(this.collectionName)) {
|
|
212
|
+
throw new Error(`collection "${this.collectionName}" is part of a running transaction; write through tx.getCollection() instead`);
|
|
213
|
+
}
|
|
214
|
+
return fn();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
164
217
|
/**
|
|
165
218
|
* call this only within the protecting mutex
|
|
166
219
|
*/
|
|
@@ -206,7 +259,7 @@ export class BCollection {
|
|
|
206
259
|
if (!byKey) {
|
|
207
260
|
throw new Error(`index "${indexKey}" is not configured`);
|
|
208
261
|
}
|
|
209
|
-
return byKey.get(key)
|
|
262
|
+
return Array.from(byKey.get(key) ?? []);
|
|
210
263
|
});
|
|
211
264
|
}
|
|
212
265
|
async invokeHandler(fn, modificationType) {
|
|
@@ -214,41 +267,44 @@ export class BCollection {
|
|
|
214
267
|
await fn();
|
|
215
268
|
}
|
|
216
269
|
catch (error) {
|
|
217
|
-
|
|
270
|
+
// TODO switch logging to ILogging where necessary
|
|
271
|
+
console.error(`bajadab: collection "${this.collectionName}" ${modificationType} handler failed`, error);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async dispatchHandler(fn, modificationType, mode) {
|
|
275
|
+
if (mode === EBHandlerMode.SYNC) {
|
|
276
|
+
await this.invokeHandler(fn, modificationType);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
this.database.runDetached(() => this.invokeHandler(fn, modificationType));
|
|
218
280
|
}
|
|
219
281
|
}
|
|
220
282
|
async fireAdd(added) {
|
|
221
283
|
for (const v of added) {
|
|
222
284
|
for (const { handler, mode } of this.addHandlers) {
|
|
223
|
-
|
|
224
|
-
if (mode === EBHandlerMode.SYNC) {
|
|
225
|
-
await call;
|
|
226
|
-
}
|
|
285
|
+
await this.dispatchHandler(() => handler(v), EBModificationType.ADD, mode);
|
|
227
286
|
}
|
|
228
287
|
}
|
|
229
288
|
}
|
|
230
289
|
async fireUpdate(pairs) {
|
|
231
290
|
for (const [previous, updated] of pairs) {
|
|
232
291
|
for (const { handler, mode } of this.updateHandlers) {
|
|
233
|
-
|
|
234
|
-
if (mode === EBHandlerMode.SYNC) {
|
|
235
|
-
await call;
|
|
236
|
-
}
|
|
292
|
+
await this.dispatchHandler(() => handler(previous, updated), EBModificationType.UPDATE, mode);
|
|
237
293
|
}
|
|
238
294
|
}
|
|
239
295
|
}
|
|
240
296
|
async fireDelete(deleted) {
|
|
241
297
|
for (const v of deleted) {
|
|
242
298
|
for (const { handler, mode } of this.deleteHandlers) {
|
|
243
|
-
|
|
244
|
-
if (mode === EBHandlerMode.SYNC) {
|
|
245
|
-
await call;
|
|
246
|
-
}
|
|
299
|
+
await this.dispatchHandler(() => handler(v), EBModificationType.DELETE, mode);
|
|
247
300
|
}
|
|
248
301
|
}
|
|
249
302
|
}
|
|
250
303
|
async on(modificationType, handler, mode = EBHandlerMode.SYNC) {
|
|
251
304
|
return this.runProtected(async () => {
|
|
305
|
+
if (this.database.isInTransaction(this.collectionName)) {
|
|
306
|
+
throw new Error(`cannot register a handler on collection "${this.collectionName}" while it is part of a running transaction`);
|
|
307
|
+
}
|
|
252
308
|
switch (modificationType) {
|
|
253
309
|
case EBModificationType.ADD: {
|
|
254
310
|
const h = handler;
|
|
@@ -271,10 +327,11 @@ export class BCollection {
|
|
|
271
327
|
});
|
|
272
328
|
}
|
|
273
329
|
async delete(predicate) {
|
|
274
|
-
return this.
|
|
330
|
+
return this.runMutation(async () => {
|
|
275
331
|
await this.checkLoaded();
|
|
276
332
|
let ret = 0;
|
|
277
333
|
const trackDeletes = this.deleteHandlers.length > 0;
|
|
334
|
+
const collectDeleted = trackDeletes || this.txChanges !== undefined;
|
|
278
335
|
const indexDeletes = this.indexDefinitions.size > 0;
|
|
279
336
|
const deleted = [];
|
|
280
337
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
@@ -283,10 +340,10 @@ export class BCollection {
|
|
|
283
340
|
if (predicate(value)) {
|
|
284
341
|
ret++;
|
|
285
342
|
if (indexDeletes) {
|
|
286
|
-
|
|
343
|
+
this.removeFromIndexes(value);
|
|
287
344
|
}
|
|
288
345
|
this.mapData.delete(key);
|
|
289
|
-
if (
|
|
346
|
+
if (collectDeleted) {
|
|
290
347
|
deleted.push(value);
|
|
291
348
|
}
|
|
292
349
|
}
|
|
@@ -306,9 +363,9 @@ export class BCollection {
|
|
|
306
363
|
else {
|
|
307
364
|
ret++;
|
|
308
365
|
if (indexDeletes) {
|
|
309
|
-
|
|
366
|
+
this.removeFromIndexes(item);
|
|
310
367
|
}
|
|
311
|
-
if (
|
|
368
|
+
if (collectDeleted) {
|
|
312
369
|
deleted.push(item);
|
|
313
370
|
}
|
|
314
371
|
}
|
|
@@ -317,20 +374,29 @@ export class BCollection {
|
|
|
317
374
|
break;
|
|
318
375
|
default:
|
|
319
376
|
case EBArrayDeletionStrategy.NEW_ARRAY:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
377
|
+
if (collectDeleted || indexDeletes) {
|
|
378
|
+
const kept = [];
|
|
379
|
+
for (const v of this.arrayData) {
|
|
380
|
+
if (predicate(v)) {
|
|
381
|
+
ret++;
|
|
382
|
+
if (indexDeletes) {
|
|
383
|
+
this.removeFromIndexes(v);
|
|
384
|
+
}
|
|
385
|
+
if (collectDeleted) {
|
|
386
|
+
deleted.push(v);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
kept.push(v);
|
|
329
391
|
}
|
|
330
392
|
}
|
|
393
|
+
this.arrayData = kept;
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
const ol = this.arrayData.length;
|
|
397
|
+
this.arrayData = this.arrayData.filter(v => !predicate(v));
|
|
398
|
+
ret += (ol - this.arrayData.length);
|
|
331
399
|
}
|
|
332
|
-
this.arrayData = this.arrayData.filter(v => !predicate(v));
|
|
333
|
-
ret += (ol - this.arrayData.length);
|
|
334
400
|
break;
|
|
335
401
|
}
|
|
336
402
|
break;
|
|
@@ -338,7 +404,8 @@ export class BCollection {
|
|
|
338
404
|
if (ret > 0) {
|
|
339
405
|
await this.doOnDirty();
|
|
340
406
|
}
|
|
341
|
-
|
|
407
|
+
await this.recordTxChanges(EBModificationType.DELETE, deleted);
|
|
408
|
+
if (trackDeletes && deleted.length > 0) {
|
|
342
409
|
await this.fireDelete(deleted);
|
|
343
410
|
}
|
|
344
411
|
return ret;
|
|
@@ -349,25 +416,23 @@ export class BCollection {
|
|
|
349
416
|
await this.checkLoaded();
|
|
350
417
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
351
418
|
case EBMemStorageStrategy.MAP:
|
|
352
|
-
return this.mapData.values().
|
|
419
|
+
return this.mapData.values().some(predicate);
|
|
353
420
|
default:
|
|
354
421
|
case EBMemStorageStrategy.ARRAY:
|
|
355
|
-
return this.arrayData.
|
|
422
|
+
return this.arrayData.some(predicate);
|
|
356
423
|
}
|
|
357
424
|
});
|
|
358
425
|
}
|
|
359
426
|
resolveIndexDefinitions() {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
for (const [indexKey, registryKey] of Object.entries(this.collectionConfig.indexes)) {
|
|
427
|
+
const ret = new Map();
|
|
428
|
+
for (const [indexKey, registryKey] of Object.entries(this.collectionConfig.indexes ?? {})) {
|
|
365
429
|
const extractor = this.database.functionRegistry?.fetch(registryKey);
|
|
366
430
|
if (!extractor) {
|
|
367
431
|
throw new Error(`index key function "${registryKey}" is not registered`);
|
|
368
432
|
}
|
|
369
|
-
|
|
433
|
+
ret.set(indexKey, extractor);
|
|
370
434
|
}
|
|
435
|
+
return ret;
|
|
371
436
|
}
|
|
372
437
|
async indexKeysFor(v) {
|
|
373
438
|
const result = [];
|
|
@@ -381,31 +446,46 @@ export class BCollection {
|
|
|
381
446
|
return result;
|
|
382
447
|
}
|
|
383
448
|
async addToIndexes(v) {
|
|
384
|
-
|
|
449
|
+
this.addKeysToIndexes(v, await this.indexKeysFor(v));
|
|
450
|
+
}
|
|
451
|
+
async indexKeysForAll(vs) {
|
|
452
|
+
const ret = [];
|
|
453
|
+
for (const v of vs) {
|
|
454
|
+
ret.push(await this.indexKeysFor(v));
|
|
455
|
+
}
|
|
456
|
+
return ret;
|
|
457
|
+
}
|
|
458
|
+
addKeysToIndexes(v, indexKeys) {
|
|
459
|
+
for (const [indexKey, keys] of indexKeys) {
|
|
385
460
|
const byKey = this.indexRuntime.get(indexKey);
|
|
386
461
|
for (const key of keys) {
|
|
387
462
|
let bucket = byKey.get(key);
|
|
388
463
|
if (!bucket) {
|
|
389
|
-
bucket =
|
|
464
|
+
bucket = new Set();
|
|
390
465
|
byKey.set(key, bucket);
|
|
391
466
|
}
|
|
392
|
-
bucket.
|
|
467
|
+
bucket.add(v);
|
|
393
468
|
}
|
|
394
469
|
}
|
|
470
|
+
if (indexKeys.length > 0) {
|
|
471
|
+
this.indexedKeys.set(v, indexKeys);
|
|
472
|
+
}
|
|
395
473
|
}
|
|
396
|
-
|
|
397
|
-
|
|
474
|
+
removeFromIndexes(v) {
|
|
475
|
+
const indexKeys = this.indexedKeys.get(v);
|
|
476
|
+
if (!indexKeys) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
this.indexedKeys.delete(v);
|
|
480
|
+
for (const [indexKey, keys] of indexKeys) {
|
|
398
481
|
const byKey = this.indexRuntime.get(indexKey);
|
|
399
482
|
for (const key of keys) {
|
|
400
483
|
const bucket = byKey.get(key);
|
|
401
484
|
if (!bucket) {
|
|
402
485
|
continue;
|
|
403
486
|
}
|
|
404
|
-
|
|
405
|
-
if (
|
|
406
|
-
bucket.splice(at, 1);
|
|
407
|
-
}
|
|
408
|
-
if (bucket.length === 0) {
|
|
487
|
+
bucket.delete(v);
|
|
488
|
+
if (bucket.size === 0) {
|
|
409
489
|
byKey.delete(key);
|
|
410
490
|
}
|
|
411
491
|
}
|
|
@@ -419,6 +499,7 @@ export class BCollection {
|
|
|
419
499
|
// every configured index gets an entry up front, even if empty, so getByIndex()/
|
|
420
500
|
// addToIndexes()/removeFromIndexes() can assume it's there instead of checking twice
|
|
421
501
|
this.indexRuntime = new Map(Array.from(this.indexDefinitions.keys(), (indexKey) => [indexKey, new Map()]));
|
|
502
|
+
this.indexedKeys = new Map();
|
|
422
503
|
if (this.indexDefinitions.size === 0) {
|
|
423
504
|
return;
|
|
424
505
|
}
|
|
@@ -441,18 +522,18 @@ export class BCollection {
|
|
|
441
522
|
/**
|
|
442
523
|
* id -> value for every stored item, resolved via the plain id getter (no auto-id assignment - every
|
|
443
524
|
* stored item already has one). Used to diff a transaction's working copy against the pre-transaction
|
|
444
|
-
* state in {@link
|
|
525
|
+
* state in {@link prepareTransactionCommit}.
|
|
445
526
|
*/
|
|
446
527
|
async idKeyedSnapshot() {
|
|
447
528
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
448
529
|
case EBMemStorageStrategy.MAP:
|
|
449
|
-
return
|
|
530
|
+
return this.mapData;
|
|
450
531
|
default:
|
|
451
532
|
case EBMemStorageStrategy.ARRAY:
|
|
452
533
|
const getter = this.resolveIDGetter();
|
|
453
534
|
const ret = new Map();
|
|
454
535
|
for (const v of this.arrayData) {
|
|
455
|
-
const id = await getter(v);
|
|
536
|
+
const id = normalizeID(await getter(v));
|
|
456
537
|
if (id) {
|
|
457
538
|
ret.set(id, v);
|
|
458
539
|
}
|
|
@@ -460,43 +541,90 @@ export class BCollection {
|
|
|
460
541
|
return ret;
|
|
461
542
|
}
|
|
462
543
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
544
|
+
resolveIDSetter() {
|
|
545
|
+
if (this.collectionConfig.setID) {
|
|
546
|
+
const found = this.database.functionRegistry?.fetch(this.collectionConfig.setID);
|
|
547
|
+
if (!found) {
|
|
548
|
+
throw new Error(`setID function "${this.collectionConfig.setID}" is not registered`);
|
|
549
|
+
}
|
|
550
|
+
return found;
|
|
551
|
+
}
|
|
552
|
+
return DEFAULT_SET_ID;
|
|
553
|
+
}
|
|
554
|
+
async resolveIncomingIDs(vs) {
|
|
555
|
+
const getter = this.resolveIDGetter();
|
|
556
|
+
const ids = [];
|
|
557
|
+
for (const v of vs) {
|
|
558
|
+
const id = normalizeID(await getter(v));
|
|
559
|
+
if (!id && this.collectionConfig.autoIDStrategy === EBAutoIDStrategy.NO_AUTO_ID) {
|
|
560
|
+
throw new Error("cannot omit auto-ids and than trying to add a value without an id");
|
|
561
|
+
}
|
|
562
|
+
ids.push(id);
|
|
563
|
+
}
|
|
564
|
+
return ids;
|
|
565
|
+
}
|
|
566
|
+
async assignMissingIDs(vs, ids) {
|
|
567
|
+
let setter;
|
|
568
|
+
const ret = [];
|
|
569
|
+
for (let i = 0; i < vs.length; i++) {
|
|
570
|
+
let id = ids[i];
|
|
571
|
+
if (!id) {
|
|
572
|
+
setter ??= this.resolveIDSetter();
|
|
573
|
+
id = uuidv7();
|
|
574
|
+
await setter(vs[i], id);
|
|
575
|
+
}
|
|
576
|
+
ret.push(id);
|
|
577
|
+
}
|
|
578
|
+
return ret;
|
|
579
|
+
}
|
|
580
|
+
async storedArrayIDs() {
|
|
581
|
+
const getter = this.resolveIDGetter();
|
|
582
|
+
const ids = [];
|
|
583
|
+
for (const v of this.arrayData) {
|
|
584
|
+
ids.push(normalizeID(await getter(v)));
|
|
585
|
+
}
|
|
586
|
+
return ids;
|
|
587
|
+
}
|
|
588
|
+
async recordTxChanges(modificationType, values) {
|
|
589
|
+
if (!this.txChanges || values.length === 0) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const getter = this.resolveIDGetter();
|
|
593
|
+
for (const v of values) {
|
|
594
|
+
const id = normalizeID(await getter(v));
|
|
595
|
+
if (id) {
|
|
596
|
+
this.txChanges.set(id, modificationType);
|
|
485
597
|
}
|
|
486
598
|
}
|
|
487
|
-
return id;
|
|
488
599
|
}
|
|
489
600
|
async add(...vs) {
|
|
490
|
-
return this.
|
|
601
|
+
return this.runMutation(async () => {
|
|
491
602
|
await this.checkLoaded();
|
|
603
|
+
const trackAdds = this.addHandlers.length > 0;
|
|
604
|
+
const collectAdded = trackAdds || this.txChanges !== undefined;
|
|
605
|
+
const indexAdds = this.indexDefinitions.size > 0;
|
|
606
|
+
const incomingIDs = await this.resolveIncomingIDs(vs);
|
|
607
|
+
const added = [];
|
|
608
|
+
let ret = 0;
|
|
492
609
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
493
|
-
case EBMemStorageStrategy.MAP:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
610
|
+
case EBMemStorageStrategy.MAP: {
|
|
611
|
+
if (this.collectionConfig.duplicateIDStrategy === EBDuplicateIDStrategy.ERROR) {
|
|
612
|
+
const batchIDs = new Set();
|
|
613
|
+
for (const id of incomingIDs) {
|
|
614
|
+
if (id === undefined) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (this.mapData.has(id) || batchIDs.has(id)) {
|
|
618
|
+
throw new Error(`duplicate id ${id}`);
|
|
619
|
+
}
|
|
620
|
+
batchIDs.add(id);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const ids = await this.assignMissingIDs(vs, incomingIDs);
|
|
624
|
+
const indexKeys = indexAdds ? await this.indexKeysForAll(vs) : [];
|
|
625
|
+
for (let i = 0; i < vs.length; i++) {
|
|
626
|
+
const id = ids[i];
|
|
627
|
+
const v = vs[i];
|
|
500
628
|
if (this.mapData.has(id)) {
|
|
501
629
|
switch (this.collectionConfig.duplicateIDStrategy) {
|
|
502
630
|
case EBDuplicateIDStrategy.ERROR:
|
|
@@ -508,15 +636,15 @@ export class BCollection {
|
|
|
508
636
|
case EBDuplicateIDStrategy.OVERWRITE:
|
|
509
637
|
// replace the new value
|
|
510
638
|
ret++;
|
|
511
|
-
if (
|
|
512
|
-
|
|
639
|
+
if (indexAdds) {
|
|
640
|
+
this.removeFromIndexes(this.mapData.get(id));
|
|
513
641
|
}
|
|
514
642
|
this.mapData.set(id, v);
|
|
515
|
-
if (
|
|
516
|
-
|
|
643
|
+
if (indexAdds) {
|
|
644
|
+
this.addKeysToIndexes(v, indexKeys[i]);
|
|
517
645
|
}
|
|
518
|
-
if (
|
|
519
|
-
|
|
646
|
+
if (collectAdded) {
|
|
647
|
+
added.push(v);
|
|
520
648
|
}
|
|
521
649
|
break;
|
|
522
650
|
}
|
|
@@ -524,169 +652,164 @@ export class BCollection {
|
|
|
524
652
|
else {
|
|
525
653
|
ret++;
|
|
526
654
|
this.mapData.set(id, v);
|
|
527
|
-
if (
|
|
528
|
-
|
|
655
|
+
if (indexAdds) {
|
|
656
|
+
this.addKeysToIndexes(v, indexKeys[i]);
|
|
529
657
|
}
|
|
530
|
-
if (
|
|
531
|
-
|
|
658
|
+
if (collectAdded) {
|
|
659
|
+
added.push(v);
|
|
532
660
|
}
|
|
533
661
|
}
|
|
534
662
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
if (addedMap.length > 0) {
|
|
539
|
-
await this.fireAdd(addedMap);
|
|
540
|
-
}
|
|
541
|
-
return ret;
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
542
665
|
default:
|
|
543
|
-
case EBMemStorageStrategy.ARRAY:
|
|
544
|
-
const
|
|
545
|
-
const indexAddsArray = this.indexDefinitions.size > 0;
|
|
546
|
-
const addedArray = [];
|
|
666
|
+
case EBMemStorageStrategy.ARRAY: {
|
|
667
|
+
const storedIDs = await this.storedArrayIDs();
|
|
547
668
|
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);
|
|
669
|
+
case EBDuplicateIDStrategy.ERROR: {
|
|
670
|
+
const haveIDs = new Set(storedIDs.filter((id) => id !== undefined));
|
|
671
|
+
for (const id of incomingIDs) {
|
|
672
|
+
if (id === undefined) {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
555
675
|
if (haveIDs.has(id)) {
|
|
556
676
|
throw new Error(`duplicate id ${id}`);
|
|
557
677
|
}
|
|
558
678
|
haveIDs.add(id);
|
|
679
|
+
}
|
|
680
|
+
await this.assignMissingIDs(vs, incomingIDs);
|
|
681
|
+
const indexKeys = indexAdds ? await this.indexKeysForAll(vs) : [];
|
|
682
|
+
vs.forEach((v, i) => {
|
|
683
|
+
ret++;
|
|
559
684
|
this.arrayData.push(v);
|
|
560
|
-
if (
|
|
561
|
-
|
|
685
|
+
if (indexAdds) {
|
|
686
|
+
this.addKeysToIndexes(v, indexKeys[i]);
|
|
562
687
|
}
|
|
563
|
-
if (
|
|
564
|
-
|
|
688
|
+
if (collectAdded) {
|
|
689
|
+
added.push(v);
|
|
565
690
|
}
|
|
566
|
-
}
|
|
691
|
+
});
|
|
567
692
|
break;
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const id = await this.getID(v);
|
|
693
|
+
}
|
|
694
|
+
case EBDuplicateIDStrategy.IGNORE: {
|
|
695
|
+
const ids = await this.assignMissingIDs(vs, incomingIDs);
|
|
696
|
+
const seenIDs = new Set(storedIDs.filter((id) => id !== undefined));
|
|
697
|
+
const toAdd = [];
|
|
698
|
+
ids.forEach((id, i) => {
|
|
575
699
|
if (!seenIDs.has(id)) {
|
|
576
700
|
seenIDs.add(id);
|
|
577
|
-
|
|
578
|
-
if (indexAddsArray) {
|
|
579
|
-
await this.addToIndexes(v);
|
|
580
|
-
}
|
|
581
|
-
if (trackAddsArray) {
|
|
582
|
-
addedArray.push(v);
|
|
583
|
-
}
|
|
701
|
+
toAdd.push(vs[i]);
|
|
584
702
|
}
|
|
585
|
-
}
|
|
703
|
+
});
|
|
704
|
+
const indexKeys = indexAdds ? await this.indexKeysForAll(toAdd) : [];
|
|
705
|
+
toAdd.forEach((v, i) => {
|
|
706
|
+
ret++;
|
|
707
|
+
this.arrayData.push(v);
|
|
708
|
+
if (indexAdds) {
|
|
709
|
+
this.addKeysToIndexes(v, indexKeys[i]);
|
|
710
|
+
}
|
|
711
|
+
if (collectAdded) {
|
|
712
|
+
added.push(v);
|
|
713
|
+
}
|
|
714
|
+
});
|
|
586
715
|
break;
|
|
716
|
+
}
|
|
587
717
|
default:
|
|
588
|
-
case EBDuplicateIDStrategy.OVERWRITE:
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
718
|
+
case EBDuplicateIDStrategy.OVERWRITE: {
|
|
719
|
+
const ids = await this.assignMissingIDs(vs, incomingIDs);
|
|
720
|
+
const indexKeys = indexAdds ? await this.indexKeysForAll(vs) : [];
|
|
721
|
+
const positions = new Map();
|
|
722
|
+
storedIDs.forEach((id, i) => {
|
|
723
|
+
if (id !== undefined) {
|
|
724
|
+
positions.set(id, i);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
vs.forEach((v, i) => {
|
|
728
|
+
const id = ids[i];
|
|
729
|
+
const existingIndex = positions.get(id);
|
|
730
|
+
ret++;
|
|
596
731
|
if (existingIndex !== undefined) {
|
|
597
|
-
if (
|
|
598
|
-
|
|
732
|
+
if (indexAdds) {
|
|
733
|
+
this.removeFromIndexes(this.arrayData[existingIndex]);
|
|
599
734
|
}
|
|
600
735
|
this.arrayData[existingIndex] = v;
|
|
601
736
|
}
|
|
602
737
|
else {
|
|
603
|
-
|
|
604
|
-
indexMap.set(id, this.arrayData.length);
|
|
738
|
+
positions.set(id, this.arrayData.length);
|
|
605
739
|
this.arrayData.push(v);
|
|
606
740
|
}
|
|
607
|
-
if (
|
|
608
|
-
|
|
741
|
+
if (indexAdds) {
|
|
742
|
+
this.addKeysToIndexes(v, indexKeys[i]);
|
|
609
743
|
}
|
|
610
|
-
if (
|
|
611
|
-
|
|
744
|
+
if (collectAdded) {
|
|
745
|
+
added.push(v);
|
|
612
746
|
}
|
|
613
|
-
}
|
|
747
|
+
});
|
|
614
748
|
break;
|
|
749
|
+
}
|
|
615
750
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (ret > 0) {
|
|
755
|
+
await this.doOnDirty();
|
|
756
|
+
}
|
|
757
|
+
await this.recordTxChanges(EBModificationType.ADD, added);
|
|
758
|
+
if (trackAdds && added.length > 0) {
|
|
759
|
+
await this.fireAdd(added);
|
|
623
760
|
}
|
|
761
|
+
return ret;
|
|
624
762
|
});
|
|
625
763
|
}
|
|
626
764
|
async update(predicate, updater) {
|
|
627
|
-
return this.
|
|
765
|
+
return this.runMutation(async () => {
|
|
628
766
|
await this.checkLoaded();
|
|
629
|
-
let ret = 0;
|
|
630
767
|
const trackUpdates = this.updateHandlers.length > 0;
|
|
631
768
|
const indexUpdates = this.indexDefinitions.size > 0;
|
|
632
|
-
const
|
|
769
|
+
const planned = [];
|
|
770
|
+
const plan = async (previous, store) => {
|
|
771
|
+
const replacement = await updater(previous);
|
|
772
|
+
if (replacement === undefined) {
|
|
773
|
+
throw new Error("updater gave undefined");
|
|
774
|
+
}
|
|
775
|
+
const replacementKeys = indexUpdates ? await this.indexKeysFor(replacement) : [];
|
|
776
|
+
planned.push({ previous, replacement, replacementKeys, store });
|
|
777
|
+
};
|
|
633
778
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
634
779
|
case EBMemStorageStrategy.MAP:
|
|
635
780
|
for (const [key, value] of this.mapData) {
|
|
636
781
|
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
|
-
}
|
|
782
|
+
await plan(value, (replacement) => { this.mapData.set(key, replacement); });
|
|
654
783
|
}
|
|
655
784
|
}
|
|
656
785
|
break;
|
|
657
786
|
default:
|
|
658
787
|
case EBMemStorageStrategy.ARRAY:
|
|
659
788
|
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
|
-
}
|
|
789
|
+
const previous = this.arrayData[i];
|
|
790
|
+
if (predicate(previous)) {
|
|
791
|
+
await plan(previous, (replacement) => { this.arrayData[i] = replacement; });
|
|
679
792
|
}
|
|
680
793
|
}
|
|
681
794
|
break;
|
|
682
795
|
}
|
|
683
|
-
|
|
796
|
+
for (const { previous, replacement, replacementKeys, store } of planned) {
|
|
797
|
+
if (indexUpdates) {
|
|
798
|
+
this.removeFromIndexes(previous);
|
|
799
|
+
}
|
|
800
|
+
store(replacement);
|
|
801
|
+
if (indexUpdates) {
|
|
802
|
+
this.addKeysToIndexes(replacement, replacementKeys);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (planned.length > 0) {
|
|
684
806
|
await this.doOnDirty();
|
|
685
807
|
}
|
|
686
|
-
|
|
687
|
-
|
|
808
|
+
await this.recordTxChanges(EBModificationType.UPDATE, planned.map(({ replacement }) => replacement));
|
|
809
|
+
if (trackUpdates && planned.length > 0) {
|
|
810
|
+
await this.fireUpdate(planned.map(({ previous, replacement }) => [previous, replacement]));
|
|
688
811
|
}
|
|
689
|
-
return
|
|
812
|
+
return planned.length;
|
|
690
813
|
});
|
|
691
814
|
}
|
|
692
815
|
async doOnDirty() {
|
|
@@ -713,6 +836,7 @@ export class BCollection {
|
|
|
713
836
|
}
|
|
714
837
|
async reload() {
|
|
715
838
|
return this.runProtected(async () => {
|
|
839
|
+
const indexDefinitions = this.resolveIndexDefinitions();
|
|
716
840
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
717
841
|
case EBMemStorageStrategy.MAP:
|
|
718
842
|
this.mapData = await this.database.io.loadMapData(this.collectionID);
|
|
@@ -722,10 +846,11 @@ export class BCollection {
|
|
|
722
846
|
this.arrayData = await this.database.io.loadArrayData(this.collectionID);
|
|
723
847
|
break;
|
|
724
848
|
}
|
|
725
|
-
this.
|
|
849
|
+
this.indexDefinitions = indexDefinitions;
|
|
726
850
|
this.dirty = false;
|
|
727
|
-
this.
|
|
851
|
+
this.loaded = false;
|
|
728
852
|
await this.rebuildIndexRuntime();
|
|
853
|
+
this.loaded = true;
|
|
729
854
|
});
|
|
730
855
|
}
|
|
731
856
|
async unload() {
|
|
@@ -752,6 +877,8 @@ export class BCollection {
|
|
|
752
877
|
this.arrayData = [];
|
|
753
878
|
break;
|
|
754
879
|
}
|
|
880
|
+
this.indexRuntime.clear();
|
|
881
|
+
this.indexedKeys.clear();
|
|
755
882
|
this.loaded = false;
|
|
756
883
|
this.dirty = false;
|
|
757
884
|
});
|
|
@@ -770,63 +897,123 @@ export class BCollection {
|
|
|
770
897
|
clone.mapData = new Map(this.mapData);
|
|
771
898
|
clone.arrayData = this.arrayData.slice();
|
|
772
899
|
clone.loaded = true;
|
|
773
|
-
clone.
|
|
774
|
-
|
|
900
|
+
clone.transactionClone = true;
|
|
901
|
+
if (this.addHandlers.length > 0 || this.updateHandlers.length > 0 || this.deleteHandlers.length > 0) {
|
|
902
|
+
clone.txChanges = new Map();
|
|
903
|
+
}
|
|
904
|
+
clone.indexDefinitions = this.indexDefinitions;
|
|
905
|
+
clone.indexRuntime = new Map(Array.from(this.indexRuntime, ([indexKey, byKey]) => [indexKey, new Map(Array.from(byKey, ([key, bucket]) => [key, new Set(bucket)]))]));
|
|
906
|
+
clone.indexedKeys = new Map(this.indexedKeys);
|
|
775
907
|
return clone;
|
|
776
908
|
});
|
|
777
909
|
}
|
|
778
910
|
/**
|
|
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.
|
|
911
|
+
* Writes `clone`'s state to a temp file (see {@link IPreparedWrite}), unless the transaction left it unchanged, and computes the handler events for the
|
|
912
|
+
* net changes against this collection's current state: one per id the transaction touched, typed by the last
|
|
913
|
+
* operation on it (`DELETE` if gone, `ADD` if new or last stored via `add()`, `UPDATE` otherwise). Nothing is
|
|
914
|
+
* applied until `apply()`; call only while already holding this collection's mutex for the whole transaction.
|
|
785
915
|
*/
|
|
786
|
-
async
|
|
916
|
+
async prepareTransactionCommit(clone) {
|
|
787
917
|
return this.runProtected(async () => {
|
|
918
|
+
if (!clone.dirty) {
|
|
919
|
+
return UNCHANGED_TRANSACTION_COMMIT;
|
|
920
|
+
}
|
|
788
921
|
await this.checkLoaded();
|
|
789
922
|
const added = [];
|
|
790
923
|
const updated = [];
|
|
791
924
|
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) {
|
|
925
|
+
if (clone.txChanges && clone.txChanges.size > 0) {
|
|
796
926
|
const before = await this.idKeyedSnapshot();
|
|
797
927
|
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]);
|
|
928
|
+
for (const [id, modificationType] of clone.txChanges) {
|
|
929
|
+
if (!after.has(id)) {
|
|
930
|
+
if (before.has(id)) {
|
|
931
|
+
deleted.push(before.get(id));
|
|
806
932
|
}
|
|
807
933
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
934
|
+
else if (!before.has(id) || modificationType === EBModificationType.ADD) {
|
|
935
|
+
added.push(after.get(id));
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
updated.push([before.get(id), after.get(id)]);
|
|
812
939
|
}
|
|
813
940
|
}
|
|
814
941
|
}
|
|
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);
|
|
942
|
+
let write;
|
|
943
|
+
switch (this.collectionConfig.memStorageStrategy) {
|
|
944
|
+
case EBMemStorageStrategy.MAP:
|
|
945
|
+
write = await this.database.io.prepareMapData(this.collectionID, clone.mapData);
|
|
946
|
+
break;
|
|
947
|
+
default:
|
|
948
|
+
case EBMemStorageStrategy.ARRAY:
|
|
949
|
+
write = await this.database.io.prepareArrayData(this.collectionID, clone.arrayData);
|
|
950
|
+
break;
|
|
828
951
|
}
|
|
952
|
+
return {
|
|
953
|
+
write,
|
|
954
|
+
apply: () => {
|
|
955
|
+
this.mapData = clone.mapData;
|
|
956
|
+
this.arrayData = clone.arrayData;
|
|
957
|
+
this.indexRuntime = clone.indexRuntime;
|
|
958
|
+
this.indexedKeys = clone.indexedKeys;
|
|
959
|
+
this.dirty = false;
|
|
960
|
+
},
|
|
961
|
+
fire: () => this.runProtected(async () => {
|
|
962
|
+
if (added.length > 0) {
|
|
963
|
+
await this.fireAdd(added);
|
|
964
|
+
}
|
|
965
|
+
if (updated.length > 0) {
|
|
966
|
+
await this.fireUpdate(updated);
|
|
967
|
+
}
|
|
968
|
+
if (deleted.length > 0) {
|
|
969
|
+
await this.fireDelete(deleted);
|
|
970
|
+
}
|
|
971
|
+
}),
|
|
972
|
+
};
|
|
829
973
|
});
|
|
830
974
|
}
|
|
831
975
|
}
|
|
976
|
+
class BTransactionCollection {
|
|
977
|
+
clone;
|
|
978
|
+
ended = false;
|
|
979
|
+
constructor(clone) {
|
|
980
|
+
this.clone = clone;
|
|
981
|
+
}
|
|
982
|
+
get collectionName() {
|
|
983
|
+
return this.clone.collectionName;
|
|
984
|
+
}
|
|
985
|
+
get collectionID() {
|
|
986
|
+
return this.clone.collectionID;
|
|
987
|
+
}
|
|
988
|
+
end() {
|
|
989
|
+
this.ended = true;
|
|
990
|
+
}
|
|
991
|
+
active() {
|
|
992
|
+
if (this.ended) {
|
|
993
|
+
throw new Error(`the transaction on collection "${this.clone.collectionName}" has already ended`);
|
|
994
|
+
}
|
|
995
|
+
return this.clone;
|
|
996
|
+
}
|
|
997
|
+
async get(predicate) {
|
|
998
|
+
return this.active().get(predicate);
|
|
999
|
+
}
|
|
1000
|
+
async has(predicate) {
|
|
1001
|
+
return this.active().has(predicate);
|
|
1002
|
+
}
|
|
1003
|
+
async count(predicate) {
|
|
1004
|
+
return this.active().count(predicate);
|
|
1005
|
+
}
|
|
1006
|
+
async getByIndex(indexKey, key) {
|
|
1007
|
+
return this.active().getByIndex(indexKey, key);
|
|
1008
|
+
}
|
|
1009
|
+
async add(...vs) {
|
|
1010
|
+
return this.active().add(...vs);
|
|
1011
|
+
}
|
|
1012
|
+
async update(predicate, updater) {
|
|
1013
|
+
return this.active().update(predicate, updater);
|
|
1014
|
+
}
|
|
1015
|
+
async delete(predicate) {
|
|
1016
|
+
return this.active().delete(predicate);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
832
1019
|
//# sourceMappingURL=basics.js.map
|