@incoqnito.io/bajadab 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +192 -174
- package/dist/base.d.ts +13 -9
- package/dist/base.js.map +1 -1
- package/dist/basics.d.ts +27 -10
- package/dist/basics.js +378 -251
- package/dist/basics.js.map +1 -1
- package/dist/fileio.d.ts +7 -0
- package/dist/fileio.js +65 -14
- package/dist/fileio.js.map +1 -1
- package/package.json +2 -2
- package/src/main/base.ts +235 -231
- package/src/main/basics.ts +1058 -912
- package/src/main/fileio.ts +270 -207
package/README.md
CHANGED
|
@@ -1,174 +1,192 @@
|
|
|
1
|
-
# bajadab
|
|
2
|
-
|
|
3
|
-
A lightweight JSON-file-backed database for small, single-machine setups. No server, no native bindings — just collections of values persisted as JSON under a directory you choose.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- Collections stored as plain JSON files, one directory per database.
|
|
8
|
-
- `MAP` (keyed by id) or `ARRAY` (ordered list) in-memory storage per collection.
|
|
9
|
-
- Configurable behavior for duplicate ids, missing ids, array deletion, auto-flush, and unloading dirty data.
|
|
10
|
-
- Custom id extraction/assignment via a pluggable function registry.
|
|
11
|
-
- Optional secondary indexes for fast lookup by a derived key.
|
|
12
|
-
- Atomic writes (write-to-temp, then rename) and, on POSIX systems, owner-only file permissions (`0600` files, `0700` directories).
|
|
13
|
-
- In-process concurrency safety
|
|
14
|
-
- `db.snapshot(targetDir, options?)` to copy the whole database into a new timestamped version folder, optionally pruning older ones.
|
|
15
|
-
- `db.transaction(collectionNames, fn)` to apply reads and writes across several collections as one unit — committed together, or not applied at all.
|
|
16
|
-
|
|
17
|
-
## Install
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
npm install @incoqnito.io/bajadab
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Requires Node.js `>= 24`.
|
|
24
|
-
|
|
25
|
-
## Quick start
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
import { getDatabase, EBMemStorageStrategy } from "@incoqnito.io/bajadab";
|
|
29
|
-
|
|
30
|
-
const db = await getDatabase("./data");
|
|
31
|
-
|
|
32
|
-
const notes = await db.getCollection<{ id?: string; text: string }>("notes", {
|
|
33
|
-
...db.config.defaultCollectionConfig,
|
|
34
|
-
memStorageStrategy: EBMemStorageStrategy.ARRAY,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
await notes.add({ text: "hello" });
|
|
38
|
-
const all = await notes.get();
|
|
39
|
-
const some = await notes.get(n => n.text.startsWith("h"));
|
|
40
|
-
|
|
41
|
-
await notes.delete(n => n.text === "hello");
|
|
42
|
-
await db.dropCollection("notes");
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
`getDatabase(basePath)` opens the database at `basePath`, creating it if it doesn't exist. `db.getCollection(name, config?)` returns the named collection, creating it with `config` (or the database's default) if it doesn't exist yet; `db.createCollection(name, config?)` instead rejects if one already exists. Collection names are matched case- and whitespace-insensitively (`"Notes"` and `" notes "` refer to the same collection).
|
|
46
|
-
|
|
47
|
-
## Configuring a collection
|
|
48
|
-
|
|
49
|
-
Every collection has an `IBCollectionConfig`. `db.config.defaultCollectionConfig` holds the database's defaults — spread it and override what you need, as above.
|
|
50
|
-
|
|
51
|
-
| Option | Values | Meaning |
|
|
52
|
-
| --- | --- | --- |
|
|
53
|
-
| `memStorageStrategy` | `MAP` (default) / `ARRAY` | Keyed lookup by id, or an ordered list. |
|
|
54
|
-
| `autoIDStrategy` | `AUTO_ID_UUID` (default) / `NO_AUTO_ID` | Generate a `uuidv7` for values with no id, or reject them. |
|
|
55
|
-
| `duplicateIDStrategy` | `OVERWRITE` (default) / `ERROR` / `IGNORE` | What `add()` does when a value's id already exists. |
|
|
56
|
-
| `arrayDeletionStrategy` | `IN_PLACE` (default) / `NEW_ARRAY` | How `delete()` removes matches from an `ARRAY` collection. |
|
|
57
|
-
| `autoFlushStrategy` | `ALWAYS_AUTO_FLUSH` (default) / `NO_AUTO_FLUSH` | Whether every mutation is written to disk immediately, or only on an explicit `flush()`. |
|
|
58
|
-
| `dirtyUnloadStrategy` | `FLUSH` (default) / `ERROR` / `IGNORE` | What `unload()` does with unsaved changes. |
|
|
59
|
-
| `getID` / `setID` | registry key (optional) | Custom id accessors — see below. Omit to use the default, which reads/writes a plain `.id` property. |
|
|
60
|
-
| `indexes` | `{ [indexKey: string]: registry key }` (optional) | Secondary indexes — see below. Omit for no indexes. |
|
|
61
|
-
|
|
62
|
-
## Custom ids
|
|
63
|
-
|
|
64
|
-
By default, a value's id is its `.id` property. To use something else, implement `IBFunctionRegistry` and register `getID`/`setID` functions under whatever keys you configure:
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
import { getDatabase, IBFunctionRegistry } from "@incoqnito.io/bajadab";
|
|
68
|
-
|
|
69
|
-
class Registry implements IBFunctionRegistry {
|
|
70
|
-
private fns = new Map<string, Function>([
|
|
71
|
-
["userGetID", async (v: any) => v.userId],
|
|
72
|
-
["userSetID", async (v: any, id: string) => { v.userId = id; }],
|
|
73
|
-
]);
|
|
74
|
-
fetch(key?: string) {
|
|
75
|
-
return key ? this.fns.get(key) : undefined;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const db = await getDatabase("./data", new Registry());
|
|
80
|
-
const users = await db.createCollection("users", {
|
|
81
|
-
...db.config.defaultCollectionConfig,
|
|
82
|
-
getID: "userGetID",
|
|
83
|
-
setID: "userSetID",
|
|
84
|
-
});
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
A `getID`/`setID` key that isn't found in the registry causes `add()` to reject, rather than silently falling back to the default.
|
|
88
|
-
|
|
89
|
-
## Indexes
|
|
90
|
-
|
|
91
|
-
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:
|
|
92
|
-
|
|
93
|
-
```ts
|
|
94
|
-
class Registry implements IBFunctionRegistry {
|
|
95
|
-
private fns = new Map<string, Function>([
|
|
96
|
-
["byEmail", async (v: any) => v.email],
|
|
97
|
-
]);
|
|
98
|
-
fetch(key?: string) {
|
|
99
|
-
return key ? this.fns.get(key) : undefined;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const db = await getDatabase("./data", new Registry());
|
|
104
|
-
const users = await db.createCollection("users", {
|
|
105
|
-
...db.config.defaultCollectionConfig,
|
|
106
|
-
indexes: { email: "byEmail" },
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
await users.add({ email: "a@example.com", name: "A" });
|
|
110
|
-
const matches = await users.getByIndex("email", "a@example.com");
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
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()`.
|
|
114
|
-
|
|
115
|
-
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.
|
|
116
|
-
|
|
117
|
-
##
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
- `
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
`
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
`
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
`
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
1
|
+
# bajadab
|
|
2
|
+
|
|
3
|
+
A lightweight JSON-file-backed database for small, single-machine setups. No server, no native bindings — just collections of values persisted as JSON under a directory you choose.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Collections stored as plain JSON files, one directory per database.
|
|
8
|
+
- `MAP` (keyed by id) or `ARRAY` (ordered list) in-memory storage per collection.
|
|
9
|
+
- Configurable behavior for duplicate ids, missing ids, array deletion, auto-flush, and unloading dirty data.
|
|
10
|
+
- Custom id extraction/assignment via a pluggable function registry.
|
|
11
|
+
- Optional secondary indexes for fast lookup by a derived key.
|
|
12
|
+
- Atomic writes (write-to-temp, then rename) and, on POSIX systems, owner-only file permissions (`0600` files, `0700` directories).
|
|
13
|
+
- In-process concurrency safety via one lock per database — deadlock-free, even when handlers touch other collections.
|
|
14
|
+
- `db.snapshot(targetDir, options?)` to copy the whole database into a new timestamped version folder, optionally pruning older ones.
|
|
15
|
+
- `db.transaction(collectionNames, fn)` to apply reads and writes across several collections as one unit — committed together, or not applied at all.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
npm install @incoqnito.io/bajadab
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Node.js `>= 24`.
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { getDatabase, EBMemStorageStrategy } from "@incoqnito.io/bajadab";
|
|
29
|
+
|
|
30
|
+
const db = await getDatabase("./data");
|
|
31
|
+
|
|
32
|
+
const notes = await db.getCollection<{ id?: string; text: string }>("notes", {
|
|
33
|
+
...db.config.defaultCollectionConfig,
|
|
34
|
+
memStorageStrategy: EBMemStorageStrategy.ARRAY,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await notes.add({ text: "hello" });
|
|
38
|
+
const all = await notes.get();
|
|
39
|
+
const some = await notes.get(n => n.text.startsWith("h"));
|
|
40
|
+
|
|
41
|
+
await notes.delete(n => n.text === "hello");
|
|
42
|
+
await db.dropCollection("notes");
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`getDatabase(basePath)` opens the database at `basePath`, creating it if it doesn't exist. `db.getCollection(name, config?)` returns the named collection, creating it with `config` (or the database's default) if it doesn't exist yet; `db.createCollection(name, config?)` instead rejects if one already exists. Collection names are matched case- and whitespace-insensitively (`"Notes"` and `" notes "` refer to the same collection).
|
|
46
|
+
|
|
47
|
+
## Configuring a collection
|
|
48
|
+
|
|
49
|
+
Every collection has an `IBCollectionConfig`. `db.config.defaultCollectionConfig` holds the database's defaults — spread it and override what you need, as above.
|
|
50
|
+
|
|
51
|
+
| Option | Values | Meaning |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `memStorageStrategy` | `MAP` (default) / `ARRAY` | Keyed lookup by id, or an ordered list. |
|
|
54
|
+
| `autoIDStrategy` | `AUTO_ID_UUID` (default) / `NO_AUTO_ID` | Generate a `uuidv7` for values with no id, or reject them. |
|
|
55
|
+
| `duplicateIDStrategy` | `OVERWRITE` (default) / `ERROR` / `IGNORE` | What `add()` does when a value's id already exists. |
|
|
56
|
+
| `arrayDeletionStrategy` | `IN_PLACE` (default) / `NEW_ARRAY` | How `delete()` removes matches from an `ARRAY` collection. |
|
|
57
|
+
| `autoFlushStrategy` | `ALWAYS_AUTO_FLUSH` (default) / `NO_AUTO_FLUSH` | Whether every mutation is written to disk immediately, or only on an explicit `flush()`. |
|
|
58
|
+
| `dirtyUnloadStrategy` | `FLUSH` (default) / `ERROR` / `IGNORE` | What `unload()` does with unsaved changes. |
|
|
59
|
+
| `getID` / `setID` | registry key (optional) | Custom id accessors — see below. Omit to use the default, which reads/writes a plain `.id` property. |
|
|
60
|
+
| `indexes` | `{ [indexKey: string]: registry key }` (optional) | Secondary indexes — see below. Omit for no indexes. |
|
|
61
|
+
|
|
62
|
+
## Custom ids
|
|
63
|
+
|
|
64
|
+
By default, a value's id is its `.id` property. To use something else, implement `IBFunctionRegistry` and register `getID`/`setID` functions under whatever keys you configure:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { getDatabase, IBFunctionRegistry } from "@incoqnito.io/bajadab";
|
|
68
|
+
|
|
69
|
+
class Registry implements IBFunctionRegistry {
|
|
70
|
+
private fns = new Map<string, Function>([
|
|
71
|
+
["userGetID", async (v: any) => v.userId],
|
|
72
|
+
["userSetID", async (v: any, id: string) => { v.userId = id; }],
|
|
73
|
+
]);
|
|
74
|
+
fetch(key?: string) {
|
|
75
|
+
return key ? this.fns.get(key) : undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const db = await getDatabase("./data", new Registry());
|
|
80
|
+
const users = await db.createCollection("users", {
|
|
81
|
+
...db.config.defaultCollectionConfig,
|
|
82
|
+
getID: "userGetID",
|
|
83
|
+
setID: "userSetID",
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A `getID`/`setID` key that isn't found in the registry causes `add()` to reject, rather than silently falling back to the default.
|
|
88
|
+
|
|
89
|
+
## Indexes
|
|
90
|
+
|
|
91
|
+
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:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
class Registry implements IBFunctionRegistry {
|
|
95
|
+
private fns = new Map<string, Function>([
|
|
96
|
+
["byEmail", async (v: any) => v.email],
|
|
97
|
+
]);
|
|
98
|
+
fetch(key?: string) {
|
|
99
|
+
return key ? this.fns.get(key) : undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const db = await getDatabase("./data", new Registry());
|
|
104
|
+
const users = await db.createCollection("users", {
|
|
105
|
+
...db.config.defaultCollectionConfig,
|
|
106
|
+
indexes: { email: "byEmail" },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await users.add({ email: "a@example.com", name: "A" });
|
|
110
|
+
const matches = await users.getByIndex("email", "a@example.com");
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
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()`.
|
|
114
|
+
|
|
115
|
+
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. Each value remembers the keys it was indexed under, so re-adding a value that was mutated in place moves it out of its old buckets correctly.
|
|
116
|
+
|
|
117
|
+
## Values are stored by reference
|
|
118
|
+
|
|
119
|
+
bajadab keeps the objects you hand it; it doesn't copy them. `get()`/`getByIndex()` return a new array, but the values in it are the stored objects themselves, and `update()`'s updater receives the stored object too. Change values only by passing a new object to `update()` (`async v => ({ ...v, value: 5 })`) or to `add()` with an existing id:
|
|
120
|
+
|
|
121
|
+
- Mutating a value you got from `get()` changes the in-memory state without marking the collection dirty, updating indexes, or firing handlers.
|
|
122
|
+
- An updater that mutates in place and returns the same object still keeps indexes consistent, but the `UPDATE` handler receives that same object as both `previous` and `updated`.
|
|
123
|
+
- Inside a transaction, in-place mutations are outside transactional safety: they aren't reverted on rollback. A value mutated directly fires nothing on commit; an in-place updater passed to `update()` fires `UPDATE` with the same object as both `previous` and `updated`, same as outside a transaction.
|
|
124
|
+
|
|
125
|
+
`update()` is all-or-nothing: if the updater throws or returns `undefined` for any matching item, no item is replaced.
|
|
126
|
+
|
|
127
|
+
**Ids are immutable.** Once a value is stored, never change its id — neither in place nor through the object `update()`'s updater returns. bajadab doesn't check this; a changed id leaves the collection inconsistent (a `MAP` entry stored under its old key, a duplicate id in an `ARRAY`). To "rename" a value, `delete()` it and `add()` it under the new id.
|
|
128
|
+
|
|
129
|
+
## Persistence and file layout
|
|
130
|
+
|
|
131
|
+
Each database directory contains:
|
|
132
|
+
|
|
133
|
+
- `dbmeta.json` — the database config and a name → id map for its collections.
|
|
134
|
+
- `<id>.json` — per-collection metadata (name, config, id).
|
|
135
|
+
- `<id>_data/data.json` — the collection's actual values.
|
|
136
|
+
|
|
137
|
+
Collections are addressed by a stable id, not by name, so a handle to a since-renamed-or-recreated collection can't end up reading or writing the wrong data. Every write goes through a temp-file-then-rename step to avoid partial writes. A rename that fails with `EPERM`/`EBUSY`/`EACCES` (typically a virus scanner or indexer briefly holding the file on Windows) is retried a few times before giving up; a failed write never leaves its temp file behind. On POSIX systems, created files get mode `0600` and directories `0700`; this has no effect on Windows, which has no equivalent permission bits.
|
|
138
|
+
|
|
139
|
+
## Concurrency
|
|
140
|
+
|
|
141
|
+
All operations on a database — on any of its collections as well as `create`/`get`/`has`/`dropCollection()`, `snapshot()` and `transaction()` — are serialized within one process by a single lock per database. The lock is reentrant: code that runs while an operation holds it (handlers, predicates, updaters, a transaction's `fn`) can call any other operation on the same database without deadlocking. The flip side is that such code blocks the whole database while it runs, so keep `EBHandlerMode.SYNC` handlers and transaction callbacks short, and don't await anything slow or external inside them. There is no cross-process locking — multiple processes pointed at the same directory can still race on the underlying files.
|
|
142
|
+
|
|
143
|
+
## Snapshots
|
|
144
|
+
|
|
145
|
+
`db.snapshot(targetDir, options?)` flushes every loaded collection, then copies the whole database directory into a new version folder under `targetDir` (created if it doesn't exist yet). The version folder is named after a sortable timestamp (e.g. `20260904T153012345Z`) and is itself a complete, independent database — open it directly with `getDatabase(versionPath)`.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const versionPath = await db.snapshot("./backups");
|
|
149
|
+
// later, e.g. after a bad deploy:
|
|
150
|
+
const restored = await getDatabase(versionPath);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`targetDir` is a pool that can hold several versions; each call adds one more. Options:
|
|
154
|
+
|
|
155
|
+
| Option | Values | Meaning |
|
|
156
|
+
| --- | --- | --- |
|
|
157
|
+
| `label` | string (optional) | Appended to the version folder's timestamp, e.g. `..._before-deploy`. Letters, digits, `-` and `_` only. |
|
|
158
|
+
| `keepLast` | number (optional) | Older version folders under `targetDir` to keep besides the one just created. `0` keeps only the new one. Omit to keep everything. Negative values reject. |
|
|
159
|
+
|
|
160
|
+
Pruning only ever deletes folders matching bajadab's own naming scheme — anything else you keep in `targetDir` is left alone.
|
|
161
|
+
|
|
162
|
+
Version folder names have millisecond resolution. Two `snapshot()` calls into the same `targetDir` within the same millisecond (and with the same or no `label`) resolve to the same folder name: the second copy is merged into the first, and if that copy fails, the cleanup removes the earlier snapshot along with it. Don't take snapshots into the same `targetDir` in such quick succession.
|
|
163
|
+
|
|
164
|
+
`snapshot()` blocks every other operation on the database for as long as the copy takes, to guarantee a consistent point-in-time copy. There's no separate `restoreSnapshot()` — restoring means copying a version folder back over the original `basePath` (while nothing has it open), or just pointing `getDatabase()` at the version folder directly.
|
|
165
|
+
|
|
166
|
+
## Transactions
|
|
167
|
+
|
|
168
|
+
`db.transaction(collectionNames, fn)` runs `fn` against isolated, in-memory working copies of the named collections. Reads and writes inside `fn` only see the transaction's own state until it resolves:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
await db.transaction(["accounts", "orders"], async (tx) => {
|
|
172
|
+
const accounts = await tx.getCollection<Account>("accounts");
|
|
173
|
+
const orders = await tx.getCollection<Order>("orders");
|
|
174
|
+
|
|
175
|
+
await accounts.update(a => a.id === "u1", async a => ({ ...a, balance: a.balance - 10 }));
|
|
176
|
+
await orders.add({ userId: "u1", total: 10 });
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Every name passed to `transaction()` must be an existing collection; otherwise it rejects before `fn` runs, and nothing is created.
|
|
181
|
+
|
|
182
|
+
If `fn` resolves, every declared collection's working copy is first written to a temp file. If any of these writes fails, all temp files are discarded and nothing is applied. Otherwise the temp files replace the collections' files, every live state is replaced by its working copy, and only then do the `ADD`/`UPDATE`/`DELETE` handlers registered with `on()` fire — so a handler that reads or writes another declared collection already sees its committed state. They fire once per id the transaction touched, typed by the last operation on it, the same way as outside a transaction: `ADD` if the value is new or was last stored via `add()` (including an id-based overwrite), `UPDATE` (pre-transaction value → final value) if it was last changed via `update()`, `DELETE` if it's gone. A value added and later deleted within the same transaction fires nothing, and a value updated twice fires one `UPDATE`. If `fn` throws, nothing is applied, nothing is flushed, and no handler fires — every declared collection stays exactly as it was. This only covers changes made through the working copies' `add()`/`update()`/`delete()`; values mutated in place are shared with the live collection and aren't isolated (see "Values are stored by reference").
|
|
183
|
+
|
|
184
|
+
`tx.getCollection(name)` only accepts names passed to `transaction()`; anything else throws. It mirrors `IBCollection`'s read/write surface (`get`/`has`/`count`/`getByIndex`/`add`/`update`/`delete`) but not `flush`/`reload`/`unload`/`on` — those don't make sense against a working copy that might still be rolled back.
|
|
185
|
+
|
|
186
|
+
Inside `fn`, `add()`/`update()`/`delete()` through a declared collection's regular handle (the one from `db.getCollection()`) reject — those writes would otherwise be overwritten by the commit. Reading through it is fine and shows the pre-transaction state; collections not declared to the transaction stay fully usable, but writes to them aren't part of the transaction. Calling `transaction()` again from inside `fn` rejects as well, whichever collections it names.
|
|
187
|
+
|
|
188
|
+
`transaction()` holds the database lock for its whole duration, so every other operation on the database — on any collection — waits until it resolves. Across collections, the final renames at commit still happen one file at a time — a crash, or a rename that keeps failing, between them can leave a transaction partially applied. If a rename fails, the collections renamed so far are applied (on disk and in memory, and their handlers fire), the rest are discarded, and `transaction()` rejects. There's no cross-process locking here either, same as everywhere else in bajadab.
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
See [LICENSE](./LICENSE).
|
package/dist/base.d.ts
CHANGED
|
@@ -97,12 +97,16 @@ export interface IBDatabase {
|
|
|
97
97
|
*/
|
|
98
98
|
snapshot(targetDir: string, options?: IBSnapshotOptions): Promise<string>;
|
|
99
99
|
/**
|
|
100
|
-
* Runs `fn` against isolated, in-memory working copies of `collectionNames
|
|
101
|
-
* are visible only within the transaction. If `fn` resolves, every working copy
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* transaction
|
|
105
|
-
*
|
|
100
|
+
* Runs `fn` against isolated, in-memory working copies of `collectionNames`, which must all exist. Reads and
|
|
101
|
+
* writes inside `fn` are visible only within the transaction. If `fn` resolves, every working copy is written
|
|
102
|
+
* to a temp file first - if any of those writes fails, nothing is applied - then all of them replace their
|
|
103
|
+
* files and their collections' live state, and only then do `ADD`/`UPDATE`/`DELETE` handlers fire: once per
|
|
104
|
+
* id the transaction touched, typed by the last operation on it, same as outside a transaction - a value
|
|
105
|
+
* added and later deleted within the same transaction fires nothing. If `fn` throws, nothing is applied,
|
|
106
|
+
* nothing is flushed, and no handler fires.
|
|
107
|
+
* Only collections named in `collectionNames` can be reached from within `fn`; writing to one of them through
|
|
108
|
+
* its regular {@link IBCollection} handle inside `fn` rejects, and so does a nested `transaction()` call.
|
|
109
|
+
* Values mutated in place are shared with the live collection and are neither isolated nor reverted.
|
|
106
110
|
*/
|
|
107
111
|
transaction(collectionNames: string[], fn: (tx: IBTransaction) => Promise<void>): Promise<void>;
|
|
108
112
|
}
|
|
@@ -141,7 +145,7 @@ export interface IBCollection<T = any> {
|
|
|
141
145
|
readonly collectionName: string;
|
|
142
146
|
/** Stable identity used to address this collection's storage, independent of its name. */
|
|
143
147
|
readonly collectionID: string;
|
|
144
|
-
/** All items, or only those matching `predicate`.
|
|
148
|
+
/** All items, or only those matching `predicate`. A new array, but holding the stored objects themselves - don't mutate them in place. */
|
|
145
149
|
get(predicate?: (v: T) => boolean): Promise<T[]>;
|
|
146
150
|
/** Deletes matching items. Returns the number removed. */
|
|
147
151
|
delete(predicate: (v: T) => boolean): Promise<number>;
|
|
@@ -149,11 +153,11 @@ export interface IBCollection<T = any> {
|
|
|
149
153
|
has(predicate: (v: T) => boolean): Promise<boolean>;
|
|
150
154
|
/** Adds values, resolving/assigning ids per the collection's id strategies. Returns the number added. */
|
|
151
155
|
add(...v: T[]): Promise<number>;
|
|
152
|
-
/** Replaces each item matching `predicate` with the (defined) result of `updater`. Returns the number updated. */
|
|
156
|
+
/** Replaces each item matching `predicate` with the (defined) result of `updater`; all-or-nothing if `updater` throws or returns `undefined`. `updater` receives the stored object - return a new one rather than mutating it, and never change its id. Returns the number updated. */
|
|
153
157
|
update(predicate: (v: T) => boolean, updater: (v: T) => Promise<T>): Promise<number>;
|
|
154
158
|
/** Number of items, or only those matching `predicate`. */
|
|
155
159
|
count(predicate?: (v: T) => boolean): Promise<number>;
|
|
156
|
-
/** Items whose {@link IBCollectionConfig.indexes}-configured `indexKey` extracts `key`.
|
|
160
|
+
/** Items whose {@link IBCollectionConfig.indexes}-configured `indexKey` extracts `key`. A new array, but holding the stored objects themselves. Throws if `indexKey` isn't configured. */
|
|
157
161
|
getByIndex(indexKey: string, key: string): Promise<T[]>;
|
|
158
162
|
/** 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. */
|
|
159
163
|
on(modificationType: EBModificationType.ADD, handler: TAddHandler<T>, mode?: EBHandlerMode): Promise<IHandlerRegistration>;
|
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;AAyED,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,9 +1,14 @@
|
|
|
1
|
-
import type { IODatabase } from "./fileio.js";
|
|
1
|
+
import type { IODatabase, IPreparedWrite } from "./fileio.js";
|
|
2
2
|
import { EBHandlerMode, EBModificationType, IBCollection, IBCollectionConfig, IBDatabase, IBDatabaseConfig, IBFunctionRegistry, IBSnapshotOptions, IBTransaction, 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`. */
|
|
6
6
|
export declare const DEFAULT_SET_ID: TSetIDFunction;
|
|
7
|
+
export interface IPreparedTransactionCommit {
|
|
8
|
+
readonly write: IPreparedWrite;
|
|
9
|
+
apply(): void;
|
|
10
|
+
fire(): Promise<void>;
|
|
11
|
+
}
|
|
7
12
|
/** {@link IBCollectionConfig} used when a collection is created without an explicit one. */
|
|
8
13
|
export declare const DEFAULT_COLLECTION_CONFIG: IBCollectionConfig;
|
|
9
14
|
/** {@link IBDatabaseConfig} used when `getDatabase()` creates a fresh database. */
|
|
@@ -13,7 +18,11 @@ export declare class BDatabase implements IBDatabase {
|
|
|
13
18
|
private collections;
|
|
14
19
|
readonly io: IODatabase;
|
|
15
20
|
readonly functionRegistry?: IBFunctionRegistry;
|
|
21
|
+
readonly iid: string;
|
|
22
|
+
private readonly transactionScope;
|
|
16
23
|
constructor(io: IODatabase, config?: IBDatabaseConfig, functionRegistry?: IBFunctionRegistry);
|
|
24
|
+
runExclusive<R>(fn: () => Promise<R>): Promise<R>;
|
|
25
|
+
isInTransaction(collectionName: string): boolean;
|
|
17
26
|
addCollection(collection: BCollection): void;
|
|
18
27
|
getCollection<T>(collectionName: string, collectionConfig?: IBCollectionConfig): Promise<IBCollection<T>>;
|
|
19
28
|
createCollection<T = any>(collectionName: string, collectionConfig?: IBCollectionConfig): Promise<BCollection<T>>;
|
|
@@ -32,15 +41,19 @@ export declare class BCollection<T = any> implements IBCollection<T> {
|
|
|
32
41
|
private loaded;
|
|
33
42
|
private dirty;
|
|
34
43
|
private dropped;
|
|
44
|
+
private transactionClone;
|
|
35
45
|
private addHandlers;
|
|
36
46
|
private updateHandlers;
|
|
37
47
|
private deleteHandlers;
|
|
38
48
|
private indexDefinitions;
|
|
39
49
|
private indexRuntime;
|
|
50
|
+
private indexedKeys;
|
|
51
|
+
private txChanges?;
|
|
40
52
|
constructor(database: BDatabase, collectionID: string, collectionName: string, collectionConfig: IBCollectionConfig);
|
|
41
53
|
private checkLoaded;
|
|
42
54
|
private runExclusive;
|
|
43
55
|
private runProtected;
|
|
56
|
+
private runMutation;
|
|
44
57
|
/**
|
|
45
58
|
* call this only within the protecting mutex
|
|
46
59
|
*/
|
|
@@ -60,6 +73,8 @@ export declare class BCollection<T = any> implements IBCollection<T> {
|
|
|
60
73
|
private resolveIndexDefinitions;
|
|
61
74
|
private indexKeysFor;
|
|
62
75
|
private addToIndexes;
|
|
76
|
+
private indexKeysForAll;
|
|
77
|
+
private addKeysToIndexes;
|
|
63
78
|
private removeFromIndexes;
|
|
64
79
|
/**
|
|
65
80
|
* full rebuild from currently loaded data; only needed right after (re)load, since every
|
|
@@ -70,10 +85,14 @@ export declare class BCollection<T = any> implements IBCollection<T> {
|
|
|
70
85
|
/**
|
|
71
86
|
* id -> value for every stored item, resolved via the plain id getter (no auto-id assignment - every
|
|
72
87
|
* stored item already has one). Used to diff a transaction's working copy against the pre-transaction
|
|
73
|
-
* state in {@link
|
|
88
|
+
* state in {@link prepareTransactionCommit}.
|
|
74
89
|
*/
|
|
75
90
|
private idKeyedSnapshot;
|
|
76
|
-
private
|
|
91
|
+
private resolveIDSetter;
|
|
92
|
+
private resolveIncomingIDs;
|
|
93
|
+
private assignMissingIDs;
|
|
94
|
+
private storedArrayIDs;
|
|
95
|
+
private recordTxChanges;
|
|
77
96
|
add(...vs: T[]): Promise<number>;
|
|
78
97
|
update(predicate: (v: T) => boolean, updater: (v: T) => Promise<T>): Promise<number>;
|
|
79
98
|
private doOnDirty;
|
|
@@ -88,12 +107,10 @@ export declare class BCollection<T = any> implements IBCollection<T> {
|
|
|
88
107
|
*/
|
|
89
108
|
cloneForTransaction(): Promise<BCollection<T>>;
|
|
90
109
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* least one handler is registered on this collection - a transaction on a collection nobody listens to
|
|
96
|
-
* skips it entirely. Call only while already holding this collection's mutex for the whole transaction.
|
|
110
|
+
* Writes `clone`'s state to a temp file (see {@link IPreparedWrite}) and computes the handler events for the
|
|
111
|
+
* net changes against this collection's current state: one per id the transaction touched, typed by the last
|
|
112
|
+
* operation on it (`DELETE` if gone, `ADD` if new or last stored via `add()`, `UPDATE` otherwise). Nothing is
|
|
113
|
+
* applied until `apply()`; call only while already holding this collection's mutex for the whole transaction.
|
|
97
114
|
*/
|
|
98
|
-
|
|
115
|
+
prepareTransactionCommit(clone: BCollection<T>): Promise<IPreparedTransactionCommit>;
|
|
99
116
|
}
|