@incoqnito.io/bajadab 1.1.0 → 1.2.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.
@@ -1,482 +1,777 @@
1
- import logger from "winston";
2
-
3
- import { uuidv7, } from "uuidv7";
4
- import { runMultiReentrantProtected, runReentrantProtected } from "@incoqnito.io/ts-iq-core";
5
-
6
- import type { IODatabase } from "./fileio.js";
7
-
8
- import {
9
- EBArrayDeletionStrategy,
10
- EBAutoFlushStrategy,
11
- EBAutoIDStrategy,
12
- EBDirtyUnloadStrategy,
13
- EBDuplicateIDStrategy,
14
- EBMemStorageStrategy,
15
- IBCollection,
16
- IBCollectionConfig,
17
- IBDatabase,
18
- IBDatabaseConfig,
19
- IBFunctionRegistry,
20
- TGetIDFunction,
21
- TSetIDFunction,
22
- } from "./base.js";
23
- import { canonicalName } from "./utils.js";
24
-
25
- /** Default {@link TGetIDFunction}: reads `v.id`, `undefined` if absent. */
26
- export const DEFAULT_GET_ID: TGetIDFunction = (v: any) => ("id" in v) ? v.id : undefined;
27
- /** Default {@link TSetIDFunction}: assigns to `v.id`. */
28
- export const DEFAULT_SET_ID: TSetIDFunction = async (v: any, id: string) => { v.id = id; };
29
-
30
- const enum MutexKeys {
31
- // because of the spaces this will never be a collection name and as such is safe to use as a mutex key
32
- DATABASE = " database ",
33
- }
34
-
35
- /** {@link IBCollectionConfig} used when a collection is created without an explicit one. */
36
- export const DEFAULT_COLLECTION_CONFIG: IBCollectionConfig = {
37
- arrayDeletionStrategy: EBArrayDeletionStrategy.IN_PLACE,
38
- autoIDStrategy: EBAutoIDStrategy.AUTO_ID_UUID,
39
- duplicateIDStrategy: EBDuplicateIDStrategy.OVERWRITE,
40
- memStorageStrategy: EBMemStorageStrategy.MAP,
41
- dirtyUnloadStrategy: EBDirtyUnloadStrategy.FLUSH,
42
- autoFlushStrategy: EBAutoFlushStrategy.ALWAYS_AUTO_FLUSH,
43
- };
44
-
45
- /** {@link IBDatabaseConfig} used when `getDatabase()` creates a fresh database. */
46
- export const DEFAULT_DATABASE_CONFIG: IBDatabaseConfig = {
47
- defaultCollectionConfig: DEFAULT_COLLECTION_CONFIG,
48
- };
49
-
50
- export class BDatabase implements IBDatabase {
51
- public readonly config: IBDatabaseConfig;
52
- private collections: Map<string, BCollection> = new Map();
53
- readonly io: IODatabase;
54
- readonly functionRegistry?: IBFunctionRegistry;
55
-
56
- public constructor(io: IODatabase, config?: IBDatabaseConfig, functionRegistry?: IBFunctionRegistry) {
57
- this.config = config ?? DEFAULT_DATABASE_CONFIG;
58
- this.io = io;
59
- this.functionRegistry = functionRegistry;
60
- }
61
-
62
- public addCollection(collection: BCollection): void {
63
- this.collections.set(collection.collectionName, collection);
64
- }
65
-
66
- public async getCollection<T>(collectionName: string, collectionConfig?: IBCollectionConfig): Promise<IBCollection<T>> {
67
- return runReentrantProtected(MutexKeys.DATABASE, async () => {
68
- const cname = canonicalName(collectionName);
69
- let ret: BCollection<T> | undefined = this.collections.get(cname);
70
- if (!ret) {
71
- ret = await this.createCollection(collectionName, collectionConfig);
72
- }
73
- return ret;
74
- });
75
- }
76
-
77
- public async createCollection<T = any>(collectionName: string, collectionConfig?: IBCollectionConfig): Promise<BCollection<T>> {
78
- return runReentrantProtected(MutexKeys.DATABASE, async () => {
79
- const cname = canonicalName(collectionName);
80
- if (this.collections.has(cname)) {
81
- throw new Error(`cannot create already existing collection ${collectionName}`);
82
- }
83
- const id = uuidv7();
84
- const ret: BCollection<T> = new BCollection<T>(this, id, collectionName, collectionConfig ?? this.config.defaultCollectionConfig);
85
- this.addCollection(ret);
86
- return this.io.createCollection<T>(ret);
87
- });
88
- }
89
-
90
- public async hasCollection(collectionName: string): Promise<boolean> {
91
- return runReentrantProtected(MutexKeys.DATABASE, async () => {
92
- return this.collections.has(canonicalName(collectionName));
93
- });
94
- }
95
-
96
- public async dropCollection(collectionName: string): Promise<void> {
97
- const cname = canonicalName(collectionName);
98
- return runMultiReentrantProtected([MutexKeys.DATABASE, cname], async () => {
99
- const coll: BCollection = this.collections.get(cname) as BCollection;
100
- if (coll) {
101
- coll.markDropped();
102
- this.collections.delete(cname);
103
- return this.io.deleteCollection(cname);
104
- }
105
- else {
106
- throw new Error(`no such collection ${collectionName}`);
107
- }
108
- });
109
- }
110
-
111
- }
112
-
113
- export class BCollection<T = any> implements IBCollection<T> {
114
- readonly database: BDatabase;
115
- readonly collectionConfig: IBCollectionConfig;
116
- readonly collectionName: string;
117
- readonly collectionID: string;
118
-
119
- private mapData: Map<string, T> = new Map();
120
- private arrayData: Array<T> = [];
121
- private loaded: boolean = false;
122
- private dirty: boolean = false;
123
- private dropped: boolean = false;
124
-
125
- public constructor(database: BDatabase, collectionID: string, collectionName: string, collectionConfig: IBCollectionConfig) {
126
- this.database = database;
127
- this.collectionID = collectionID;
128
- this.collectionName = canonicalName(collectionName);
129
- this.collectionConfig = collectionConfig;
130
- switch (collectionConfig.memStorageStrategy) {
131
- case EBMemStorageStrategy.MAP:
132
- // if (!collectionConfig.getID || !collectionConfig.setID) {
133
- // throw new Error("need id accessors configured when using map mem storage");
134
- // }
135
- break;
136
- default:
137
- case EBMemStorageStrategy.ARRAY:
138
- break;
139
- }
140
- }
141
-
142
- private async checkLoaded(): Promise<void> {
143
- if (!this.loaded) {
144
- await this.reload();
145
- }
146
- }
147
-
148
- private async runProtected<R>(fn: () => Promise<R>): Promise<R> {
149
- return runReentrantProtected(this.collectionName, async () => {
150
- if (this.dropped) {
151
- throw new Error(`collection "${this.collectionName}" has been dropped`);
152
- }
153
- return fn();
154
- });
155
- }
156
-
157
- /**
158
- * call this only within the proteing mutex
159
- */
160
- public markDropped(): void {
161
- this.dropped = true;
162
- }
163
-
164
- public async get(predicate?: (v: T) => boolean): Promise<T[]> {
165
- return this.runProtected(async () => {
166
- await this.checkLoaded();
167
- switch (this.collectionConfig.memStorageStrategy) {
168
- case EBMemStorageStrategy.MAP:
169
- return !predicate ?
170
- this.mapData.values().toArray() :
171
- this.mapData.values().filter(predicate).toArray();
172
- default:
173
- case EBMemStorageStrategy.ARRAY:
174
- return !predicate ?
175
- // MARK this may be a bit slow for bigger collections
176
- [...this.arrayData] :
177
- this.arrayData.filter(predicate);
178
- }
179
- });
180
- }
181
-
182
- public async delete(predicate: (v: T) => boolean): Promise<number> {
183
- return this.runProtected(async () => {
184
- await this.checkLoaded();
185
- let ret = 0;
186
- switch (this.collectionConfig.memStorageStrategy) {
187
- case EBMemStorageStrategy.MAP:
188
- for (const [key, value] of this.mapData) {
189
- if (predicate(value)) {
190
- ret++;
191
- this.mapData.delete(key);
192
- }
193
- }
194
- break;
195
- default:
196
- case EBMemStorageStrategy.ARRAY:
197
- switch (this.collectionConfig.arrayDeletionStrategy) {
198
- case EBArrayDeletionStrategy.IN_PLACE:
199
- let writeIndex = 0;
200
- for (let readIndex = 0; readIndex < this.arrayData.length; readIndex++) {
201
- const item = this.arrayData[readIndex];
202
- if (!predicate(item!)) {
203
- this.arrayData[writeIndex] = item!;
204
- writeIndex++;
205
- }
206
- else {
207
- ret++;
208
- }
209
- }
210
- this.arrayData.length = writeIndex;
211
- break;
212
- default:
213
- case EBArrayDeletionStrategy.NEW_ARRAY:
214
- const ol = this.arrayData.length;
215
- this.arrayData = this.arrayData.filter(v => !predicate(v));
216
- ret += (ol - this.arrayData.length);
217
- break;
218
- }
219
- break;
220
- }
221
- if (ret > 0) {
222
- await this.doOnDirty();
223
- }
224
- return ret;
225
- });
226
- }
227
-
228
- public async has(predicate: (v: T) => boolean): Promise<boolean> {
229
- return this.runProtected(async () => {
230
- await this.checkLoaded();
231
- switch (this.collectionConfig.memStorageStrategy) {
232
- case EBMemStorageStrategy.MAP:
233
- return this.mapData.values().find(predicate) !== undefined;
234
- default:
235
- case EBMemStorageStrategy.ARRAY:
236
- return this.arrayData.find(predicate) !== undefined;
237
- }
238
- });
239
- }
240
-
241
- private async getID(v: T): Promise<string> {
242
- let getter: TGetIDFunction;
243
- if (this.collectionConfig.getID) {
244
- const found = this.database.functionRegistry?.fetch(this.collectionConfig.getID) as TGetIDFunction | undefined;
245
- if (!found) {
246
- throw new Error(`getID function "${this.collectionConfig.getID}" is not registered`);
247
- }
248
- getter = found;
249
- }
250
- else {
251
- getter = DEFAULT_GET_ID;
252
- }
253
- let id = await getter(v);
254
- if (!id) {
255
- switch (this.collectionConfig.autoIDStrategy) {
256
- case EBAutoIDStrategy.NO_AUTO_ID:
257
- throw new Error("cannot omit auto-ids and than trying to add a value without an id");
258
- default:
259
- case EBAutoIDStrategy.AUTO_ID_UUID:
260
- id = uuidv7();
261
- let setter: TSetIDFunction;
262
- if (this.collectionConfig.setID) {
263
- const found = this.database.functionRegistry?.fetch(this.collectionConfig.setID) as TSetIDFunction | undefined;
264
- if (!found) {
265
- throw new Error(`setID function "${this.collectionConfig.setID}" is not registered`);
266
- }
267
- setter = found;
268
- }
269
- else {
270
- setter = DEFAULT_SET_ID;
271
- }
272
- await setter(v, id);
273
- break;
274
- }
275
- }
276
- return id;
277
- }
278
-
279
- public async add(...vs: T[]): Promise<number> {
280
- return this.runProtected(async () => {
281
- await this.checkLoaded();
282
- switch (this.collectionConfig.memStorageStrategy) {
283
- case EBMemStorageStrategy.MAP:
284
- let ret = 0;
285
- for (const v of vs) {
286
- const id = await this.getID(v);
287
- if (this.mapData.has(id)) {
288
- switch (this.collectionConfig.duplicateIDStrategy) {
289
- case EBDuplicateIDStrategy.ERROR:
290
- throw new Error(`duplicate id ${id}`);
291
- case EBDuplicateIDStrategy.IGNORE:
292
- // ignore the new value
293
- break;
294
- default:
295
- case EBDuplicateIDStrategy.OVERWRITE:
296
- // replace the new value
297
- ret++;
298
- this.mapData.set(id, v);
299
- break;
300
- }
301
- }
302
- else {
303
- ret++;
304
- this.mapData.set(id, v);
305
- }
306
- }
307
- if (ret > 0) {
308
- await this.doOnDirty();
309
- }
310
- return ret;
311
- default:
312
- case EBMemStorageStrategy.ARRAY:
313
- switch (this.collectionConfig.duplicateIDStrategy) {
314
- case EBDuplicateIDStrategy.ERROR:
315
- const haveIDs = new Set<string>();
316
- for (const v of this.arrayData) {
317
- haveIDs.add(await this.getID(v));
318
- }
319
- for (const v of vs) {
320
- const id = await this.getID(v);
321
- if (haveIDs.has(id)) {
322
- throw new Error(`duplicate id ${id}`);
323
- }
324
- haveIDs.add(id);
325
- this.arrayData.push(v);
326
- }
327
- break;
328
- case EBDuplicateIDStrategy.IGNORE:
329
- const seenIDs = new Set<string>();
330
- for (const v of this.arrayData) {
331
- seenIDs.add(await this.getID(v));
332
- }
333
- for (const v of vs) {
334
- const id = await this.getID(v);
335
- if (!seenIDs.has(id)) {
336
- seenIDs.add(id);
337
- this.arrayData.push(v);
338
- }
339
- }
340
- break;
341
- default:
342
- case EBDuplicateIDStrategy.OVERWRITE:
343
- const indexMap = new Map<string | number, number>();
344
- for (let i = 0; i < this.arrayData.length; i++) {
345
- indexMap.set(await this.getID(this.arrayData[i]!), i);
346
- }
347
- for (const v of vs) {
348
- const id = await this.getID(v);
349
- const existingIndex = indexMap.get(id);
350
-
351
- if (existingIndex !== undefined) {
352
- this.arrayData[existingIndex] = v;
353
- }
354
- else {
355
- // store new index in indexCache in the case this id appears again
356
- indexMap.set(id, this.arrayData.length);
357
- this.arrayData.push(v);
358
- }
359
- }
360
- break;
361
- }
362
- if (vs.length > 0) {
363
- await this.doOnDirty();
364
- }
365
- return vs.length;
366
- }
367
- });
368
- }
369
-
370
- public async update(predicate: (v: T) => boolean, updater: (v: T) => Promise<T>): Promise<number> {
371
- return this.runProtected(async () => {
372
- await this.checkLoaded();
373
- let ret = 0;
374
- switch (this.collectionConfig.memStorageStrategy) {
375
- case EBMemStorageStrategy.MAP:
376
- for (const [key, value] of this.mapData) {
377
- if (predicate(value)) {
378
- const replacement = await updater(value);
379
- if (replacement !== undefined) {
380
- ret++;
381
- this.mapData.set(key, replacement);
382
- }
383
- else {
384
- throw new Error("updater gave undefined");
385
- }
386
- }
387
- }
388
- break;
389
- default:
390
- case EBMemStorageStrategy.ARRAY:
391
- for (let i = 0; i < this.arrayData.length; i++) {
392
- if (predicate(this.arrayData[i]!)) {
393
- const replacement = await updater(this.arrayData[i]!);
394
- if (replacement !== undefined) {
395
- ret++;
396
- this.arrayData[i] = replacement;
397
- }
398
- else {
399
- throw new Error("updater gave undefined");
400
- }
401
- }
402
- }
403
- break;
404
- }
405
- if (ret > 0) {
406
- await this.doOnDirty();
407
- }
408
- return ret;
409
- });
410
- }
411
-
412
- private async doOnDirty(): Promise<void> {
413
- this.dirty = true;
414
- if (this.collectionConfig.autoFlushStrategy == EBAutoFlushStrategy.ALWAYS_AUTO_FLUSH) {
415
- await this.flush();
416
- }
417
- }
418
-
419
- public async flush(): Promise<void> {
420
- return this.runProtected(async () => {
421
- if (this.loaded) {
422
- switch (this.collectionConfig.memStorageStrategy) {
423
- case EBMemStorageStrategy.MAP:
424
- await this.database.io.writeMapData(this.collectionID, this.mapData);
425
- break;
426
- default:
427
- case EBMemStorageStrategy.ARRAY:
428
- await this.database.io.writeArrayData(this.collectionID, this.arrayData);
429
- break;
430
- }
431
- this.dirty = false;
432
- }
433
- });
434
- }
435
-
436
- public async reload(): Promise<void> {
437
- return this.runProtected(async () => {
438
- switch (this.collectionConfig.memStorageStrategy) {
439
- case EBMemStorageStrategy.MAP:
440
- this.mapData = await this.database.io.loadMapData<T>(this.collectionID);
441
- break;
442
- default:
443
- case EBMemStorageStrategy.ARRAY:
444
- this.arrayData = await this.database.io.loadArrayData<T>(this.collectionID);
445
- break;
446
- }
447
- this.loaded = true;
448
- this.dirty = false;
449
- });
450
- }
451
-
452
- public async unload(): Promise<void> {
453
- return this.runProtected(async () => {
454
- if (this.dirty) {
455
- switch (this.collectionConfig.dirtyUnloadStrategy) {
456
- case EBDirtyUnloadStrategy.FLUSH:
457
- await this.flush();
458
- break;
459
- case EBDirtyUnloadStrategy.ERROR:
460
- throw new Error("cannot unload unwritten collection");
461
- default:
462
- case EBDirtyUnloadStrategy.IGNORE:
463
- // do nothing
464
- break;
465
-
466
- }
467
- }
468
- switch (this.collectionConfig.memStorageStrategy) {
469
- case EBMemStorageStrategy.MAP:
470
- this.mapData = new Map();
471
- break;
472
- default:
473
- case EBMemStorageStrategy.ARRAY:
474
- this.arrayData = [];
475
- break;
476
- }
477
- this.loaded = false;
478
- this.dirty = false;
479
- });
480
- }
481
-
1
+ import logger from "winston";
2
+
3
+ import { uuidv7, } from "uuidv7";
4
+ import { runMultiReentrantProtected, runReentrantProtected } from "@incoqnito.io/ts-iq-core";
5
+
6
+ import type { IODatabase } from "./fileio.js";
7
+
8
+ import {
9
+ EBArrayDeletionStrategy,
10
+ EBAutoFlushStrategy,
11
+ EBAutoIDStrategy,
12
+ EBDirtyUnloadStrategy,
13
+ EBDuplicateIDStrategy,
14
+ EBHandlerMode,
15
+ EBMemStorageStrategy,
16
+ EBModificationType,
17
+ IBCollection,
18
+ IBCollectionConfig,
19
+ IBDatabase,
20
+ IBDatabaseConfig,
21
+ IBFunctionRegistry,
22
+ IHandlerRegistration,
23
+ TAddHandler,
24
+ TDeleteHandler,
25
+ TGetIDFunction,
26
+ TIndexKeyFunction,
27
+ TSetIDFunction,
28
+ TUpdateHandler,
29
+ } from "./base.js";
30
+ import { canonicalName } from "./utils.js";
31
+
32
+ /** Default {@link TGetIDFunction}: reads `v.id`, `undefined` if absent. */
33
+ export const DEFAULT_GET_ID: TGetIDFunction = (v: any) => ("id" in v) ? v.id : undefined;
34
+ /** Default {@link TSetIDFunction}: assigns to `v.id`. */
35
+ export const DEFAULT_SET_ID: TSetIDFunction = async (v: any, id: string) => { v.id = id; };
36
+
37
+ const enum MutexKeys {
38
+ // because of the spaces this will never be a collection name and as such is safe to use as a mutex key
39
+ DATABASE = " database ",
40
+ }
41
+
42
+ /** {@link IBCollectionConfig} used when a collection is created without an explicit one. */
43
+ export const DEFAULT_COLLECTION_CONFIG: IBCollectionConfig = {
44
+ arrayDeletionStrategy: EBArrayDeletionStrategy.IN_PLACE,
45
+ autoIDStrategy: EBAutoIDStrategy.AUTO_ID_UUID,
46
+ duplicateIDStrategy: EBDuplicateIDStrategy.OVERWRITE,
47
+ memStorageStrategy: EBMemStorageStrategy.MAP,
48
+ dirtyUnloadStrategy: EBDirtyUnloadStrategy.FLUSH,
49
+ autoFlushStrategy: EBAutoFlushStrategy.ALWAYS_AUTO_FLUSH,
50
+ };
51
+
52
+ /** {@link IBDatabaseConfig} used when `getDatabase()` creates a fresh database. */
53
+ export const DEFAULT_DATABASE_CONFIG: IBDatabaseConfig = {
54
+ defaultCollectionConfig: DEFAULT_COLLECTION_CONFIG,
55
+ };
56
+
57
+ export class BDatabase implements IBDatabase {
58
+ public readonly config: IBDatabaseConfig;
59
+ private collections: Map<string, BCollection> = new Map();
60
+ readonly io: IODatabase;
61
+ readonly functionRegistry?: IBFunctionRegistry;
62
+
63
+ public constructor(io: IODatabase, config?: IBDatabaseConfig, functionRegistry?: IBFunctionRegistry) {
64
+ this.config = config ?? DEFAULT_DATABASE_CONFIG;
65
+ this.io = io;
66
+ this.functionRegistry = functionRegistry;
67
+ }
68
+
69
+ public addCollection(collection: BCollection): void {
70
+ this.collections.set(collection.collectionName, collection);
71
+ }
72
+
73
+ public async getCollection<T>(collectionName: string, collectionConfig?: IBCollectionConfig): Promise<IBCollection<T>> {
74
+ return runReentrantProtected(MutexKeys.DATABASE, async () => {
75
+ const cname = canonicalName(collectionName);
76
+ let ret: BCollection<T> | undefined = this.collections.get(cname);
77
+ if (!ret) {
78
+ ret = await this.createCollection(collectionName, collectionConfig);
79
+ }
80
+ return ret;
81
+ });
82
+ }
83
+
84
+ public async createCollection<T = any>(collectionName: string, collectionConfig?: IBCollectionConfig): Promise<BCollection<T>> {
85
+ return runReentrantProtected(MutexKeys.DATABASE, async () => {
86
+ const cname = canonicalName(collectionName);
87
+ if (this.collections.has(cname)) {
88
+ throw new Error(`cannot create already existing collection ${collectionName}`);
89
+ }
90
+ const id = uuidv7();
91
+ const ret: BCollection<T> = new BCollection<T>(this, id, collectionName, collectionConfig ?? this.config.defaultCollectionConfig);
92
+ this.addCollection(ret);
93
+ return this.io.createCollection<T>(ret);
94
+ });
95
+ }
96
+
97
+ public async hasCollection(collectionName: string): Promise<boolean> {
98
+ return runReentrantProtected(MutexKeys.DATABASE, async () => {
99
+ return this.collections.has(canonicalName(collectionName));
100
+ });
101
+ }
102
+
103
+ public async dropCollection(collectionName: string): Promise<void> {
104
+ const cname = canonicalName(collectionName);
105
+ return runMultiReentrantProtected([MutexKeys.DATABASE, cname], async () => {
106
+ const coll: BCollection = this.collections.get(cname) as BCollection;
107
+ if (coll) {
108
+ coll.markDropped();
109
+ this.collections.delete(cname);
110
+ return this.io.deleteCollection(cname);
111
+ }
112
+ else {
113
+ throw new Error(`no such collection ${collectionName}`);
114
+ }
115
+ });
116
+ }
117
+
118
+ }
119
+
120
+ export class BCollection<T = any> implements IBCollection<T> {
121
+ readonly database: BDatabase;
122
+ readonly collectionConfig: IBCollectionConfig;
123
+ readonly collectionName: string;
124
+ readonly collectionID: string;
125
+
126
+ private mapData: Map<string, T> = new Map();
127
+ private arrayData: Array<T> = [];
128
+ private loaded: boolean = false;
129
+ private dirty: boolean = false;
130
+ private dropped: boolean = false;
131
+
132
+ private addHandlers: Array<{ handler: TAddHandler<T>; mode: EBHandlerMode }> = [];
133
+ private updateHandlers: Array<{ handler: TUpdateHandler<T>; mode: EBHandlerMode }> = [];
134
+ private deleteHandlers: Array<{ handler: TDeleteHandler<T>; mode: EBHandlerMode }> = [];
135
+
136
+ private indexDefinitions: Map<string, TIndexKeyFunction<T>> = new Map();
137
+ private indexRuntime: Map<string, Map<string, T[]>> = new Map();
138
+
139
+ public constructor(database: BDatabase, collectionID: string, collectionName: string, collectionConfig: IBCollectionConfig) {
140
+ this.database = database;
141
+ this.collectionID = collectionID;
142
+ this.collectionName = canonicalName(collectionName);
143
+ this.collectionConfig = collectionConfig;
144
+ switch (collectionConfig.memStorageStrategy) {
145
+ case EBMemStorageStrategy.MAP:
146
+ // if (!collectionConfig.getID || !collectionConfig.setID) {
147
+ // throw new Error("need id accessors configured when using map mem storage");
148
+ // }
149
+ break;
150
+ default:
151
+ case EBMemStorageStrategy.ARRAY:
152
+ break;
153
+ }
154
+ }
155
+
156
+ private async checkLoaded(): Promise<void> {
157
+ if (!this.loaded) {
158
+ await this.reload();
159
+ }
160
+ }
161
+
162
+ private async runExclusive<R>(fn: () => Promise<R>): Promise<R> {
163
+ return runReentrantProtected(this.collectionName, fn);
164
+ }
165
+
166
+ private async runProtected<R>(fn: () => Promise<R>): Promise<R> {
167
+ return this.runExclusive(async () => {
168
+ if (this.dropped) {
169
+ throw new Error(`collection "${this.collectionName}" has been dropped`);
170
+ }
171
+ return fn();
172
+ });
173
+ }
174
+
175
+ /**
176
+ * call this only within the protecting mutex
177
+ */
178
+ public markDropped(): void {
179
+ this.dropped = true;
180
+ }
181
+
182
+ public async get(predicate?: (v: T) => boolean): Promise<T[]> {
183
+ return this.runProtected(async () => {
184
+ await this.checkLoaded();
185
+ switch (this.collectionConfig.memStorageStrategy) {
186
+ case EBMemStorageStrategy.MAP:
187
+ return !predicate ?
188
+ this.mapData.values().toArray() :
189
+ this.mapData.values().filter(predicate).toArray();
190
+ default:
191
+ case EBMemStorageStrategy.ARRAY:
192
+ return !predicate ?
193
+ this.arrayData.slice() :
194
+ this.arrayData.filter(predicate);
195
+ }
196
+ });
197
+ }
198
+
199
+ public async count(predicate?: (v: T) => boolean): Promise<number> {
200
+ return this.runProtected(async () => {
201
+ await this.checkLoaded();
202
+ switch (this.collectionConfig.memStorageStrategy) {
203
+ case EBMemStorageStrategy.MAP:
204
+ return !predicate ?
205
+ this.mapData.size :
206
+ this.mapData.values().filter(predicate).reduce((n) => n + 1, 0);
207
+ default:
208
+ case EBMemStorageStrategy.ARRAY:
209
+ return !predicate ?
210
+ this.arrayData.length :
211
+ this.arrayData.filter(predicate).length;
212
+ }
213
+ });
214
+ }
215
+
216
+ public async getByIndex(indexKey: string, key: string): Promise<T[]> {
217
+ return this.runProtected(async () => {
218
+ await this.checkLoaded();
219
+ const byKey = this.indexRuntime.get(indexKey);
220
+ if (!byKey) {
221
+ throw new Error(`index "${indexKey}" is not configured`);
222
+ }
223
+ return byKey.get(key)?.slice() ?? [];
224
+ });
225
+ }
226
+
227
+ private async invokeHandler(fn: () => void | Promise<void>, modificationType: EBModificationType): Promise<void> {
228
+ try {
229
+ await fn();
230
+ }
231
+ catch (error) {
232
+ logger.error(`bajadab: collection "${this.collectionName}" ${modificationType} handler failed`, error);
233
+ }
234
+ }
235
+
236
+ private async fireAdd(added: T[]): Promise<void> {
237
+ for (const v of added) {
238
+ for (const { handler, mode } of this.addHandlers) {
239
+ const call = this.invokeHandler(() => handler(v), EBModificationType.ADD);
240
+ if (mode === EBHandlerMode.SYNC) {
241
+ await call;
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ private async fireUpdate(pairs: Array<[T, T]>): Promise<void> {
248
+ for (const [previous, updated] of pairs) {
249
+ for (const { handler, mode } of this.updateHandlers) {
250
+ const call = this.invokeHandler(() => handler(previous, updated), EBModificationType.UPDATE);
251
+ if (mode === EBHandlerMode.SYNC) {
252
+ await call;
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ private async fireDelete(deleted: T[]): Promise<void> {
259
+ for (const v of deleted) {
260
+ for (const { handler, mode } of this.deleteHandlers) {
261
+ const call = this.invokeHandler(() => handler(v), EBModificationType.DELETE);
262
+ if (mode === EBHandlerMode.SYNC) {
263
+ await call;
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ public async on(modificationType: EBModificationType.ADD, handler: TAddHandler<T>, mode?: EBHandlerMode): Promise<IHandlerRegistration>;
270
+ public async on(modificationType: EBModificationType.UPDATE, handler: TUpdateHandler<T>, mode?: EBHandlerMode): Promise<IHandlerRegistration>;
271
+ public async on(modificationType: EBModificationType.DELETE, handler: TDeleteHandler<T>, mode?: EBHandlerMode): Promise<IHandlerRegistration>;
272
+ public async on(modificationType: EBModificationType, handler: TAddHandler<T> | TUpdateHandler<T> | TDeleteHandler<T>, mode: EBHandlerMode = EBHandlerMode.SYNC): Promise<IHandlerRegistration> {
273
+ return this.runProtected(async () => {
274
+ switch (modificationType) {
275
+ case EBModificationType.ADD: {
276
+ const h = handler as TAddHandler<T>;
277
+ this.addHandlers.push({ handler: h, mode });
278
+ return { cancel: async () => this.runExclusive(async () => { this.addHandlers = this.addHandlers.filter(x => x.handler !== h); }) };
279
+ }
280
+ case EBModificationType.UPDATE: {
281
+ const h = handler as TUpdateHandler<T>;
282
+ this.updateHandlers.push({ handler: h, mode });
283
+ return { cancel: async () => this.runExclusive(async () => { this.updateHandlers = this.updateHandlers.filter(x => x.handler !== h); }) };
284
+ }
285
+ case EBModificationType.DELETE: {
286
+ const h = handler as TDeleteHandler<T>;
287
+ this.deleteHandlers.push({ handler: h, mode });
288
+ return { cancel: async () => this.runExclusive(async () => { this.deleteHandlers = this.deleteHandlers.filter(x => x.handler !== h); }) };
289
+ }
290
+ default:
291
+ throw new Error(`unknown modification type ${modificationType}`);
292
+ }
293
+ });
294
+ }
295
+
296
+ public async delete(predicate: (v: T) => boolean): Promise<number> {
297
+ return this.runProtected(async () => {
298
+ await this.checkLoaded();
299
+ let ret = 0;
300
+ const trackDeletes = this.deleteHandlers.length > 0;
301
+ const indexDeletes = this.indexDefinitions.size > 0;
302
+ const deleted: T[] = [];
303
+ switch (this.collectionConfig.memStorageStrategy) {
304
+ case EBMemStorageStrategy.MAP:
305
+ for (const [key, value] of this.mapData) {
306
+ if (predicate(value)) {
307
+ ret++;
308
+ if (indexDeletes) {
309
+ await this.removeFromIndexes(value);
310
+ }
311
+ this.mapData.delete(key);
312
+ if (trackDeletes) {
313
+ deleted.push(value);
314
+ }
315
+ }
316
+ }
317
+ break;
318
+ default:
319
+ case EBMemStorageStrategy.ARRAY:
320
+ switch (this.collectionConfig.arrayDeletionStrategy) {
321
+ case EBArrayDeletionStrategy.IN_PLACE:
322
+ let writeIndex = 0;
323
+ for (let readIndex = 0; readIndex < this.arrayData.length; readIndex++) {
324
+ const item = this.arrayData[readIndex];
325
+ if (!predicate(item!)) {
326
+ this.arrayData[writeIndex] = item!;
327
+ writeIndex++;
328
+ }
329
+ else {
330
+ ret++;
331
+ if (indexDeletes) {
332
+ await this.removeFromIndexes(item!);
333
+ }
334
+ if (trackDeletes) {
335
+ deleted.push(item!);
336
+ }
337
+ }
338
+ }
339
+ this.arrayData.length = writeIndex;
340
+ break;
341
+ default:
342
+ case EBArrayDeletionStrategy.NEW_ARRAY:
343
+ const ol = this.arrayData.length;
344
+ if (trackDeletes || indexDeletes) {
345
+ const removed = this.arrayData.filter(predicate);
346
+ if (trackDeletes) {
347
+ deleted.push(...removed);
348
+ }
349
+ if (indexDeletes) {
350
+ for (const v of removed) {
351
+ await this.removeFromIndexes(v);
352
+ }
353
+ }
354
+ }
355
+ this.arrayData = this.arrayData.filter(v => !predicate(v));
356
+ ret += (ol - this.arrayData.length);
357
+ break;
358
+ }
359
+ break;
360
+ }
361
+ if (ret > 0) {
362
+ await this.doOnDirty();
363
+ }
364
+ if (deleted.length > 0) {
365
+ await this.fireDelete(deleted);
366
+ }
367
+ return ret;
368
+ });
369
+ }
370
+
371
+ public async has(predicate: (v: T) => boolean): Promise<boolean> {
372
+ return this.runProtected(async () => {
373
+ await this.checkLoaded();
374
+ switch (this.collectionConfig.memStorageStrategy) {
375
+ case EBMemStorageStrategy.MAP:
376
+ return this.mapData.values().find(predicate) !== undefined;
377
+ default:
378
+ case EBMemStorageStrategy.ARRAY:
379
+ return this.arrayData.find(predicate) !== undefined;
380
+ }
381
+ });
382
+ }
383
+
384
+ private resolveIndexDefinitions(): void {
385
+ this.indexDefinitions = new Map();
386
+ if (!this.collectionConfig.indexes) {
387
+ return;
388
+ }
389
+ for (const [indexKey, registryKey] of Object.entries(this.collectionConfig.indexes)) {
390
+ const extractor = this.database.functionRegistry?.fetch(registryKey) as TIndexKeyFunction<T> | undefined;
391
+ if (!extractor) {
392
+ throw new Error(`index key function "${registryKey}" is not registered`);
393
+ }
394
+ this.indexDefinitions.set(indexKey, extractor);
395
+ }
396
+ }
397
+
398
+ private async indexKeysFor(v: T): Promise<Array<[string, string[]]>> {
399
+ const result: Array<[string, string[]]> = [];
400
+ for (const [indexKey, extractor] of this.indexDefinitions) {
401
+ const extracted = await extractor(v);
402
+ if (extracted === undefined) {
403
+ continue;
404
+ }
405
+ result.push([indexKey, Array.isArray(extracted) ? extracted : [extracted]]);
406
+ }
407
+ return result;
408
+ }
409
+
410
+ private async addToIndexes(v: T): Promise<void> {
411
+ for (const [indexKey, keys] of await this.indexKeysFor(v)) {
412
+ const byKey = this.indexRuntime.get(indexKey)!;
413
+ for (const key of keys) {
414
+ let bucket = byKey.get(key);
415
+ if (!bucket) {
416
+ bucket = [];
417
+ byKey.set(key, bucket);
418
+ }
419
+ bucket.push(v);
420
+ }
421
+ }
422
+ }
423
+
424
+ private async removeFromIndexes(v: T): Promise<void> {
425
+ for (const [indexKey, keys] of await this.indexKeysFor(v)) {
426
+ const byKey = this.indexRuntime.get(indexKey)!;
427
+ for (const key of keys) {
428
+ const bucket = byKey.get(key);
429
+ if (!bucket) {
430
+ continue;
431
+ }
432
+ const at = bucket.indexOf(v);
433
+ if (at !== -1) {
434
+ bucket.splice(at, 1);
435
+ }
436
+ if (bucket.length === 0) {
437
+ byKey.delete(key);
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ /**
444
+ * full rebuild from currently loaded data; only needed right after (re)load, since every
445
+ * value then is a freshly deserialized object the incremental add/update/delete hooks never saw
446
+ */
447
+ private async rebuildIndexRuntime(): Promise<void> {
448
+ // every configured index gets an entry up front, even if empty, so getByIndex()/
449
+ // addToIndexes()/removeFromIndexes() can assume it's there instead of checking twice
450
+ this.indexRuntime = new Map(Array.from(this.indexDefinitions.keys(), (indexKey) => [indexKey, new Map()]));
451
+ if (this.indexDefinitions.size === 0) {
452
+ return;
453
+ }
454
+ const values = this.collectionConfig.memStorageStrategy === EBMemStorageStrategy.MAP ?
455
+ [...this.mapData.values()] : this.arrayData;
456
+ for (const v of values) {
457
+ await this.addToIndexes(v);
458
+ }
459
+ }
460
+
461
+ private async getID(v: T): Promise<string> {
462
+ let getter: TGetIDFunction;
463
+ if (this.collectionConfig.getID) {
464
+ const found = this.database.functionRegistry?.fetch(this.collectionConfig.getID) as TGetIDFunction | undefined;
465
+ if (!found) {
466
+ throw new Error(`getID function "${this.collectionConfig.getID}" is not registered`);
467
+ }
468
+ getter = found;
469
+ }
470
+ else {
471
+ getter = DEFAULT_GET_ID;
472
+ }
473
+ let id = await getter(v);
474
+ if (!id) {
475
+ switch (this.collectionConfig.autoIDStrategy) {
476
+ case EBAutoIDStrategy.NO_AUTO_ID:
477
+ throw new Error("cannot omit auto-ids and than trying to add a value without an id");
478
+ default:
479
+ case EBAutoIDStrategy.AUTO_ID_UUID:
480
+ id = uuidv7();
481
+ let setter: TSetIDFunction;
482
+ if (this.collectionConfig.setID) {
483
+ const found = this.database.functionRegistry?.fetch(this.collectionConfig.setID) as TSetIDFunction | undefined;
484
+ if (!found) {
485
+ throw new Error(`setID function "${this.collectionConfig.setID}" is not registered`);
486
+ }
487
+ setter = found;
488
+ }
489
+ else {
490
+ setter = DEFAULT_SET_ID;
491
+ }
492
+ await setter(v, id);
493
+ break;
494
+ }
495
+ }
496
+ return id;
497
+ }
498
+
499
+ public async add(...vs: T[]): Promise<number> {
500
+ return this.runProtected(async () => {
501
+ await this.checkLoaded();
502
+ switch (this.collectionConfig.memStorageStrategy) {
503
+ case EBMemStorageStrategy.MAP:
504
+ let ret = 0;
505
+ const trackAddsMap = this.addHandlers.length > 0;
506
+ const indexAddsMap = this.indexDefinitions.size > 0;
507
+ const addedMap: T[] = [];
508
+ for (const v of vs) {
509
+ const id = await this.getID(v);
510
+ if (this.mapData.has(id)) {
511
+ switch (this.collectionConfig.duplicateIDStrategy) {
512
+ case EBDuplicateIDStrategy.ERROR:
513
+ throw new Error(`duplicate id ${id}`);
514
+ case EBDuplicateIDStrategy.IGNORE:
515
+ // ignore the new value
516
+ break;
517
+ default:
518
+ case EBDuplicateIDStrategy.OVERWRITE:
519
+ // replace the new value
520
+ ret++;
521
+ if (indexAddsMap) {
522
+ await this.removeFromIndexes(this.mapData.get(id)!);
523
+ }
524
+ this.mapData.set(id, v);
525
+ if (indexAddsMap) {
526
+ await this.addToIndexes(v);
527
+ }
528
+ if (trackAddsMap) {
529
+ addedMap.push(v);
530
+ }
531
+ break;
532
+ }
533
+ }
534
+ else {
535
+ ret++;
536
+ this.mapData.set(id, v);
537
+ if (indexAddsMap) {
538
+ await this.addToIndexes(v);
539
+ }
540
+ if (trackAddsMap) {
541
+ addedMap.push(v);
542
+ }
543
+ }
544
+ }
545
+ if (ret > 0) {
546
+ await this.doOnDirty();
547
+ }
548
+ if (addedMap.length > 0) {
549
+ await this.fireAdd(addedMap);
550
+ }
551
+ return ret;
552
+ default:
553
+ case EBMemStorageStrategy.ARRAY:
554
+ const trackAddsArray = this.addHandlers.length > 0;
555
+ const indexAddsArray = this.indexDefinitions.size > 0;
556
+ const addedArray: T[] = [];
557
+ switch (this.collectionConfig.duplicateIDStrategy) {
558
+ case EBDuplicateIDStrategy.ERROR:
559
+ const haveIDs = new Set<string>();
560
+ for (const v of this.arrayData) {
561
+ haveIDs.add(await this.getID(v));
562
+ }
563
+ for (const v of vs) {
564
+ const id = await this.getID(v);
565
+ if (haveIDs.has(id)) {
566
+ throw new Error(`duplicate id ${id}`);
567
+ }
568
+ haveIDs.add(id);
569
+ this.arrayData.push(v);
570
+ if (indexAddsArray) {
571
+ await this.addToIndexes(v);
572
+ }
573
+ if (trackAddsArray) {
574
+ addedArray.push(v);
575
+ }
576
+ }
577
+ break;
578
+ case EBDuplicateIDStrategy.IGNORE:
579
+ const seenIDs = new Set<string>();
580
+ for (const v of this.arrayData) {
581
+ seenIDs.add(await this.getID(v));
582
+ }
583
+ for (const v of vs) {
584
+ const id = await this.getID(v);
585
+ if (!seenIDs.has(id)) {
586
+ seenIDs.add(id);
587
+ this.arrayData.push(v);
588
+ if (indexAddsArray) {
589
+ await this.addToIndexes(v);
590
+ }
591
+ if (trackAddsArray) {
592
+ addedArray.push(v);
593
+ }
594
+ }
595
+ }
596
+ break;
597
+ default:
598
+ case EBDuplicateIDStrategy.OVERWRITE:
599
+ const indexMap = new Map<string | number, number>();
600
+ for (let i = 0; i < this.arrayData.length; i++) {
601
+ indexMap.set(await this.getID(this.arrayData[i]!), i);
602
+ }
603
+ for (const v of vs) {
604
+ const id = await this.getID(v);
605
+ const existingIndex = indexMap.get(id);
606
+
607
+ if (existingIndex !== undefined) {
608
+ if (indexAddsArray) {
609
+ await this.removeFromIndexes(this.arrayData[existingIndex]!);
610
+ }
611
+ this.arrayData[existingIndex] = v;
612
+ }
613
+ else {
614
+ // store new index in indexCache in the case this id appears again
615
+ indexMap.set(id, this.arrayData.length);
616
+ this.arrayData.push(v);
617
+ }
618
+ if (indexAddsArray) {
619
+ await this.addToIndexes(v);
620
+ }
621
+ if (trackAddsArray) {
622
+ addedArray.push(v);
623
+ }
624
+ }
625
+ break;
626
+ }
627
+ if (vs.length > 0) {
628
+ await this.doOnDirty();
629
+ }
630
+ if (addedArray.length > 0) {
631
+ await this.fireAdd(addedArray);
632
+ }
633
+ return vs.length;
634
+ }
635
+ });
636
+ }
637
+
638
+ public async update(predicate: (v: T) => boolean, updater: (v: T) => Promise<T>): Promise<number> {
639
+ return this.runProtected(async () => {
640
+ await this.checkLoaded();
641
+ let ret = 0;
642
+ const trackUpdates = this.updateHandlers.length > 0;
643
+ const indexUpdates = this.indexDefinitions.size > 0;
644
+ const updated: Array<[T, T]> = [];
645
+ switch (this.collectionConfig.memStorageStrategy) {
646
+ case EBMemStorageStrategy.MAP:
647
+ for (const [key, value] of this.mapData) {
648
+ if (predicate(value)) {
649
+ const replacement = await updater(value);
650
+ if (replacement !== undefined) {
651
+ ret++;
652
+ if (indexUpdates) {
653
+ await this.removeFromIndexes(value);
654
+ }
655
+ this.mapData.set(key, replacement);
656
+ if (indexUpdates) {
657
+ await this.addToIndexes(replacement);
658
+ }
659
+ if (trackUpdates) {
660
+ updated.push([value, replacement]);
661
+ }
662
+ }
663
+ else {
664
+ throw new Error("updater gave undefined");
665
+ }
666
+ }
667
+ }
668
+ break;
669
+ default:
670
+ case EBMemStorageStrategy.ARRAY:
671
+ for (let i = 0; i < this.arrayData.length; i++) {
672
+ if (predicate(this.arrayData[i]!)) {
673
+ const previous = this.arrayData[i]!;
674
+ const replacement = await updater(previous);
675
+ if (replacement !== undefined) {
676
+ ret++;
677
+ if (indexUpdates) {
678
+ await this.removeFromIndexes(previous);
679
+ }
680
+ this.arrayData[i] = replacement;
681
+ if (indexUpdates) {
682
+ await this.addToIndexes(replacement);
683
+ }
684
+ if (trackUpdates) {
685
+ updated.push([previous, replacement]);
686
+ }
687
+ }
688
+ else {
689
+ throw new Error("updater gave undefined");
690
+ }
691
+ }
692
+ }
693
+ break;
694
+ }
695
+ if (ret > 0) {
696
+ await this.doOnDirty();
697
+ }
698
+ if (updated.length > 0) {
699
+ await this.fireUpdate(updated);
700
+ }
701
+ return ret;
702
+ });
703
+ }
704
+
705
+ private async doOnDirty(): Promise<void> {
706
+ this.dirty = true;
707
+ if (this.collectionConfig.autoFlushStrategy == EBAutoFlushStrategy.ALWAYS_AUTO_FLUSH) {
708
+ await this.flush();
709
+ }
710
+ }
711
+
712
+ public async flush(): Promise<void> {
713
+ return this.runProtected(async () => {
714
+ if (this.loaded) {
715
+ switch (this.collectionConfig.memStorageStrategy) {
716
+ case EBMemStorageStrategy.MAP:
717
+ await this.database.io.writeMapData(this.collectionID, this.mapData);
718
+ break;
719
+ default:
720
+ case EBMemStorageStrategy.ARRAY:
721
+ await this.database.io.writeArrayData(this.collectionID, this.arrayData);
722
+ break;
723
+ }
724
+ this.dirty = false;
725
+ }
726
+ });
727
+ }
728
+
729
+ public async reload(): Promise<void> {
730
+ return this.runProtected(async () => {
731
+ switch (this.collectionConfig.memStorageStrategy) {
732
+ case EBMemStorageStrategy.MAP:
733
+ this.mapData = await this.database.io.loadMapData<T>(this.collectionID);
734
+ break;
735
+ default:
736
+ case EBMemStorageStrategy.ARRAY:
737
+ this.arrayData = await this.database.io.loadArrayData<T>(this.collectionID);
738
+ break;
739
+ }
740
+ this.loaded = true;
741
+ this.dirty = false;
742
+ this.resolveIndexDefinitions();
743
+ await this.rebuildIndexRuntime();
744
+ });
745
+ }
746
+
747
+ public async unload(): Promise<void> {
748
+ return this.runProtected(async () => {
749
+ if (this.dirty) {
750
+ switch (this.collectionConfig.dirtyUnloadStrategy) {
751
+ case EBDirtyUnloadStrategy.FLUSH:
752
+ await this.flush();
753
+ break;
754
+ case EBDirtyUnloadStrategy.ERROR:
755
+ throw new Error("cannot unload unwritten collection");
756
+ default:
757
+ case EBDirtyUnloadStrategy.IGNORE:
758
+ // do nothing
759
+ break;
760
+
761
+ }
762
+ }
763
+ switch (this.collectionConfig.memStorageStrategy) {
764
+ case EBMemStorageStrategy.MAP:
765
+ this.mapData = new Map();
766
+ break;
767
+ default:
768
+ case EBMemStorageStrategy.ARRAY:
769
+ this.arrayData = [];
770
+ break;
771
+ }
772
+ this.loaded = false;
773
+ this.dirty = false;
774
+ });
775
+ }
776
+
482
777
  }