@bodil/bdb 0.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/LICENCE.md +288 -0
- package/dist/backend.d.ts +34 -0
- package/dist/backend.js +97 -0
- package/dist/backend.js.map +1 -0
- package/dist/broadcast.d.ts +9 -0
- package/dist/broadcast.js +20 -0
- package/dist/broadcast.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.js +141 -0
- package/dist/index.test.js.map +1 -0
- package/dist/indices.d.ts +56 -0
- package/dist/indices.js +56 -0
- package/dist/indices.js.map +1 -0
- package/dist/query.d.ts +49 -0
- package/dist/query.js +148 -0
- package/dist/query.js.map +1 -0
- package/dist/serial.d.ts +6 -0
- package/dist/serial.js +92 -0
- package/dist/serial.js.map +1 -0
- package/dist/storage.d.ts +39 -0
- package/dist/storage.js +69 -0
- package/dist/storage.js.map +1 -0
- package/dist/table.d.ts +75 -0
- package/dist/table.js +223 -0
- package/dist/table.js.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +86 -0
- package/src/backend.ts +137 -0
- package/src/broadcast.ts +27 -0
- package/src/index.test.ts +200 -0
- package/src/index.ts +76 -0
- package/src/indices.ts +108 -0
- package/src/query.ts +233 -0
- package/src/serial.ts +92 -0
- package/src/storage.ts +83 -0
- package/src/table.ts +302 -0
- package/src/types.ts +16 -0
package/dist/table.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { assert, isIterable } from "@bodil/core/assert";
|
|
2
|
+
import { Emitter } from "@bodil/core/async";
|
|
3
|
+
import { DisposableContext } from "@bodil/core/disposable";
|
|
4
|
+
import { Signal } from "@bodil/signal";
|
|
5
|
+
import { freeze, produce } from "immer";
|
|
6
|
+
import BTree from "sorted-btree";
|
|
7
|
+
import { CustomIndex } from "./indices";
|
|
8
|
+
import { IndexQuery, IteratorDirection } from "./query";
|
|
9
|
+
import { TableStorage } from "./storage";
|
|
10
|
+
export class Table extends Emitter {
|
|
11
|
+
#storage;
|
|
12
|
+
#signals;
|
|
13
|
+
#signalCleanupRegistry;
|
|
14
|
+
#context;
|
|
15
|
+
constructor(primaryIndex) {
|
|
16
|
+
super();
|
|
17
|
+
this.ready = Promise.resolve();
|
|
18
|
+
this.primary = new BTree();
|
|
19
|
+
this.indices = {};
|
|
20
|
+
this.indexTables = {};
|
|
21
|
+
this.changed = Signal(0);
|
|
22
|
+
this.#signals = new BTree();
|
|
23
|
+
this.#signalCleanupRegistry = new FinalizationRegistry(this.#signalCleanup.bind(this));
|
|
24
|
+
this.#context = new DisposableContext();
|
|
25
|
+
this.primaryIndex = primaryIndex;
|
|
26
|
+
}
|
|
27
|
+
[Symbol.dispose]() {
|
|
28
|
+
super[Symbol.dispose]();
|
|
29
|
+
this.#context.dispose();
|
|
30
|
+
}
|
|
31
|
+
withIndex(index) {
|
|
32
|
+
assert(this.indices[index.name] === undefined, `duplicate index definition: "${index.name}"`);
|
|
33
|
+
this.indices[index.name] = index;
|
|
34
|
+
this.indexTables[index.name] =
|
|
35
|
+
index instanceof CustomIndex ? index.makeIndex() : new BTree();
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
withStorage(backend, name) {
|
|
39
|
+
this.#storage = new TableStorage(backend, name, this.primaryIndex);
|
|
40
|
+
this.ready = this.#storage.ready;
|
|
41
|
+
this.#context.use(this.#storage.on((event) => {
|
|
42
|
+
switch (event.type) {
|
|
43
|
+
case "update":
|
|
44
|
+
this.add(event.item);
|
|
45
|
+
break;
|
|
46
|
+
case "delete":
|
|
47
|
+
this.delete(event.key);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}));
|
|
51
|
+
void this.#storage.link(this);
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
signal(primaryKey) {
|
|
55
|
+
const active = this.#signals.get(primaryKey)?.deref();
|
|
56
|
+
if (active !== undefined) {
|
|
57
|
+
return active.readOnly();
|
|
58
|
+
}
|
|
59
|
+
const sig = Signal(this.get(primaryKey), {
|
|
60
|
+
equals: () => false,
|
|
61
|
+
});
|
|
62
|
+
this.#signalCleanupRegistry.register(sig, primaryKey);
|
|
63
|
+
this.#signals.set(primaryKey, new WeakRef(sig));
|
|
64
|
+
return sig.readOnly();
|
|
65
|
+
}
|
|
66
|
+
get(primaryKey) {
|
|
67
|
+
return this.primary.get(primaryKey);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Given a primary key, apply an update function to the value stored under
|
|
71
|
+
* that key. If there's no value, call the create function to create the
|
|
72
|
+
* value before passing it to the update function.
|
|
73
|
+
*/
|
|
74
|
+
createAndUpdate(primaryKey, create, update) {
|
|
75
|
+
const oldItem = this.primary.get(primaryKey) ?? create();
|
|
76
|
+
const item = produce(oldItem, update);
|
|
77
|
+
return this.#setItem(item, oldItem, primaryKey);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Given a primary key, apply an update function to the value stored under
|
|
81
|
+
* that key. If there's no value, call the create function to create a new
|
|
82
|
+
* value. In this case, the update function is not called.
|
|
83
|
+
*/
|
|
84
|
+
createOrUpdate(primaryKey, create, update) {
|
|
85
|
+
const oldItem = this.primary.get(primaryKey);
|
|
86
|
+
const item = oldItem === undefined ? create() : produce(oldItem, update);
|
|
87
|
+
return this.#setItem(item, oldItem, primaryKey);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Apply an update function to the value stored under the given primary key.
|
|
91
|
+
* If no value exists, the update function is not called and `undefined` is
|
|
92
|
+
* returned. Otherwise, the updated value is returned.
|
|
93
|
+
*/
|
|
94
|
+
update(primaryKey, update) {
|
|
95
|
+
const oldItem = this.primary.get(primaryKey);
|
|
96
|
+
if (oldItem === undefined) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const item = produce(oldItem, update);
|
|
100
|
+
return this.#setItem(item, oldItem, primaryKey);
|
|
101
|
+
}
|
|
102
|
+
add(...items) {
|
|
103
|
+
return this.#addFrom(items.length === 1 && isIterable(items[0]) ? items[0] : items);
|
|
104
|
+
}
|
|
105
|
+
delete(...items) {
|
|
106
|
+
return this.#deleteFrom(items.length === 1 && isIterable(items[0]) ? items[0] : items);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Delete all data from the table.
|
|
110
|
+
*/
|
|
111
|
+
clear() {
|
|
112
|
+
this.delete(...this.primary.keys());
|
|
113
|
+
}
|
|
114
|
+
[Symbol.iterator]() {
|
|
115
|
+
return this.primary.values();
|
|
116
|
+
}
|
|
117
|
+
where(index) {
|
|
118
|
+
return new IndexQuery(this, index, IteratorDirection.Ascending);
|
|
119
|
+
}
|
|
120
|
+
orderBy(index) {
|
|
121
|
+
return this.where(index);
|
|
122
|
+
}
|
|
123
|
+
size() {
|
|
124
|
+
return this.primary.size;
|
|
125
|
+
}
|
|
126
|
+
#addFrom(items) {
|
|
127
|
+
for (const item of items) {
|
|
128
|
+
const key = this.primaryIndex.extractKey(item);
|
|
129
|
+
this.#setItem(item, this.primary.get(key), key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
#deleteFrom(primaryKeys) {
|
|
133
|
+
let deleted = 0;
|
|
134
|
+
for (const primaryKey of primaryKeys) {
|
|
135
|
+
const item = this.primary.get(primaryKey);
|
|
136
|
+
if (item !== undefined) {
|
|
137
|
+
this.#deleteItem(item, primaryKey);
|
|
138
|
+
deleted++;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return deleted;
|
|
142
|
+
}
|
|
143
|
+
#setItem(item, oldItem, key) {
|
|
144
|
+
freeze(item, true);
|
|
145
|
+
// stop now if the new value is the same as the old one
|
|
146
|
+
if (Object.is(oldItem, item)) {
|
|
147
|
+
return item;
|
|
148
|
+
}
|
|
149
|
+
// remove the old item from indices
|
|
150
|
+
if (oldItem !== undefined) {
|
|
151
|
+
this.#deleteFromIndices(oldItem);
|
|
152
|
+
}
|
|
153
|
+
// store the new item under its primary key, overwriting the old value
|
|
154
|
+
this.primary.set(key, item, true);
|
|
155
|
+
// add new item to indices
|
|
156
|
+
for (const property of Object.keys(this.indices)) {
|
|
157
|
+
const index = this.indices[property];
|
|
158
|
+
const table = this.indexTables[property];
|
|
159
|
+
const keys = index.extractKeys(item);
|
|
160
|
+
for (const key of keys) {
|
|
161
|
+
const record = table.get(key);
|
|
162
|
+
if (record === undefined) {
|
|
163
|
+
table.set(key, [item]);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
record.push(item);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// emit an update event and update signals
|
|
171
|
+
this.#withSignal(this.primaryIndex.extractKey(item), (sig) => sig.set(item));
|
|
172
|
+
this.emit({ type: "update", item });
|
|
173
|
+
this.changed.update((i) => i + 1);
|
|
174
|
+
return item;
|
|
175
|
+
}
|
|
176
|
+
#deleteItem(item, primaryKey) {
|
|
177
|
+
// remove the item from indices
|
|
178
|
+
this.#deleteFromIndices(item);
|
|
179
|
+
// remove the item from the primary index
|
|
180
|
+
this.primary.delete(primaryKey);
|
|
181
|
+
// emit a delete event and update signals
|
|
182
|
+
this.#withSignal(primaryKey, (sig) => sig.set(undefined));
|
|
183
|
+
this.emit({ type: "delete", key: primaryKey });
|
|
184
|
+
this.changed.update((i) => i + 1);
|
|
185
|
+
}
|
|
186
|
+
#withSignal(primaryKey, fn) {
|
|
187
|
+
const sigRef = this.#signals.get(primaryKey);
|
|
188
|
+
const sig = sigRef?.deref();
|
|
189
|
+
if (sig !== undefined) {
|
|
190
|
+
// Only run the function if we have a live signal
|
|
191
|
+
return fn(sig);
|
|
192
|
+
}
|
|
193
|
+
else if (sigRef !== undefined) {
|
|
194
|
+
// Remove dangling reference from signal map
|
|
195
|
+
this.#signals.delete(primaryKey);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
#deleteFromIndices(item) {
|
|
199
|
+
for (const property of Object.keys(this.indices)) {
|
|
200
|
+
const index = this.indices[property];
|
|
201
|
+
const table = this.indexTables[property];
|
|
202
|
+
const keys = index.extractKeys(item);
|
|
203
|
+
for (const key of keys) {
|
|
204
|
+
const record = table.get(key);
|
|
205
|
+
if (record !== undefined) {
|
|
206
|
+
const recordIndex = record.indexOf(item);
|
|
207
|
+
if (recordIndex >= 0) {
|
|
208
|
+
if (record.length === 1) {
|
|
209
|
+
table.delete(key);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
record.splice(recordIndex, 1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
#signalCleanup(primaryKey) {
|
|
220
|
+
this.#signals.delete(primaryKey);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table.js","sourceRoot":"","sources":["../src/table.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type IndexablePrimitive = string | number | null | undefined | bigint;
|
|
2
|
+
export type IndexableType = IndexablePrimitive | Array<IndexablePrimitive> | ReadonlyArray<IndexablePrimitive>;
|
|
3
|
+
export type CustomIndexablesOf<A, T> = string & keyof A & {
|
|
4
|
+
[Key in keyof A]: A[Key] extends T ? Key : never;
|
|
5
|
+
}[keyof A];
|
|
6
|
+
export type IndexablesOf<A> = CustomIndexablesOf<A, IndexableType>;
|
|
7
|
+
export type PrimitiveIndexablesOf<A> = CustomIndexablesOf<A, IndexablePrimitive>;
|
|
8
|
+
export type ArrayIndexablesOf<A> = CustomIndexablesOf<A, Array<IndexablePrimitive> | ReadonlyArray<IndexablePrimitive>>;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bodil/bdb",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A signal enabled database",
|
|
5
|
+
"homepage": "https://codeberg.org/bodil/bdb",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://codeberg.org/bodil/bdb.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "EUPL-1.2+",
|
|
11
|
+
"module": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"import": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@bodil/core": "^0.4.9",
|
|
30
|
+
"@bodil/opt": "^0.4.2",
|
|
31
|
+
"@bodil/signal": "^0.3.3",
|
|
32
|
+
"immer": "^11.0.1",
|
|
33
|
+
"sorted-btree": "2.0.0-alpha.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@eslint/eslintrc": "^3.3.3",
|
|
37
|
+
"@eslint/js": "^9.39.1",
|
|
38
|
+
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^8.49.0",
|
|
40
|
+
"@typescript-eslint/parser": "^8.49.0",
|
|
41
|
+
"eslint": "^9.39.1",
|
|
42
|
+
"eslint-config-prettier": "^10.1.8",
|
|
43
|
+
"eslint-plugin-jsdoc": "^54.7.0",
|
|
44
|
+
"fake-indexeddb": "^6.2.5",
|
|
45
|
+
"globals": "^16.5.0",
|
|
46
|
+
"npm-run-all2": "^8.0.4",
|
|
47
|
+
"prettier": "^3.7.4",
|
|
48
|
+
"temporal-polyfill": "^0.3.0",
|
|
49
|
+
"typedoc": "^0.28.15",
|
|
50
|
+
"typedoc-plugin-extras": "^4.0.1",
|
|
51
|
+
"typedoc-plugin-mdn-links": "^5.0.10",
|
|
52
|
+
"typescript": "^5.9.3",
|
|
53
|
+
"vitest": "^3.2.4"
|
|
54
|
+
},
|
|
55
|
+
"prettier": {
|
|
56
|
+
"editorconfig": true,
|
|
57
|
+
"plugins": [
|
|
58
|
+
"@ianvs/prettier-plugin-sort-imports"
|
|
59
|
+
],
|
|
60
|
+
"importOrderParserPlugins": [
|
|
61
|
+
"typescript",
|
|
62
|
+
"decorators"
|
|
63
|
+
],
|
|
64
|
+
"importOrderTypeScriptVersion": "5.0.0",
|
|
65
|
+
"importOrder": [
|
|
66
|
+
"",
|
|
67
|
+
"<BUILTIN_MODULES>",
|
|
68
|
+
"",
|
|
69
|
+
"<THIRD_PARTY_MODULES>",
|
|
70
|
+
"",
|
|
71
|
+
"^[.](?!.*[?]inline$)",
|
|
72
|
+
"",
|
|
73
|
+
"[?]inline$"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"build": "tsc",
|
|
78
|
+
"test": "vitest --run",
|
|
79
|
+
"check:eslint": "eslint src",
|
|
80
|
+
"check:tsc": "tsc --noEmit",
|
|
81
|
+
"check": "run-p check:tsc check:eslint",
|
|
82
|
+
"doc": "typedoc",
|
|
83
|
+
"doc:readthedocs": "typedoc --out $READTHEDOCS_OUTPUT/html",
|
|
84
|
+
"prepublish": "tsc"
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/backend.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Broadcaster } from "./broadcast";
|
|
2
|
+
import { deserialise, serialise, type Serialised } from "./serial";
|
|
3
|
+
import type { IndexablePrimitive } from "./types";
|
|
4
|
+
|
|
5
|
+
export type DatabaseBroadcast = { event: "update" | "delete"; table: string; key: unknown };
|
|
6
|
+
|
|
7
|
+
export type IndexedDBBackendConfig = {
|
|
8
|
+
name: string;
|
|
9
|
+
version: number;
|
|
10
|
+
table?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const backendConfigDefaults = {
|
|
14
|
+
table: "store",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type StorageRecord = { table: string; key: IndexablePrimitive; value: Serialised };
|
|
18
|
+
|
|
19
|
+
function request<A>(req: IDBRequest): Promise<A> {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
req.onerror = () => reject(req.error as Error);
|
|
22
|
+
req.onsuccess = () => resolve(req.result);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function unpackRecord<A = unknown>(record: StorageRecord): A {
|
|
27
|
+
return deserialise(record.value) as A;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export abstract class StorageBackend {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
|
|
33
|
+
abstract get<A = unknown>(table: string, key: unknown): Promise<A>;
|
|
34
|
+
abstract getAll<A = unknown>(table: string): Promise<Array<A>>;
|
|
35
|
+
abstract update(table: string, key: unknown, value: unknown): Promise<void>;
|
|
36
|
+
abstract delete(table: string, key: unknown): Promise<void>;
|
|
37
|
+
|
|
38
|
+
protected readonly broadcast?: Broadcaster<DatabaseBroadcast>;
|
|
39
|
+
|
|
40
|
+
protected constructor(name: string) {
|
|
41
|
+
this.name = name;
|
|
42
|
+
if (Object.hasOwn(globalThis, "BroadcastChannel")) {
|
|
43
|
+
this.broadcast = new Broadcaster<DatabaseBroadcast>(`memdb-broadcast-${name}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onBroadcast(callback: (message: DatabaseBroadcast) => void): Disposable | undefined {
|
|
48
|
+
return this.broadcast?.on(callback);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class IndexedDBBackend extends StorageBackend {
|
|
53
|
+
private db?: IDBDatabase;
|
|
54
|
+
|
|
55
|
+
readonly version: number;
|
|
56
|
+
readonly table: string;
|
|
57
|
+
|
|
58
|
+
static async open(config: IndexedDBBackendConfig): Promise<IndexedDBBackend> {
|
|
59
|
+
const c = { ...backendConfigDefaults, ...config };
|
|
60
|
+
const storage = new IndexedDBBackend(c.name, c.version, c.table);
|
|
61
|
+
await storage.open();
|
|
62
|
+
return storage;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
protected constructor(name: string, version: number, table: string) {
|
|
66
|
+
super(name);
|
|
67
|
+
this.version = version;
|
|
68
|
+
this.table = table;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private async open(): Promise<void> {
|
|
72
|
+
const resolver = Promise.withResolvers<void>();
|
|
73
|
+
const request = indexedDB.open(this.name, this.version);
|
|
74
|
+
request.onerror = (event: Event) => {
|
|
75
|
+
const error = (event.target as IDBOpenDBRequest).error as Error;
|
|
76
|
+
resolver.reject(error);
|
|
77
|
+
};
|
|
78
|
+
request.onsuccess = (event: Event) => {
|
|
79
|
+
this.db = (event.target as IDBOpenDBRequest).result;
|
|
80
|
+
this.db.onerror = null;
|
|
81
|
+
resolver.resolve();
|
|
82
|
+
};
|
|
83
|
+
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
|
|
84
|
+
const db = (event.target as IDBOpenDBRequest).result;
|
|
85
|
+
db.onerror = (event) => {
|
|
86
|
+
const error = (event.target as IDBOpenDBRequest).error as Error;
|
|
87
|
+
resolver.reject(error);
|
|
88
|
+
};
|
|
89
|
+
const store = db.createObjectStore(this.table, { keyPath: ["table", "key"] });
|
|
90
|
+
store.createIndex("table", "table", { unique: false });
|
|
91
|
+
};
|
|
92
|
+
return resolver.promise;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private transaction<A>(
|
|
96
|
+
mode: "readonly" | "readwrite",
|
|
97
|
+
job: (store: IDBObjectStore) => Promise<A>,
|
|
98
|
+
): Promise<A> {
|
|
99
|
+
if (this.db === undefined) {
|
|
100
|
+
throw new Error("database not ready");
|
|
101
|
+
}
|
|
102
|
+
return new Promise((resolve, reject: (reason?: unknown) => void) => {
|
|
103
|
+
const tx = this.db!.transaction([this.table], mode);
|
|
104
|
+
tx.onerror = () => reject(tx.error as Error);
|
|
105
|
+
tx.oncomplete = async () => resolve(await result);
|
|
106
|
+
const result = job(tx.objectStore(this.table)).catch(reject) as Promise<A>;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async get<A = unknown>(table: string, key: unknown): Promise<A> {
|
|
111
|
+
const value: StorageRecord = await this.transaction("readonly", (store) => {
|
|
112
|
+
return request(store.get([table, key] as IDBValidKey));
|
|
113
|
+
});
|
|
114
|
+
return unpackRecord(value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async getAll<A = unknown>(table: string): Promise<Array<A>> {
|
|
118
|
+
const values = await this.transaction("readonly", (store) => {
|
|
119
|
+
return request(store.index("table").getAll(table));
|
|
120
|
+
});
|
|
121
|
+
return (values as Array<StorageRecord>).map(unpackRecord<A>);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async update(table: string, key: unknown, value: unknown): Promise<void> {
|
|
125
|
+
await this.transaction("readwrite", (store) => {
|
|
126
|
+
return request(store.put({ table, key, value: serialise(value) }));
|
|
127
|
+
});
|
|
128
|
+
this.broadcast?.send({ event: "update", table, key });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async delete(table: string, key: unknown): Promise<void> {
|
|
132
|
+
await this.transaction("readwrite", (store) => {
|
|
133
|
+
return request(store.delete([table, key as IDBValidKey]));
|
|
134
|
+
});
|
|
135
|
+
this.broadcast?.send({ event: "delete", table, key });
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/broadcast.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Emitter } from "@bodil/core/async";
|
|
2
|
+
import { DisposableContext, eventListener } from "@bodil/core/disposable";
|
|
3
|
+
|
|
4
|
+
/** @internal */
|
|
5
|
+
export class Broadcaster<MessageType> extends Emitter<MessageType> implements Disposable {
|
|
6
|
+
private readonly channel: BroadcastChannel;
|
|
7
|
+
private readonly context = new DisposableContext();
|
|
8
|
+
|
|
9
|
+
constructor(name: string) {
|
|
10
|
+
super();
|
|
11
|
+
this.channel = new BroadcastChannel(name);
|
|
12
|
+
this.context.use(() => this.channel.close());
|
|
13
|
+
this.context.use(
|
|
14
|
+
eventListener(this.channel, "message", ((event: MessageEvent<MessageType>) =>
|
|
15
|
+
this.emit(event.data)) as EventListener),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override [Symbol.dispose]() {
|
|
20
|
+
super[Symbol.dispose]();
|
|
21
|
+
this.context.dispose();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
send(message: MessageType) {
|
|
25
|
+
this.channel.postMessage(message);
|
|
26
|
+
}
|
|
27
|
+
}
|