@incoqnito.io/bajadab 1.1.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/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@incoqnito.io/bajadab",
3
+ "version": "1.1.0",
4
+ "description": "a lightweight json-based database for small-scale single-machine setups",
5
+ "homepage": "https://bitbucket.org/incoqnito/bajadab#readme",
6
+ "bugs": {
7
+ "url": "https://bitbucket.org/incoqnito/bajadab/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+ssh://git@bitbucket.org/incoqnito/bajadab.git"
12
+ },
13
+ "license": "LicenseRef-LICENSE",
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "author": "Incoqnito GmbH",
18
+ "type": "module",
19
+ "main": "./dist/bajadab.js",
20
+ "types": "./dist/bajadab.d.ts",
21
+ "scripts": {
22
+ "prepublishOnly": "npm run build",
23
+ "clean": "rimraf dist",
24
+ "prebuild": "npm run clean",
25
+ "build": "tsc --project ./tsconfig.json",
26
+ "test": "node --import tsx --test-global-setup ./src/tests/tests.global.ts --test --enable-source-maps --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info --test-reporter=spec --test-reporter-destination=stdout ./src/tests/**/*.test.ts"
27
+ },
28
+ "engines": {
29
+ "node": ">= 24"
30
+ },
31
+ "dependencies": {
32
+ "@incoqnito.io/ts-iq-core": "^2.1.0",
33
+ "uuidv7": "^1.2.1"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^26.1.2",
37
+ "rimraf": "^6.1.3",
38
+ "tsx": "^4.23.1",
39
+ "typescript": "^7.0.2"
40
+ },
41
+ "overrides": {
42
+ "minimatch": "^10.2.6",
43
+ "glob": "^13.0.6"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "src/main"
48
+ ],
49
+ "allowScripts": {
50
+ "esbuild": true
51
+ }
52
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./base.js";
2
+ export { getDatabase } from "./fileio.js";
3
+ export { DEFAULT_COLLECTION_CONFIG, DEFAULT_DATABASE_CONFIG, DEFAULT_GET_ID, DEFAULT_SET_ID, } from "./basics.js";
@@ -0,0 +1,125 @@
1
+ /** How a missing id is handled on {@link IBCollection.add}. */
2
+ export enum EBAutoIDStrategy {
3
+ /** Reject values without an id. */
4
+ NO_AUTO_ID = "NO_AUTO_ID",
5
+ /** Generate a `uuidv7` and assign it via the configured setter. */
6
+ AUTO_ID_UUID = "AUTO_ID_UUID",
7
+ }
8
+
9
+ /** In-memory representation of a collection's data. */
10
+ export enum EBMemStorageStrategy {
11
+ /** Keyed by id; `add()` looks up existing entries by id in O(1). */
12
+ MAP = "MAP",
13
+ // LINKED_LIST = "LINKED_LIST",
14
+ /** Plain list; insertion order is preserved. */
15
+ ARRAY = "ARRAY",
16
+ }
17
+
18
+ /** What happens on {@link IBCollection.add} when a value's id already exists. */
19
+ export enum EBDuplicateIDStrategy {
20
+ /** Replace the existing value. */
21
+ OVERWRITE = "OVERWRITE",
22
+ /** Reject the whole call. */
23
+ ERROR = "ERROR",
24
+ /** Keep the existing value, drop the new one. */
25
+ IGNORE = "IGNORE",
26
+ }
27
+
28
+ /** How {@link IBCollection.delete} removes matched items from an `ARRAY` collection. */
29
+ export enum EBArrayDeletionStrategy {
30
+ /** Build a new array via `filter()`. */
31
+ NEW_ARRAY = "NEW_ARRAY",
32
+ /** Compact the existing array in place. */
33
+ IN_PLACE = "IN_PLACE",
34
+ }
35
+
36
+ /** When a collection is written to disk after a mutation. */
37
+ export enum EBAutoFlushStrategy {
38
+ /** Only on an explicit {@link IBCollection.flush} call. */
39
+ NO_AUTO_FLUSH = "NO_AUTO_FLUSH",
40
+ /** After every mutating call (`add`/`update`/`delete`). */
41
+ ALWAYS_AUTO_FLUSH = "ALWAYS_AUTO_FLUSH",
42
+ }
43
+
44
+ /** What {@link IBCollection.unload} does with unsaved changes. */
45
+ export enum EBDirtyUnloadStrategy {
46
+ /** Reject the unload. */
47
+ ERROR = "ERROR",
48
+ /** Flush first, then unload. */
49
+ FLUSH = "FLUSH",
50
+ /** Discard the unsaved changes silently. */
51
+ IGNORE = "IGNORE",
52
+ }
53
+
54
+ /** Resolves the id of a value; `undefined` means "no id yet". */
55
+ export type TGetIDFunction = (v: any) => Promise<string | undefined>;
56
+
57
+ /** Assigns an id onto a value. */
58
+ export type TSetIDFunction = (v: any, id: string) => Promise<void>;
59
+
60
+ /** Per-collection behavior. Passed to {@link IBDatabase.createCollection}/{@link IBDatabase.getCollection}. */
61
+ export interface IBCollectionConfig {
62
+ autoIDStrategy: EBAutoIDStrategy;
63
+ duplicateIDStrategy: EBDuplicateIDStrategy;
64
+ memStorageStrategy: EBMemStorageStrategy;
65
+ arrayDeletionStrategy: EBArrayDeletionStrategy;
66
+ dirtyUnloadStrategy: EBDirtyUnloadStrategy;
67
+ autoFlushStrategy: EBAutoFlushStrategy;
68
+ /** Registry key for a custom {@link TGetIDFunction}. Omit to use the default (reads `.id`). */
69
+ getID?: string;
70
+ /** Registry key for a custom {@link TSetIDFunction}. Omit to use the default (sets `.id`). */
71
+ setID?: string;
72
+ }
73
+
74
+ /** Database-wide configuration. */
75
+ export interface IBDatabaseConfig {
76
+ /** Used for any collection created without an explicit config. */
77
+ readonly defaultCollectionConfig: IBCollectionConfig;
78
+ }
79
+
80
+ /** A JSON-file-backed database. Obtain one via `getDatabase()`. */
81
+ export interface IBDatabase {
82
+ readonly config: IBDatabaseConfig;
83
+ /** Creates a new collection. Rejects if one with the same (canonicalized) name already exists. */
84
+ createCollection<T>(collectionName: string, collectionConfig?: IBCollectionConfig): Promise<IBCollection<T>>;
85
+ /** Returns the named collection, creating it with `collectionConfig` (or the database default) if it doesn't exist yet. */
86
+ getCollection<T>(collectionName: string, collectionConfig?: IBCollectionConfig): Promise<IBCollection<T>>;
87
+ /** Whether a collection with this (canonicalized) name exists. */
88
+ hasCollection(collectionName: string): Promise<boolean>;
89
+ /** Deletes a collection and invalidates any handle still held on it. Rejects if it doesn't exist. */
90
+ dropCollection(collectionName: string): Promise<void>;
91
+ }
92
+
93
+ /** A single collection of values of type `T`. Obtain one via {@link IBDatabase}. */
94
+ export interface IBCollection<T = any> {
95
+ readonly database: IBDatabase;
96
+ readonly collectionConfig: IBCollectionConfig;
97
+ /** Canonicalized (trimmed, lowercased) name. */
98
+ readonly collectionName: string;
99
+ /** Stable identity used to address this collection's storage, independent of its name. */
100
+ readonly collectionID: string;
101
+
102
+ /** All items, or only those matching `predicate`. Always a defensive copy, never the live storage. */
103
+ get(predicate?: (v: T) => boolean): Promise<T[]>;
104
+ /** Deletes matching items. Returns the number removed. */
105
+ delete(predicate: (v: T) => boolean): Promise<number>;
106
+ /** Whether any item matches `predicate`. */
107
+ has(predicate: (v: T) => boolean): Promise<boolean>;
108
+ /** Adds values, resolving/assigning ids per the collection's id strategies. Returns the number added. */
109
+ add(...v: T[]): Promise<number>;
110
+ /** Replaces each item matching `predicate` with the (defined) result of `updater`. Returns the number updated. */
111
+ update(predicate: (v: T) => boolean, updater: (v: T) => Promise<T>): Promise<number>;
112
+
113
+ /** Writes the current in-memory state to disk. */
114
+ flush(): Promise<void>;
115
+ /** Discards in-memory state and re-reads it from disk. */
116
+ reload(): Promise<void>;
117
+ /** Evicts the in-memory state (per the dirty-unload strategy if there are unsaved changes). */
118
+ unload(): Promise<void>;
119
+ }
120
+
121
+ /** Resolves the custom id functions referenced by {@link IBCollectionConfig.getID}/`setID`. */
122
+ export interface IBFunctionRegistry {
123
+ /** Returns the function registered under `key`, or `undefined` if there is none. */
124
+ fetch(key?: string): Function | undefined;
125
+ }
@@ -0,0 +1,482 @@
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
+
482
+ }