@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.
- package/README.md +30 -0
- package/dist/base.d.ts +43 -0
- package/dist/base.js +18 -0
- package/dist/base.js.map +1 -1
- package/dist/basics.d.ts +26 -2
- package/dist/basics.js +277 -6
- package/dist/basics.js.map +1 -1
- package/package.json +1 -1
- package/src/main/base.ts +47 -0
- package/src/main/basics.ts +776 -481
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ A lightweight JSON-file-backed database for small, single-machine setups. No ser
|
|
|
8
8
|
- `MAP` (keyed by id) or `ARRAY` (ordered list) in-memory storage per collection.
|
|
9
9
|
- Configurable behavior for duplicate ids, missing ids, array deletion, auto-flush, and unloading dirty data.
|
|
10
10
|
- Custom id extraction/assignment via a pluggable function registry.
|
|
11
|
+
- Optional secondary indexes for fast lookup by a derived key.
|
|
11
12
|
- Atomic writes (write-to-temp, then rename) and, on POSIX systems, owner-only file permissions (`0600` files, `0700` directories).
|
|
12
13
|
- In-process concurrency safety per collection and per database.
|
|
13
14
|
|
|
@@ -54,6 +55,7 @@ Every collection has an `IBCollectionConfig`. `db.config.defaultCollectionConfig
|
|
|
54
55
|
| `autoFlushStrategy` | `ALWAYS_AUTO_FLUSH` (default) / `NO_AUTO_FLUSH` | Whether every mutation is written to disk immediately, or only on an explicit `flush()`. |
|
|
55
56
|
| `dirtyUnloadStrategy` | `FLUSH` (default) / `ERROR` / `IGNORE` | What `unload()` does with unsaved changes. |
|
|
56
57
|
| `getID` / `setID` | registry key (optional) | Custom id accessors — see below. Omit to use the default, which reads/writes a plain `.id` property. |
|
|
58
|
+
| `indexes` | `{ [indexKey: string]: registry key }` (optional) | Secondary indexes — see below. Omit for no indexes. |
|
|
57
59
|
|
|
58
60
|
## Custom ids
|
|
59
61
|
|
|
@@ -82,6 +84,34 @@ const users = await db.createCollection("users", {
|
|
|
82
84
|
|
|
83
85
|
A `getID`/`setID` key that isn't found in the registry causes `add()` to reject, rather than silently falling back to the default.
|
|
84
86
|
|
|
87
|
+
## Indexes
|
|
88
|
+
|
|
89
|
+
A collection can declare secondary indexes for fast lookup by a derived key. Like `getID`/`setID`, the extractor function lives in the registry — only its key is part of the (persisted) collection config:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
class Registry implements IBFunctionRegistry {
|
|
93
|
+
private fns = new Map<string, Function>([
|
|
94
|
+
["byEmail", async (v: any) => v.email],
|
|
95
|
+
]);
|
|
96
|
+
fetch(key?: string) {
|
|
97
|
+
return key ? this.fns.get(key) : undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const db = await getDatabase("./data", new Registry());
|
|
102
|
+
const users = await db.createCollection("users", {
|
|
103
|
+
...db.config.defaultCollectionConfig,
|
|
104
|
+
indexes: { email: "byEmail" },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await users.add({ email: "a@example.com", name: "A" });
|
|
108
|
+
const matches = await users.getByIndex("email", "a@example.com");
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
An extractor returns a `string`, a `string[]` for a multi-valued index, or `undefined` to leave a value out of the index. `getByIndex()` rejects if `indexKey` isn't configured. A configured index whose registry key isn't found rejects on the collection's first use after it's (re)loaded — `get()`, `add()`, whatever comes first — not just the mutations that actually need indexing; that's a wider blast radius than `getID`/`setID`, which only ever fail inside `add()`.
|
|
112
|
+
|
|
113
|
+
The index is maintained incrementally: `add()`/`update()`/`delete()` update only the affected buckets, not the whole index. A full rebuild only happens once, right after `reload()` loads fresh data from disk.
|
|
114
|
+
|
|
85
115
|
## Persistence and file layout
|
|
86
116
|
|
|
87
117
|
Each database directory contains:
|
package/dist/base.d.ts
CHANGED
|
@@ -48,6 +48,8 @@ export declare enum EBDirtyUnloadStrategy {
|
|
|
48
48
|
export type TGetIDFunction = (v: any) => Promise<string | undefined>;
|
|
49
49
|
/** Assigns an id onto a value. */
|
|
50
50
|
export type TSetIDFunction = (v: any, id: string) => Promise<void>;
|
|
51
|
+
/** Extracts an index key (or several, for a multi-valued index) from a value; `undefined` means "not indexed". */
|
|
52
|
+
export type TIndexKeyFunction<T = any> = (v: T) => Promise<string | string[] | undefined>;
|
|
51
53
|
/** Per-collection behavior. Passed to {@link IBDatabase.createCollection}/{@link IBDatabase.getCollection}. */
|
|
52
54
|
export interface IBCollectionConfig {
|
|
53
55
|
autoIDStrategy: EBAutoIDStrategy;
|
|
@@ -60,6 +62,10 @@ export interface IBCollectionConfig {
|
|
|
60
62
|
getID?: string;
|
|
61
63
|
/** Registry key for a custom {@link TSetIDFunction}. Omit to use the default (sets `.id`). */
|
|
62
64
|
setID?: string;
|
|
65
|
+
/** Index name → registry key of a {@link TIndexKeyFunction} for that index. Omit for no indexes. */
|
|
66
|
+
indexes?: {
|
|
67
|
+
[indexKey: string]: string;
|
|
68
|
+
};
|
|
63
69
|
}
|
|
64
70
|
/** Database-wide configuration. */
|
|
65
71
|
export interface IBDatabaseConfig {
|
|
@@ -78,6 +84,33 @@ export interface IBDatabase {
|
|
|
78
84
|
/** Deletes a collection and invalidates any handle still held on it. Rejects if it doesn't exist. */
|
|
79
85
|
dropCollection(collectionName: string): Promise<void>;
|
|
80
86
|
}
|
|
87
|
+
/** What kind of collection mutation a handler subscribes to via {@link IBCollection.on}. */
|
|
88
|
+
export declare enum EBModificationType {
|
|
89
|
+
/** A value was stored by `add()` (including an id-based overwrite). */
|
|
90
|
+
ADD = "ADD",
|
|
91
|
+
/** A value was replaced by `update()`. */
|
|
92
|
+
UPDATE = "UPDATE",
|
|
93
|
+
/** A value was removed by `delete()`. */
|
|
94
|
+
DELETE = "DELETE"
|
|
95
|
+
}
|
|
96
|
+
/** Handler for {@link EBModificationType.ADD}: called with each value stored by `add()`. */
|
|
97
|
+
export type TAddHandler<T> = (added: T) => void | Promise<void>;
|
|
98
|
+
/** Handler for {@link EBModificationType.UPDATE}: called with the previous and the replacement value. */
|
|
99
|
+
export type TUpdateHandler<T> = (previous: T, updated: T) => void | Promise<void>;
|
|
100
|
+
/** Handler for {@link EBModificationType.DELETE}: called with each value removed by `delete()`. */
|
|
101
|
+
export type TDeleteHandler<T> = (deleted: T) => void | Promise<void>;
|
|
102
|
+
/** Handle returned by {@link IBCollection.on}. */
|
|
103
|
+
export interface IHandlerRegistration {
|
|
104
|
+
/** Unsubscribes the handler. Idempotent. */
|
|
105
|
+
cancel(): Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
/** Whether a handler registered via {@link IBCollection.on} blocks the call that triggered it. */
|
|
108
|
+
export declare enum EBHandlerMode {
|
|
109
|
+
/** The triggering `add()`/`update()`/`delete()` call doesn't resolve until this handler has run. Default. */
|
|
110
|
+
SYNC = "SYNC",
|
|
111
|
+
/** The handler runs without the triggering call waiting for it. */
|
|
112
|
+
ASYNC = "ASYNC"
|
|
113
|
+
}
|
|
81
114
|
/** A single collection of values of type `T`. Obtain one via {@link IBDatabase}. */
|
|
82
115
|
export interface IBCollection<T = any> {
|
|
83
116
|
readonly database: IBDatabase;
|
|
@@ -96,6 +129,16 @@ export interface IBCollection<T = any> {
|
|
|
96
129
|
add(...v: T[]): Promise<number>;
|
|
97
130
|
/** Replaces each item matching `predicate` with the (defined) result of `updater`. Returns the number updated. */
|
|
98
131
|
update(predicate: (v: T) => boolean, updater: (v: T) => Promise<T>): Promise<number>;
|
|
132
|
+
/** Number of items, or only those matching `predicate`. */
|
|
133
|
+
count(predicate?: (v: T) => boolean): Promise<number>;
|
|
134
|
+
/** Items whose {@link IBCollectionConfig.indexes}-configured `indexKey` extracts `key`. Always a defensive copy. Throws if `indexKey` isn't configured. */
|
|
135
|
+
getByIndex(indexKey: string, key: string): Promise<T[]>;
|
|
136
|
+
/** Subscribes to `add()` mutations. Fires after the value is stored, including any auto-flush to disk. `mode` defaults to {@link EBHandlerMode.SYNC}. A handler's own errors never fail the triggering call; they're logged only. */
|
|
137
|
+
on(modificationType: EBModificationType.ADD, handler: TAddHandler<T>, mode?: EBHandlerMode): Promise<IHandlerRegistration>;
|
|
138
|
+
/** Subscribes to `update()` mutations. Fires after the value is replaced, including any auto-flush to disk. `mode` defaults to {@link EBHandlerMode.SYNC}. A handler's own errors never fail the triggering call; they're logged only. */
|
|
139
|
+
on(modificationType: EBModificationType.UPDATE, handler: TUpdateHandler<T>, mode?: EBHandlerMode): Promise<IHandlerRegistration>;
|
|
140
|
+
/** Subscribes to `delete()` mutations. Fires after the value is removed, including any auto-flush to disk. `mode` defaults to {@link EBHandlerMode.SYNC}. A handler's own errors never fail the triggering call; they're logged only. */
|
|
141
|
+
on(modificationType: EBModificationType.DELETE, handler: TDeleteHandler<T>, mode?: EBHandlerMode): Promise<IHandlerRegistration>;
|
|
99
142
|
/** Writes the current in-memory state to disk. */
|
|
100
143
|
flush(): Promise<void>;
|
|
101
144
|
/** Discards in-memory state and re-reads it from disk. */
|
package/dist/base.js
CHANGED
|
@@ -51,4 +51,22 @@ export var EBDirtyUnloadStrategy;
|
|
|
51
51
|
/** Discard the unsaved changes silently. */
|
|
52
52
|
EBDirtyUnloadStrategy["IGNORE"] = "IGNORE";
|
|
53
53
|
})(EBDirtyUnloadStrategy || (EBDirtyUnloadStrategy = {}));
|
|
54
|
+
/** What kind of collection mutation a handler subscribes to via {@link IBCollection.on}. */
|
|
55
|
+
export var EBModificationType;
|
|
56
|
+
(function (EBModificationType) {
|
|
57
|
+
/** A value was stored by `add()` (including an id-based overwrite). */
|
|
58
|
+
EBModificationType["ADD"] = "ADD";
|
|
59
|
+
/** A value was replaced by `update()`. */
|
|
60
|
+
EBModificationType["UPDATE"] = "UPDATE";
|
|
61
|
+
/** A value was removed by `delete()`. */
|
|
62
|
+
EBModificationType["DELETE"] = "DELETE";
|
|
63
|
+
})(EBModificationType || (EBModificationType = {}));
|
|
64
|
+
/** Whether a handler registered via {@link IBCollection.on} blocks the call that triggered it. */
|
|
65
|
+
export var EBHandlerMode;
|
|
66
|
+
(function (EBHandlerMode) {
|
|
67
|
+
/** The triggering `add()`/`update()`/`delete()` call doesn't resolve until this handler has run. Default. */
|
|
68
|
+
EBHandlerMode["SYNC"] = "SYNC";
|
|
69
|
+
/** The handler runs without the triggering call waiting for it. */
|
|
70
|
+
EBHandlerMode["ASYNC"] = "ASYNC";
|
|
71
|
+
})(EBHandlerMode || (EBHandlerMode = {}));
|
|
54
72
|
//# sourceMappingURL=base.js.map
|
package/dist/base.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base.js","sourceRoot":"","sources":["../src/main/base.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,MAAM,CAAN,IAAY,gBAKX;AALD,WAAY,gBAAgB;IACxB,mCAAmC;IACnC,6CAAyB,CAAA;IACzB,mEAAmE;IACnE,iDAA6B,CAAA;AACjC,CAAC,EALW,gBAAgB,KAAhB,gBAAgB,QAK3B;AAED,uDAAuD;AACvD,MAAM,CAAN,IAAY,oBAMX;AAND,WAAY,oBAAoB;IAC5B,oEAAoE;IACpE,mCAAW,CAAA;IACX,+BAA+B;IAC/B,gDAAgD;IAChD,uCAAe,CAAA;AACnB,CAAC,EANW,oBAAoB,KAApB,oBAAoB,QAM/B;AAED,iFAAiF;AACjF,MAAM,CAAN,IAAY,qBAOX;AAPD,WAAY,qBAAqB;IAC7B,kCAAkC;IAClC,gDAAuB,CAAA;IACvB,6BAA6B;IAC7B,wCAAe,CAAA;IACf,iDAAiD;IACjD,0CAAiB,CAAA;AACrB,CAAC,EAPW,qBAAqB,KAArB,qBAAqB,QAOhC;AAED,wFAAwF;AACxF,MAAM,CAAN,IAAY,uBAKX;AALD,WAAY,uBAAuB;IAC/B,wCAAwC;IACxC,kDAAuB,CAAA;IACvB,2CAA2C;IAC3C,gDAAqB,CAAA;AACzB,CAAC,EALW,uBAAuB,KAAvB,uBAAuB,QAKlC;AAED,6DAA6D;AAC7D,MAAM,CAAN,IAAY,mBAKX;AALD,WAAY,mBAAmB;IAC3B,2DAA2D;IAC3D,sDAA+B,CAAA;IAC/B,2DAA2D;IAC3D,8DAAuC,CAAA;AAC3C,CAAC,EALW,mBAAmB,KAAnB,mBAAmB,QAK9B;AAED,kEAAkE;AAClE,MAAM,CAAN,IAAY,qBAOX;AAPD,WAAY,qBAAqB;IAC7B,yBAAyB;IACzB,wCAAe,CAAA;IACf,gCAAgC;IAChC,wCAAe,CAAA;IACf,4CAA4C;IAC5C,0CAAiB,CAAA;AACrB,CAAC,EAPW,qBAAqB,KAArB,qBAAqB,QAOhC"}
|
|
1
|
+
{"version":3,"file":"base.js","sourceRoot":"","sources":["../src/main/base.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,MAAM,CAAN,IAAY,gBAKX;AALD,WAAY,gBAAgB;IACxB,mCAAmC;IACnC,6CAAyB,CAAA;IACzB,mEAAmE;IACnE,iDAA6B,CAAA;AACjC,CAAC,EALW,gBAAgB,KAAhB,gBAAgB,QAK3B;AAED,uDAAuD;AACvD,MAAM,CAAN,IAAY,oBAMX;AAND,WAAY,oBAAoB;IAC5B,oEAAoE;IACpE,mCAAW,CAAA;IACX,+BAA+B;IAC/B,gDAAgD;IAChD,uCAAe,CAAA;AACnB,CAAC,EANW,oBAAoB,KAApB,oBAAoB,QAM/B;AAED,iFAAiF;AACjF,MAAM,CAAN,IAAY,qBAOX;AAPD,WAAY,qBAAqB;IAC7B,kCAAkC;IAClC,gDAAuB,CAAA;IACvB,6BAA6B;IAC7B,wCAAe,CAAA;IACf,iDAAiD;IACjD,0CAAiB,CAAA;AACrB,CAAC,EAPW,qBAAqB,KAArB,qBAAqB,QAOhC;AAED,wFAAwF;AACxF,MAAM,CAAN,IAAY,uBAKX;AALD,WAAY,uBAAuB;IAC/B,wCAAwC;IACxC,kDAAuB,CAAA;IACvB,2CAA2C;IAC3C,gDAAqB,CAAA;AACzB,CAAC,EALW,uBAAuB,KAAvB,uBAAuB,QAKlC;AAED,6DAA6D;AAC7D,MAAM,CAAN,IAAY,mBAKX;AALD,WAAY,mBAAmB;IAC3B,2DAA2D;IAC3D,sDAA+B,CAAA;IAC/B,2DAA2D;IAC3D,8DAAuC,CAAA;AAC3C,CAAC,EALW,mBAAmB,KAAnB,mBAAmB,QAK9B;AAED,kEAAkE;AAClE,MAAM,CAAN,IAAY,qBAOX;AAPD,WAAY,qBAAqB;IAC7B,yBAAyB;IACzB,wCAAe,CAAA;IACf,gCAAgC;IAChC,wCAAe,CAAA;IACf,4CAA4C;IAC5C,0CAAiB,CAAA;AACrB,CAAC,EAPW,qBAAqB,KAArB,qBAAqB,QAOhC;AA8CD,4FAA4F;AAC5F,MAAM,CAAN,IAAY,kBAOX;AAPD,WAAY,kBAAkB;IAC1B,uEAAuE;IACvE,iCAAW,CAAA;IACX,0CAA0C;IAC1C,uCAAiB,CAAA;IACjB,yCAAyC;IACzC,uCAAiB,CAAA;AACrB,CAAC,EAPW,kBAAkB,KAAlB,kBAAkB,QAO7B;AAeD,kGAAkG;AAClG,MAAM,CAAN,IAAY,aAKX;AALD,WAAY,aAAa;IACrB,6GAA6G;IAC7G,8BAAa,CAAA;IACb,mEAAmE;IACnE,gCAAe,CAAA;AACnB,CAAC,EALW,aAAa,KAAb,aAAa,QAKxB"}
|
package/dist/basics.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IODatabase } from "./fileio.js";
|
|
2
|
-
import { IBCollection, IBCollectionConfig, IBDatabase, IBDatabaseConfig, IBFunctionRegistry, TGetIDFunction, TSetIDFunction } from "./base.js";
|
|
2
|
+
import { EBHandlerMode, EBModificationType, IBCollection, IBCollectionConfig, IBDatabase, IBDatabaseConfig, IBFunctionRegistry, IHandlerRegistration, TAddHandler, TDeleteHandler, TGetIDFunction, TSetIDFunction, TUpdateHandler } from "./base.js";
|
|
3
3
|
/** Default {@link TGetIDFunction}: reads `v.id`, `undefined` if absent. */
|
|
4
4
|
export declare const DEFAULT_GET_ID: TGetIDFunction;
|
|
5
5
|
/** Default {@link TSetIDFunction}: assigns to `v.id`. */
|
|
@@ -30,16 +30,40 @@ export declare class BCollection<T = any> implements IBCollection<T> {
|
|
|
30
30
|
private loaded;
|
|
31
31
|
private dirty;
|
|
32
32
|
private dropped;
|
|
33
|
+
private addHandlers;
|
|
34
|
+
private updateHandlers;
|
|
35
|
+
private deleteHandlers;
|
|
36
|
+
private indexDefinitions;
|
|
37
|
+
private indexRuntime;
|
|
33
38
|
constructor(database: BDatabase, collectionID: string, collectionName: string, collectionConfig: IBCollectionConfig);
|
|
34
39
|
private checkLoaded;
|
|
40
|
+
private runExclusive;
|
|
35
41
|
private runProtected;
|
|
36
42
|
/**
|
|
37
|
-
* call this only within the
|
|
43
|
+
* call this only within the protecting mutex
|
|
38
44
|
*/
|
|
39
45
|
markDropped(): void;
|
|
40
46
|
get(predicate?: (v: T) => boolean): Promise<T[]>;
|
|
47
|
+
count(predicate?: (v: T) => boolean): Promise<number>;
|
|
48
|
+
getByIndex(indexKey: string, key: string): Promise<T[]>;
|
|
49
|
+
private invokeHandler;
|
|
50
|
+
private fireAdd;
|
|
51
|
+
private fireUpdate;
|
|
52
|
+
private fireDelete;
|
|
53
|
+
on(modificationType: EBModificationType.ADD, handler: TAddHandler<T>, mode?: EBHandlerMode): Promise<IHandlerRegistration>;
|
|
54
|
+
on(modificationType: EBModificationType.UPDATE, handler: TUpdateHandler<T>, mode?: EBHandlerMode): Promise<IHandlerRegistration>;
|
|
55
|
+
on(modificationType: EBModificationType.DELETE, handler: TDeleteHandler<T>, mode?: EBHandlerMode): Promise<IHandlerRegistration>;
|
|
41
56
|
delete(predicate: (v: T) => boolean): Promise<number>;
|
|
42
57
|
has(predicate: (v: T) => boolean): Promise<boolean>;
|
|
58
|
+
private resolveIndexDefinitions;
|
|
59
|
+
private indexKeysFor;
|
|
60
|
+
private addToIndexes;
|
|
61
|
+
private removeFromIndexes;
|
|
62
|
+
/**
|
|
63
|
+
* full rebuild from currently loaded data; only needed right after (re)load, since every
|
|
64
|
+
* value then is a freshly deserialized object the incremental add/update/delete hooks never saw
|
|
65
|
+
*/
|
|
66
|
+
private rebuildIndexRuntime;
|
|
43
67
|
private getID;
|
|
44
68
|
add(...vs: T[]): Promise<number>;
|
|
45
69
|
update(predicate: (v: T) => boolean, updater: (v: T) => Promise<T>): Promise<number>;
|
package/dist/basics.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import logger from "winston";
|
|
1
2
|
import { uuidv7, } from "uuidv7";
|
|
2
3
|
import { runMultiReentrantProtected, runReentrantProtected } from "@incoqnito.io/ts-iq-core";
|
|
3
|
-
import { EBArrayDeletionStrategy, EBAutoFlushStrategy, EBAutoIDStrategy, EBDirtyUnloadStrategy, EBDuplicateIDStrategy, EBMemStorageStrategy, } from "./base.js";
|
|
4
|
+
import { EBArrayDeletionStrategy, EBAutoFlushStrategy, EBAutoIDStrategy, EBDirtyUnloadStrategy, EBDuplicateIDStrategy, EBHandlerMode, EBMemStorageStrategy, EBModificationType, } from "./base.js";
|
|
4
5
|
import { canonicalName } from "./utils.js";
|
|
5
6
|
/** Default {@link TGetIDFunction}: reads `v.id`, `undefined` if absent. */
|
|
6
7
|
export const DEFAULT_GET_ID = (v) => ("id" in v) ? v.id : undefined;
|
|
@@ -89,6 +90,11 @@ export class BCollection {
|
|
|
89
90
|
loaded = false;
|
|
90
91
|
dirty = false;
|
|
91
92
|
dropped = false;
|
|
93
|
+
addHandlers = [];
|
|
94
|
+
updateHandlers = [];
|
|
95
|
+
deleteHandlers = [];
|
|
96
|
+
indexDefinitions = new Map();
|
|
97
|
+
indexRuntime = new Map();
|
|
92
98
|
constructor(database, collectionID, collectionName, collectionConfig) {
|
|
93
99
|
this.database = database;
|
|
94
100
|
this.collectionID = collectionID;
|
|
@@ -110,8 +116,11 @@ export class BCollection {
|
|
|
110
116
|
await this.reload();
|
|
111
117
|
}
|
|
112
118
|
}
|
|
119
|
+
async runExclusive(fn) {
|
|
120
|
+
return runReentrantProtected(this.collectionName, fn);
|
|
121
|
+
}
|
|
113
122
|
async runProtected(fn) {
|
|
114
|
-
return
|
|
123
|
+
return this.runExclusive(async () => {
|
|
115
124
|
if (this.dropped) {
|
|
116
125
|
throw new Error(`collection "${this.collectionName}" has been dropped`);
|
|
117
126
|
}
|
|
@@ -119,7 +128,7 @@ export class BCollection {
|
|
|
119
128
|
});
|
|
120
129
|
}
|
|
121
130
|
/**
|
|
122
|
-
* call this only within the
|
|
131
|
+
* call this only within the protecting mutex
|
|
123
132
|
*/
|
|
124
133
|
markDropped() {
|
|
125
134
|
this.dropped = true;
|
|
@@ -135,22 +144,117 @@ export class BCollection {
|
|
|
135
144
|
default:
|
|
136
145
|
case EBMemStorageStrategy.ARRAY:
|
|
137
146
|
return !predicate ?
|
|
138
|
-
|
|
139
|
-
[...this.arrayData] :
|
|
147
|
+
this.arrayData.slice() :
|
|
140
148
|
this.arrayData.filter(predicate);
|
|
141
149
|
}
|
|
142
150
|
});
|
|
143
151
|
}
|
|
152
|
+
async count(predicate) {
|
|
153
|
+
return this.runProtected(async () => {
|
|
154
|
+
await this.checkLoaded();
|
|
155
|
+
switch (this.collectionConfig.memStorageStrategy) {
|
|
156
|
+
case EBMemStorageStrategy.MAP:
|
|
157
|
+
return !predicate ?
|
|
158
|
+
this.mapData.size :
|
|
159
|
+
this.mapData.values().filter(predicate).reduce((n) => n + 1, 0);
|
|
160
|
+
default:
|
|
161
|
+
case EBMemStorageStrategy.ARRAY:
|
|
162
|
+
return !predicate ?
|
|
163
|
+
this.arrayData.length :
|
|
164
|
+
this.arrayData.filter(predicate).length;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async getByIndex(indexKey, key) {
|
|
169
|
+
return this.runProtected(async () => {
|
|
170
|
+
await this.checkLoaded();
|
|
171
|
+
const byKey = this.indexRuntime.get(indexKey);
|
|
172
|
+
if (!byKey) {
|
|
173
|
+
throw new Error(`index "${indexKey}" is not configured`);
|
|
174
|
+
}
|
|
175
|
+
return byKey.get(key)?.slice() ?? [];
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async invokeHandler(fn, modificationType) {
|
|
179
|
+
try {
|
|
180
|
+
await fn();
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
logger.error(`bajadab: collection "${this.collectionName}" ${modificationType} handler failed`, error);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async fireAdd(added) {
|
|
187
|
+
for (const v of added) {
|
|
188
|
+
for (const { handler, mode } of this.addHandlers) {
|
|
189
|
+
const call = this.invokeHandler(() => handler(v), EBModificationType.ADD);
|
|
190
|
+
if (mode === EBHandlerMode.SYNC) {
|
|
191
|
+
await call;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async fireUpdate(pairs) {
|
|
197
|
+
for (const [previous, updated] of pairs) {
|
|
198
|
+
for (const { handler, mode } of this.updateHandlers) {
|
|
199
|
+
const call = this.invokeHandler(() => handler(previous, updated), EBModificationType.UPDATE);
|
|
200
|
+
if (mode === EBHandlerMode.SYNC) {
|
|
201
|
+
await call;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async fireDelete(deleted) {
|
|
207
|
+
for (const v of deleted) {
|
|
208
|
+
for (const { handler, mode } of this.deleteHandlers) {
|
|
209
|
+
const call = this.invokeHandler(() => handler(v), EBModificationType.DELETE);
|
|
210
|
+
if (mode === EBHandlerMode.SYNC) {
|
|
211
|
+
await call;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async on(modificationType, handler, mode = EBHandlerMode.SYNC) {
|
|
217
|
+
return this.runProtected(async () => {
|
|
218
|
+
switch (modificationType) {
|
|
219
|
+
case EBModificationType.ADD: {
|
|
220
|
+
const h = handler;
|
|
221
|
+
this.addHandlers.push({ handler: h, mode });
|
|
222
|
+
return { cancel: async () => this.runExclusive(async () => { this.addHandlers = this.addHandlers.filter(x => x.handler !== h); }) };
|
|
223
|
+
}
|
|
224
|
+
case EBModificationType.UPDATE: {
|
|
225
|
+
const h = handler;
|
|
226
|
+
this.updateHandlers.push({ handler: h, mode });
|
|
227
|
+
return { cancel: async () => this.runExclusive(async () => { this.updateHandlers = this.updateHandlers.filter(x => x.handler !== h); }) };
|
|
228
|
+
}
|
|
229
|
+
case EBModificationType.DELETE: {
|
|
230
|
+
const h = handler;
|
|
231
|
+
this.deleteHandlers.push({ handler: h, mode });
|
|
232
|
+
return { cancel: async () => this.runExclusive(async () => { this.deleteHandlers = this.deleteHandlers.filter(x => x.handler !== h); }) };
|
|
233
|
+
}
|
|
234
|
+
default:
|
|
235
|
+
throw new Error(`unknown modification type ${modificationType}`);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
144
239
|
async delete(predicate) {
|
|
145
240
|
return this.runProtected(async () => {
|
|
146
241
|
await this.checkLoaded();
|
|
147
242
|
let ret = 0;
|
|
243
|
+
const trackDeletes = this.deleteHandlers.length > 0;
|
|
244
|
+
const indexDeletes = this.indexDefinitions.size > 0;
|
|
245
|
+
const deleted = [];
|
|
148
246
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
149
247
|
case EBMemStorageStrategy.MAP:
|
|
150
248
|
for (const [key, value] of this.mapData) {
|
|
151
249
|
if (predicate(value)) {
|
|
152
250
|
ret++;
|
|
251
|
+
if (indexDeletes) {
|
|
252
|
+
await this.removeFromIndexes(value);
|
|
253
|
+
}
|
|
153
254
|
this.mapData.delete(key);
|
|
255
|
+
if (trackDeletes) {
|
|
256
|
+
deleted.push(value);
|
|
257
|
+
}
|
|
154
258
|
}
|
|
155
259
|
}
|
|
156
260
|
break;
|
|
@@ -167,6 +271,12 @@ export class BCollection {
|
|
|
167
271
|
}
|
|
168
272
|
else {
|
|
169
273
|
ret++;
|
|
274
|
+
if (indexDeletes) {
|
|
275
|
+
await this.removeFromIndexes(item);
|
|
276
|
+
}
|
|
277
|
+
if (trackDeletes) {
|
|
278
|
+
deleted.push(item);
|
|
279
|
+
}
|
|
170
280
|
}
|
|
171
281
|
}
|
|
172
282
|
this.arrayData.length = writeIndex;
|
|
@@ -174,6 +284,17 @@ export class BCollection {
|
|
|
174
284
|
default:
|
|
175
285
|
case EBArrayDeletionStrategy.NEW_ARRAY:
|
|
176
286
|
const ol = this.arrayData.length;
|
|
287
|
+
if (trackDeletes || indexDeletes) {
|
|
288
|
+
const removed = this.arrayData.filter(predicate);
|
|
289
|
+
if (trackDeletes) {
|
|
290
|
+
deleted.push(...removed);
|
|
291
|
+
}
|
|
292
|
+
if (indexDeletes) {
|
|
293
|
+
for (const v of removed) {
|
|
294
|
+
await this.removeFromIndexes(v);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
177
298
|
this.arrayData = this.arrayData.filter(v => !predicate(v));
|
|
178
299
|
ret += (ol - this.arrayData.length);
|
|
179
300
|
break;
|
|
@@ -183,6 +304,9 @@ export class BCollection {
|
|
|
183
304
|
if (ret > 0) {
|
|
184
305
|
await this.doOnDirty();
|
|
185
306
|
}
|
|
307
|
+
if (deleted.length > 0) {
|
|
308
|
+
await this.fireDelete(deleted);
|
|
309
|
+
}
|
|
186
310
|
return ret;
|
|
187
311
|
});
|
|
188
312
|
}
|
|
@@ -198,6 +322,78 @@ export class BCollection {
|
|
|
198
322
|
}
|
|
199
323
|
});
|
|
200
324
|
}
|
|
325
|
+
resolveIndexDefinitions() {
|
|
326
|
+
this.indexDefinitions = new Map();
|
|
327
|
+
if (!this.collectionConfig.indexes) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
for (const [indexKey, registryKey] of Object.entries(this.collectionConfig.indexes)) {
|
|
331
|
+
const extractor = this.database.functionRegistry?.fetch(registryKey);
|
|
332
|
+
if (!extractor) {
|
|
333
|
+
throw new Error(`index key function "${registryKey}" is not registered`);
|
|
334
|
+
}
|
|
335
|
+
this.indexDefinitions.set(indexKey, extractor);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async indexKeysFor(v) {
|
|
339
|
+
const result = [];
|
|
340
|
+
for (const [indexKey, extractor] of this.indexDefinitions) {
|
|
341
|
+
const extracted = await extractor(v);
|
|
342
|
+
if (extracted === undefined) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
result.push([indexKey, Array.isArray(extracted) ? extracted : [extracted]]);
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
async addToIndexes(v) {
|
|
350
|
+
for (const [indexKey, keys] of await this.indexKeysFor(v)) {
|
|
351
|
+
const byKey = this.indexRuntime.get(indexKey);
|
|
352
|
+
for (const key of keys) {
|
|
353
|
+
let bucket = byKey.get(key);
|
|
354
|
+
if (!bucket) {
|
|
355
|
+
bucket = [];
|
|
356
|
+
byKey.set(key, bucket);
|
|
357
|
+
}
|
|
358
|
+
bucket.push(v);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async removeFromIndexes(v) {
|
|
363
|
+
for (const [indexKey, keys] of await this.indexKeysFor(v)) {
|
|
364
|
+
const byKey = this.indexRuntime.get(indexKey);
|
|
365
|
+
for (const key of keys) {
|
|
366
|
+
const bucket = byKey.get(key);
|
|
367
|
+
if (!bucket) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const at = bucket.indexOf(v);
|
|
371
|
+
if (at !== -1) {
|
|
372
|
+
bucket.splice(at, 1);
|
|
373
|
+
}
|
|
374
|
+
if (bucket.length === 0) {
|
|
375
|
+
byKey.delete(key);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* full rebuild from currently loaded data; only needed right after (re)load, since every
|
|
382
|
+
* value then is a freshly deserialized object the incremental add/update/delete hooks never saw
|
|
383
|
+
*/
|
|
384
|
+
async rebuildIndexRuntime() {
|
|
385
|
+
// every configured index gets an entry up front, even if empty, so getByIndex()/
|
|
386
|
+
// addToIndexes()/removeFromIndexes() can assume it's there instead of checking twice
|
|
387
|
+
this.indexRuntime = new Map(Array.from(this.indexDefinitions.keys(), (indexKey) => [indexKey, new Map()]));
|
|
388
|
+
if (this.indexDefinitions.size === 0) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const values = this.collectionConfig.memStorageStrategy === EBMemStorageStrategy.MAP ?
|
|
392
|
+
[...this.mapData.values()] : this.arrayData;
|
|
393
|
+
for (const v of values) {
|
|
394
|
+
await this.addToIndexes(v);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
201
397
|
async getID(v) {
|
|
202
398
|
let getter;
|
|
203
399
|
if (this.collectionConfig.getID) {
|
|
@@ -241,6 +437,9 @@ export class BCollection {
|
|
|
241
437
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
242
438
|
case EBMemStorageStrategy.MAP:
|
|
243
439
|
let ret = 0;
|
|
440
|
+
const trackAddsMap = this.addHandlers.length > 0;
|
|
441
|
+
const indexAddsMap = this.indexDefinitions.size > 0;
|
|
442
|
+
const addedMap = [];
|
|
244
443
|
for (const v of vs) {
|
|
245
444
|
const id = await this.getID(v);
|
|
246
445
|
if (this.mapData.has(id)) {
|
|
@@ -254,21 +453,42 @@ export class BCollection {
|
|
|
254
453
|
case EBDuplicateIDStrategy.OVERWRITE:
|
|
255
454
|
// replace the new value
|
|
256
455
|
ret++;
|
|
456
|
+
if (indexAddsMap) {
|
|
457
|
+
await this.removeFromIndexes(this.mapData.get(id));
|
|
458
|
+
}
|
|
257
459
|
this.mapData.set(id, v);
|
|
460
|
+
if (indexAddsMap) {
|
|
461
|
+
await this.addToIndexes(v);
|
|
462
|
+
}
|
|
463
|
+
if (trackAddsMap) {
|
|
464
|
+
addedMap.push(v);
|
|
465
|
+
}
|
|
258
466
|
break;
|
|
259
467
|
}
|
|
260
468
|
}
|
|
261
469
|
else {
|
|
262
470
|
ret++;
|
|
263
471
|
this.mapData.set(id, v);
|
|
472
|
+
if (indexAddsMap) {
|
|
473
|
+
await this.addToIndexes(v);
|
|
474
|
+
}
|
|
475
|
+
if (trackAddsMap) {
|
|
476
|
+
addedMap.push(v);
|
|
477
|
+
}
|
|
264
478
|
}
|
|
265
479
|
}
|
|
266
480
|
if (ret > 0) {
|
|
267
481
|
await this.doOnDirty();
|
|
268
482
|
}
|
|
483
|
+
if (addedMap.length > 0) {
|
|
484
|
+
await this.fireAdd(addedMap);
|
|
485
|
+
}
|
|
269
486
|
return ret;
|
|
270
487
|
default:
|
|
271
488
|
case EBMemStorageStrategy.ARRAY:
|
|
489
|
+
const trackAddsArray = this.addHandlers.length > 0;
|
|
490
|
+
const indexAddsArray = this.indexDefinitions.size > 0;
|
|
491
|
+
const addedArray = [];
|
|
272
492
|
switch (this.collectionConfig.duplicateIDStrategy) {
|
|
273
493
|
case EBDuplicateIDStrategy.ERROR:
|
|
274
494
|
const haveIDs = new Set();
|
|
@@ -282,6 +502,12 @@ export class BCollection {
|
|
|
282
502
|
}
|
|
283
503
|
haveIDs.add(id);
|
|
284
504
|
this.arrayData.push(v);
|
|
505
|
+
if (indexAddsArray) {
|
|
506
|
+
await this.addToIndexes(v);
|
|
507
|
+
}
|
|
508
|
+
if (trackAddsArray) {
|
|
509
|
+
addedArray.push(v);
|
|
510
|
+
}
|
|
285
511
|
}
|
|
286
512
|
break;
|
|
287
513
|
case EBDuplicateIDStrategy.IGNORE:
|
|
@@ -294,6 +520,12 @@ export class BCollection {
|
|
|
294
520
|
if (!seenIDs.has(id)) {
|
|
295
521
|
seenIDs.add(id);
|
|
296
522
|
this.arrayData.push(v);
|
|
523
|
+
if (indexAddsArray) {
|
|
524
|
+
await this.addToIndexes(v);
|
|
525
|
+
}
|
|
526
|
+
if (trackAddsArray) {
|
|
527
|
+
addedArray.push(v);
|
|
528
|
+
}
|
|
297
529
|
}
|
|
298
530
|
}
|
|
299
531
|
break;
|
|
@@ -307,6 +539,9 @@ export class BCollection {
|
|
|
307
539
|
const id = await this.getID(v);
|
|
308
540
|
const existingIndex = indexMap.get(id);
|
|
309
541
|
if (existingIndex !== undefined) {
|
|
542
|
+
if (indexAddsArray) {
|
|
543
|
+
await this.removeFromIndexes(this.arrayData[existingIndex]);
|
|
544
|
+
}
|
|
310
545
|
this.arrayData[existingIndex] = v;
|
|
311
546
|
}
|
|
312
547
|
else {
|
|
@@ -314,12 +549,21 @@ export class BCollection {
|
|
|
314
549
|
indexMap.set(id, this.arrayData.length);
|
|
315
550
|
this.arrayData.push(v);
|
|
316
551
|
}
|
|
552
|
+
if (indexAddsArray) {
|
|
553
|
+
await this.addToIndexes(v);
|
|
554
|
+
}
|
|
555
|
+
if (trackAddsArray) {
|
|
556
|
+
addedArray.push(v);
|
|
557
|
+
}
|
|
317
558
|
}
|
|
318
559
|
break;
|
|
319
560
|
}
|
|
320
561
|
if (vs.length > 0) {
|
|
321
562
|
await this.doOnDirty();
|
|
322
563
|
}
|
|
564
|
+
if (addedArray.length > 0) {
|
|
565
|
+
await this.fireAdd(addedArray);
|
|
566
|
+
}
|
|
323
567
|
return vs.length;
|
|
324
568
|
}
|
|
325
569
|
});
|
|
@@ -328,6 +572,9 @@ export class BCollection {
|
|
|
328
572
|
return this.runProtected(async () => {
|
|
329
573
|
await this.checkLoaded();
|
|
330
574
|
let ret = 0;
|
|
575
|
+
const trackUpdates = this.updateHandlers.length > 0;
|
|
576
|
+
const indexUpdates = this.indexDefinitions.size > 0;
|
|
577
|
+
const updated = [];
|
|
331
578
|
switch (this.collectionConfig.memStorageStrategy) {
|
|
332
579
|
case EBMemStorageStrategy.MAP:
|
|
333
580
|
for (const [key, value] of this.mapData) {
|
|
@@ -335,7 +582,16 @@ export class BCollection {
|
|
|
335
582
|
const replacement = await updater(value);
|
|
336
583
|
if (replacement !== undefined) {
|
|
337
584
|
ret++;
|
|
585
|
+
if (indexUpdates) {
|
|
586
|
+
await this.removeFromIndexes(value);
|
|
587
|
+
}
|
|
338
588
|
this.mapData.set(key, replacement);
|
|
589
|
+
if (indexUpdates) {
|
|
590
|
+
await this.addToIndexes(replacement);
|
|
591
|
+
}
|
|
592
|
+
if (trackUpdates) {
|
|
593
|
+
updated.push([value, replacement]);
|
|
594
|
+
}
|
|
339
595
|
}
|
|
340
596
|
else {
|
|
341
597
|
throw new Error("updater gave undefined");
|
|
@@ -347,10 +603,20 @@ export class BCollection {
|
|
|
347
603
|
case EBMemStorageStrategy.ARRAY:
|
|
348
604
|
for (let i = 0; i < this.arrayData.length; i++) {
|
|
349
605
|
if (predicate(this.arrayData[i])) {
|
|
350
|
-
const
|
|
606
|
+
const previous = this.arrayData[i];
|
|
607
|
+
const replacement = await updater(previous);
|
|
351
608
|
if (replacement !== undefined) {
|
|
352
609
|
ret++;
|
|
610
|
+
if (indexUpdates) {
|
|
611
|
+
await this.removeFromIndexes(previous);
|
|
612
|
+
}
|
|
353
613
|
this.arrayData[i] = replacement;
|
|
614
|
+
if (indexUpdates) {
|
|
615
|
+
await this.addToIndexes(replacement);
|
|
616
|
+
}
|
|
617
|
+
if (trackUpdates) {
|
|
618
|
+
updated.push([previous, replacement]);
|
|
619
|
+
}
|
|
354
620
|
}
|
|
355
621
|
else {
|
|
356
622
|
throw new Error("updater gave undefined");
|
|
@@ -362,6 +628,9 @@ export class BCollection {
|
|
|
362
628
|
if (ret > 0) {
|
|
363
629
|
await this.doOnDirty();
|
|
364
630
|
}
|
|
631
|
+
if (updated.length > 0) {
|
|
632
|
+
await this.fireUpdate(updated);
|
|
633
|
+
}
|
|
365
634
|
return ret;
|
|
366
635
|
});
|
|
367
636
|
}
|
|
@@ -400,6 +669,8 @@ export class BCollection {
|
|
|
400
669
|
}
|
|
401
670
|
this.loaded = true;
|
|
402
671
|
this.dirty = false;
|
|
672
|
+
this.resolveIndexDefinitions();
|
|
673
|
+
await this.rebuildIndexRuntime();
|
|
403
674
|
});
|
|
404
675
|
}
|
|
405
676
|
async unload() {
|