@moltendb-web/core 1.8.0 β 2.0.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 +164 -123
- package/dist/index.d.ts +17 -19
- package/dist/index.js +183 -139
- package/dist/moltendb-worker.js +7 -7
- package/dist/wasm/moltendb_core.d.ts +2 -3
- package/dist/wasm/moltendb_core.js +7 -11
- package/dist/wasm/moltendb_core_bg.wasm +0 -0
- package/dist/wasm/moltendb_core_bg.wasm.d.ts +2 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
<img src="../../assets/logo.png" alt="MoltenDb Logo" width="64"/>
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
**High-performance Rust engine compiled to WASM. Persistent storage via OPFS.**
|
|
6
|
+
### π The Embedded Database for the Modern Web
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
**High-performance Rust engine compiled to WASM. Persistent storage via OPFS.**
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
[Interactive Demo](https://stackblitz.com/~/github.com/maximilian27/moltendb-wasm-demo?file=package.json) β’ [Core Engine](https://www.npmjs.com/package/@moltendb-web/core) β’ [Query Builder](https://www.npmjs.com/package/@moltendb-web/query) β’ [Original Repository](https://github.com/maximilian27/MoltenDb) β’ [License](LICENSE.md)
|
|
11
|
+
|
|
12
|
+
[](https://www.npmjs.com/package/@moltendb-web/core)
|
|
13
|
+
[](LICENSE.md)
|
|
14
|
+
[](https://webassembly.org/)
|
|
15
|
+
[]()
|
|
15
16
|
|
|
16
17
|
</div>
|
|
17
18
|
|
|
@@ -34,20 +35,33 @@ Prefer to run it in your own environment? You can **[clone the demo repository](
|
|
|
34
35
|
**β οΈ Note for Online IDEs:** If you are viewing this on StackBlitz or CodeSandbox, the WASM engine may be blocked by iframe security restrictions. Please click the "Open in New Window/Tab" button in the preview pane to enable the full OPFS storage engine.
|
|
35
36
|
|
|
36
37
|
### Core Features
|
|
37
|
-
|
|
38
|
+
|
|
38
39
|
- **At-Rest Encryption:** Transparently secure your data in the browser using XChaCha20-Poly1305 (Argon2id key derivation).
|
|
39
40
|
- **OPFS Persistence:** Data persists across page reloads in a dedicated, high-speed sandbox.
|
|
40
41
|
- **Worker-Threaded:** The database runs entirely inside a Web Workerβzero impact on your UI thread.
|
|
41
|
-
- **Multi-Tab Sync
|
|
42
|
-
- **Automatic Compaction:** The engine automatically compacts the append-only log when it exceeds **500 entries or 5 MB**, keeping storage lean without any manual intervention.
|
|
42
|
+
- **Multi-Tab Sync:** Leader election via the Web Locks API ensures only one tab owns the OPFS handle. All other tabs proxy reads and writes through a `BroadcastChannel`. Seamless leader promotion when the active tab closes.
|
|
43
43
|
- **Real-Time Pub/Sub:** Every write and delete emits a typed `DbEvent` to all open tabs instantly. The `subscribe()` pattern supports multiple independent listeners per tab β perfect for modern UI frameworks like React and Angular.
|
|
44
44
|
- **GraphQL-style Selection:** Request only the fields you need (even deeply nested ones) to save memory and CPU.
|
|
45
|
-
- **Auto-Indexing:** The engine monitors your queries and automatically creates indexes for frequently filtered fields.
|
|
46
45
|
- **Conflict Resolution:** Incoming writes with `_v β€ stored _v` are silently skipped.
|
|
47
46
|
- **Inline reference embedding (`extends`):** Embed data from another collection at insert time.
|
|
48
47
|
|
|
49
48
|
---
|
|
50
49
|
|
|
50
|
+
## What's New in v2.0.0
|
|
51
|
+
|
|
52
|
+
### Query Builder
|
|
53
|
+
|
|
54
|
+
- **Bulk Delete with `.where()`** β delete documents matching a filter clause without listing individual keys (see [`@moltendb-web/query`](../query/README.md)).
|
|
55
|
+
- **Capped Collections (`.maxSize()`)** β cap a collection to a maximum number of documents; oldest entries are evicted automatically when the limit is reached.
|
|
56
|
+
- **TTL Collections (`.ttl()`)** β set a time-to-live (in seconds) on a collection; documents are removed automatically after expiry.
|
|
57
|
+
|
|
58
|
+
### Core Engine Performance
|
|
59
|
+
|
|
60
|
+
- **`Arc<str>` collection-key interning** β the outer `DashMap` key was changed from `String` to `Arc<str>`. During bulk insert and WAL replay all documents in the same collection share a single pointer instead of allocating a new `String` per document. Saves ~30 B per doc (~30 MB at 1 M docs) and reduces allocator pressure during startup.
|
|
61
|
+
- **MessagePack in-memory storage** β the hot document map was switched from `serde_json::Value` to `Box<[u8]>` (MessagePack bytes). Reduces steady-state RSS for 1 M docs from ~4 GB to ~500 MB (~8Γ lower). Decoding to `Value` happens lazily on read; write paths encode via `rmp_serde`.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
51
65
|
## Installation
|
|
52
66
|
|
|
53
67
|
MoltenDb is split into two packages: the core engine and the type-safe, chainable query builder.
|
|
@@ -59,6 +73,7 @@ npm install @moltendb-web/core
|
|
|
59
73
|
# Install the chainable query builder
|
|
60
74
|
npm install @moltendb-web/query
|
|
61
75
|
```
|
|
76
|
+
|
|
62
77
|
π¦ **Bundler Setup**
|
|
63
78
|
|
|
64
79
|
MoltenDb handles its own Web Workers and WASM loading automatically. However, depending on your build tool, you may need a tiny config tweak to ensure it serves the static files correctly.
|
|
@@ -67,9 +82,9 @@ MoltenDb handles its own Web Workers and WASM loading automatically. However, de
|
|
|
67
82
|
Exclude the core package from pre-bundling in your vite.config.js:
|
|
68
83
|
|
|
69
84
|
```js
|
|
70
|
-
// vite.config.js
|
|
85
|
+
// vite.config.js
|
|
71
86
|
export default defineConfig({
|
|
72
|
-
optimizeDeps: {
|
|
87
|
+
optimizeDeps: {exclude: ['@moltendb-web/core']}
|
|
73
88
|
});
|
|
74
89
|
```
|
|
75
90
|
|
|
@@ -79,20 +94,23 @@ Ensure Webpack treats the `.wasm` binary as a static resource in `webpack.config
|
|
|
79
94
|
```js
|
|
80
95
|
module.exports = {
|
|
81
96
|
module: {
|
|
82
|
-
rules: [{
|
|
97
|
+
rules: [{test: /\.wasm$/, type: 'asset/resource'}]
|
|
83
98
|
}
|
|
84
99
|
};
|
|
85
100
|
```
|
|
101
|
+
|
|
86
102
|
---
|
|
87
103
|
|
|
88
104
|
# Quick Start
|
|
105
|
+
|
|
89
106
|
1. Initialize the Client
|
|
90
107
|
|
|
91
108
|
MoltenDb handles the Web Worker and WASM instantiation for you.
|
|
92
109
|
TypeScript
|
|
110
|
+
|
|
93
111
|
```ts
|
|
94
|
-
import {
|
|
95
|
-
import {
|
|
112
|
+
import {MoltenDb} from '@moltendb-web/core';
|
|
113
|
+
import {MoltenDbClient, WorkerTransport} from '@moltendb-web/query';
|
|
96
114
|
|
|
97
115
|
const db = new MoltenDb('moltendb_demo', {
|
|
98
116
|
encryptionKey: 'my-secret', // Enable transparent at-rest encryption
|
|
@@ -102,36 +120,38 @@ await db.init();
|
|
|
102
120
|
|
|
103
121
|
// Connect the query builder to the WASM worker
|
|
104
122
|
const client = new MoltenDbClient(db);
|
|
123
|
+
```
|
|
105
124
|
|
|
106
|
-
|
|
125
|
+
2. Insert and Query
|
|
107
126
|
|
|
108
|
-
|
|
127
|
+
Use the `@moltendb-web/query` builder for a type-safe experience.
|
|
109
128
|
|
|
129
|
+
```ts
|
|
110
130
|
// Insert data
|
|
111
131
|
await client.collection('laptops').set({
|
|
112
132
|
lp1: {
|
|
113
133
|
brand: "Apple",
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
134
|
+
model: "MacBook Pro",
|
|
135
|
+
price: 1999,
|
|
136
|
+
in_stock: true,
|
|
137
|
+
memory_id: 'mem1',
|
|
138
|
+
specs: {
|
|
139
|
+
cpu: {
|
|
140
|
+
cores: 8,
|
|
141
|
+
clock_speed: 3.5,
|
|
142
|
+
},
|
|
143
|
+
display: {
|
|
144
|
+
refresh_hz: 60,
|
|
126
145
|
}
|
|
146
|
+
}
|
|
127
147
|
},
|
|
128
148
|
lp2: {
|
|
129
149
|
brand: "Apple",
|
|
130
150
|
model: "MacBook Air",
|
|
131
151
|
price: 900,
|
|
132
152
|
in_stock: true,
|
|
133
|
-
|
|
134
|
-
|
|
153
|
+
memory_id: 'mem2',
|
|
154
|
+
specs: {
|
|
135
155
|
cpu: {
|
|
136
156
|
cores: 4,
|
|
137
157
|
clock_speed: 3.5,
|
|
@@ -144,29 +164,29 @@ await client.collection('laptops').set({
|
|
|
144
164
|
}).exec();
|
|
145
165
|
|
|
146
166
|
await client.collection('memory').set({
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
167
|
+
mem1: {
|
|
168
|
+
capacity_gb: 16,
|
|
169
|
+
type: 'DDR4',
|
|
170
|
+
speed_mhz: 4800,
|
|
171
|
+
upgradeable: false
|
|
172
|
+
},
|
|
173
|
+
mem2: {
|
|
174
|
+
capacity_gb: 64,
|
|
175
|
+
type: 'DDR5',
|
|
176
|
+
speed_mhz: 5600,
|
|
177
|
+
upgradeable: true
|
|
178
|
+
},
|
|
159
179
|
}).exec();
|
|
160
180
|
|
|
161
181
|
// Query with field selection
|
|
162
182
|
const results = await client.collection('laptops')
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
183
|
+
.get()
|
|
184
|
+
.where({brand: {$in: ["Apple", "Dell"]}, in_stock: true}) // Using $in operator
|
|
185
|
+
.fields(['model', 'price']) // Only return these specific fields
|
|
186
|
+
.sort([{field: 'price', order: 'desc'}])
|
|
187
|
+
.exec();
|
|
168
188
|
|
|
169
|
-
console.log(results);
|
|
189
|
+
console.log(results);
|
|
170
190
|
// [
|
|
171
191
|
// {
|
|
172
192
|
// "_key": "lp1",
|
|
@@ -179,50 +199,57 @@ console.log(results);
|
|
|
179
199
|
// "price": 900
|
|
180
200
|
// }
|
|
181
201
|
// ]
|
|
202
|
+
```
|
|
182
203
|
|
|
183
|
-
|
|
184
|
-
|
|
204
|
+
Powerful Query Capabilities
|
|
205
|
+
GraphQL-style Field Selection
|
|
185
206
|
|
|
186
|
-
|
|
207
|
+
Never over-fetch data again. Use dot-notation to extract deeply nested values.
|
|
187
208
|
|
|
209
|
+
```ts
|
|
188
210
|
await client.collection('laptops')
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
211
|
+
.get()
|
|
212
|
+
.fields(["brand", "specs.cpu.cores", "specs.display.refresh_hz"])
|
|
213
|
+
.exec();
|
|
214
|
+
```
|
|
192
215
|
|
|
193
|
-
|
|
216
|
+
Inline Joins
|
|
194
217
|
|
|
195
|
-
|
|
218
|
+
Resolve relationships between collections at query time.
|
|
196
219
|
|
|
220
|
+
```ts
|
|
197
221
|
await client.collection('laptops')
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
222
|
+
.get()
|
|
223
|
+
.joins([{
|
|
224
|
+
alias: 'ram',
|
|
225
|
+
from: 'memory',
|
|
226
|
+
on: 'memory_id',
|
|
227
|
+
fields: ['capacity_gb', 'type']
|
|
228
|
+
}])
|
|
229
|
+
.exec();
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
206
233
|
|
|
207
|
-
|
|
234
|
+
## Supported Query Operators
|
|
208
235
|
|
|
209
236
|
MoltenDb supports a variety of operators in the `where` clause:
|
|
210
237
|
|
|
211
|
-
| Operator
|
|
212
|
-
|
|
213
|
-
| `$eq`
|
|
214
|
-
| `$ne`
|
|
215
|
-
| `$gt`
|
|
216
|
-
| `$gte`
|
|
217
|
-
| `$lt`
|
|
218
|
-
| `$lte`
|
|
219
|
-
| `$contains` | `$ct`
|
|
220
|
-
| `$in`
|
|
221
|
-
| `$nin`
|
|
222
|
-
| `$or`
|
|
223
|
-
| `$and`
|
|
224
|
-
|
|
225
|
-
|
|
238
|
+
| Operator | Aliases | Description |
|
|
239
|
+
| :---------- | :------------ | :---------------------------------------------------------- |
|
|
240
|
+
| `$eq` | `$equals` | Exact equality |
|
|
241
|
+
| `$ne` | `$notEquals` | Not equal |
|
|
242
|
+
| `$gt` | `$greaterThan`| Greater than (numeric) |
|
|
243
|
+
| `$gte` | | Greater than or equal |
|
|
244
|
+
| `$lt` | `$lessThan` | Less than (numeric) |
|
|
245
|
+
| `$lte` | | Less than or equal |
|
|
246
|
+
| `$contains` | `$ct` | Substring check (string) or membership check (array) |
|
|
247
|
+
| `$in` | `$oneOf` | Field value is one of a list |
|
|
248
|
+
| `$nin` | `$notIn` | Field value is not in a list |
|
|
249
|
+
| `$or` | | At least one of the sub-conditions must match (array of where-style objects) |
|
|
250
|
+
| `$and` | | All sub-conditions must match (array of where-style objects) |
|
|
251
|
+
|
|
252
|
+
## Inline reference embedding (`extends`)
|
|
226
253
|
|
|
227
254
|
The `extends` key embeds data from another collection directly into the stored document at insert time β no join needed on reads.
|
|
228
255
|
|
|
@@ -242,16 +269,18 @@ await client.collection('laptops')
|
|
|
242
269
|
.exec();
|
|
243
270
|
```
|
|
244
271
|
|
|
272
|
+
---
|
|
245
273
|
**When to use `extends` vs `joins`:**
|
|
246
274
|
|
|
247
|
-
|
|
|
248
|
-
|
|
249
|
-
| Resolved at
|
|
250
|
-
| Data freshness | Snapshot β may become stale
|
|
251
|
-
| Read cost
|
|
252
|
-
| Use when
|
|
275
|
+
| | `extends` | `joins` |
|
|
276
|
+
| :------------- | :------------------------------------- | :----------------------------------------- |
|
|
277
|
+
| Resolved at | Insert time (once) | Query time (every request) |
|
|
278
|
+
| Data freshness | Snapshot β may become stale | Always live |
|
|
279
|
+
| Read cost | O(1) β data already embedded | O(1) per join per document |
|
|
280
|
+
| Use when | Data rarely changes, fast reads matter | Data changes frequently, freshness matters |
|
|
253
281
|
|
|
254
282
|
---
|
|
283
|
+
|
|
255
284
|
## Configuration
|
|
256
285
|
|
|
257
286
|
You can customise the database behavior by passing an options object to the `MoltenDb` constructor.
|
|
@@ -266,24 +295,24 @@ await db.init();
|
|
|
266
295
|
|
|
267
296
|
### Options Reference
|
|
268
297
|
|
|
269
|
-
| Property
|
|
270
|
-
|
|
|
271
|
-
| `encryptionKey`
|
|
272
|
-
| `writeMode`
|
|
273
|
-
| `workerUrl`
|
|
274
|
-
| `maxBodySize`
|
|
275
|
-
| `maxKeysPerRequest` | `number`
|
|
276
|
-
| `inMemory`
|
|
298
|
+
| Property | Type | Default | Description |
|
|
299
|
+
| :------------------ | :------------------ | :---------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
300
|
+
| `encryptionKey` | `string` | `undefined` | **At-Rest Encryption:** If provided, all data written to OPFS is encrypted using XChaCha20-Poly1305. If omitted, data is stored as plain JSON. |
|
|
301
|
+
| `writeMode` | `'async' \| 'sync'` | `'async'` | **Durability vs Speed:** `'async'` is blazing fast (high throughput), while `'sync'` ensures every write is flushed to disk before returning (safer but slower). **Note:** `async` is recommended for most web apps to avoid blocking during heavy write bursts. |
|
|
302
|
+
| `workerUrl` | `string \| URL` | `undefined` | Custom path to the Web Worker script. |
|
|
303
|
+
| `maxBodySize` | `number` | `10485760` | **Payload Limit:** Max body size in bytes. Prevents memory spikes from large messages. |
|
|
304
|
+
| `maxKeysPerRequest` | `number` | `1000` | **Batch Limit:** Maximum number of keys allowed per JSON request. |
|
|
305
|
+
| `inMemory` | `boolean` | `false` | **Ephemeral Mode:** Run entirely in RAM β no OPFS writes, no WAL. All data is lost when **any** tab refreshes or closes. Ideal for CI environments and ephemeral caches. |
|
|
277
306
|
|
|
278
307
|
---
|
|
308
|
+
|
|
279
309
|
## Storage Architecture
|
|
280
310
|
|
|
281
311
|
### How the Log Works
|
|
282
312
|
|
|
283
313
|
MoltenDb uses an append-only JSON log. Every write is a new line, ensuring your data is safe even if the tab is closed unexpectedly.
|
|
284
314
|
|
|
285
|
-
- **
|
|
286
|
-
- **Persistence:** All data is stored in the Origin Private File System (OPFS). This is a special file system for web apps that provides much higher performance than IndexedDB.
|
|
315
|
+
- **Persistence:** Unlese inMemory option is selected all data is stored in the Origin Private File System (OPFS). This is a special file system for web apps that provides much higher performance than IndexedDB.
|
|
287
316
|
|
|
288
317
|
### Multi-Tab Sync
|
|
289
318
|
|
|
@@ -323,7 +352,7 @@ unsubscribe();
|
|
|
323
352
|
The event fires on the **leader tab** (directly from the WASM engine) and is automatically broadcast over the `BroadcastChannel` so every **follower tab** receives it too. This makes it trivial to keep your UI in sync across tabs without any extra infrastructure:
|
|
324
353
|
|
|
325
354
|
```ts
|
|
326
|
-
db.subscribe(({
|
|
355
|
+
db.subscribe(({event, collection, key}) => {
|
|
327
356
|
if (collection === 'laptops') {
|
|
328
357
|
refreshLaptopList(); // re-query and re-render
|
|
329
358
|
}
|
|
@@ -333,12 +362,13 @@ db.subscribe(({ event, collection, key }) => {
|
|
|
333
362
|
The `DbEvent` type is exported from the package for full TypeScript support:
|
|
334
363
|
|
|
335
364
|
```ts
|
|
336
|
-
import {
|
|
365
|
+
import {MoltenDb, DbEvent} from '@moltendb-web/core';
|
|
337
366
|
|
|
338
367
|
const db = new MoltenDb('my-app');
|
|
339
368
|
await db.init();
|
|
340
369
|
|
|
341
|
-
db.subscribe((e: DbEvent) => { /* fully typed */ })
|
|
370
|
+
db.subscribe((e: DbEvent) => { /* fully typed */ });
|
|
371
|
+
```
|
|
342
372
|
|
|
343
373
|
---
|
|
344
374
|
|
|
@@ -353,6 +383,22 @@ Cross-Origin-Embedder-Policy: require-corp
|
|
|
353
383
|
|
|
354
384
|
---
|
|
355
385
|
|
|
386
|
+
## Management Methods
|
|
387
|
+
|
|
388
|
+
### `clearOpfs()`
|
|
389
|
+
|
|
390
|
+
Truncate and close the OPFS file handle, then remove the OPFS directory.
|
|
391
|
+
|
|
392
|
+
Works from any tab β followers automatically route the `clear_opfs` message through the leader via BroadcastChannel, so the leader worker (which holds the exclusive FileSystemSyncAccessHandle) is the one that actually closes the file before the directory is removed.
|
|
393
|
+
|
|
394
|
+
After this resolves, call the `terminate` method and `location.reload()` if needed or re-initialize the database.
|
|
395
|
+
|
|
396
|
+
### `terminate()`
|
|
397
|
+
|
|
398
|
+
Terminates the MoltenDb worker. Call after clearing OPFS storage.
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
356
402
|
## Testing
|
|
357
403
|
|
|
358
404
|
The core package ships with a comprehensive test suite built on **Vitest**:
|
|
@@ -365,20 +411,20 @@ npm run test:coverage # with coverage report
|
|
|
365
411
|
|
|
366
412
|
### What's covered
|
|
367
413
|
|
|
368
|
-
| Suite
|
|
369
|
-
|
|
370
|
-
| `init()`
|
|
371
|
-
| CRUD β leader
|
|
372
|
-
| CRUD β follower
|
|
373
|
-
| Worker error handling
|
|
374
|
-
| Leader promotion
|
|
375
|
-
| `Pub/Sub (subscribe)`
|
|
376
|
-
| Follower timeout
|
|
377
|
-
| `terminate` / `disconnect` | 3
|
|
378
|
-
| Stress β rapid writes
|
|
379
|
-
| BC name isolation
|
|
380
|
-
| Bulk insert stress
|
|
381
|
-
| Multi-tab parallel stress
|
|
414
|
+
| Suite | Tests | What it verifies |
|
|
415
|
+
| :------------------------- | :---- | :----------------------------------------------------------------------------------------- |
|
|
416
|
+
| `init()` | 5 | Leader election, idempotency, worker error propagation |
|
|
417
|
+
| CRUD β leader | 9 | set/get/delete/getAll round-trips, collection isolation |
|
|
418
|
+
| CRUD β follower | 3 | BroadcastChannel proxy path for all mutations |
|
|
419
|
+
| Worker error handling | 3 | Transient errors, unknown actions, request isolation |
|
|
420
|
+
| Leader promotion | 2 | Follower takes over when leader tab closes |
|
|
421
|
+
| `Pub/Sub (subscribe)` | 2 | Multi-subscriber event delivery across tabs |
|
|
422
|
+
| Follower timeout | 1 | Pending requests reject after 10 s if leader disappears |
|
|
423
|
+
| `terminate` / `disconnect` | 3 | Worker cleanup, timer teardown |
|
|
424
|
+
| Stress β rapid writes | 3 | 100 sequential, 50 concurrent, interleaved set/delete |
|
|
425
|
+
| BC name isolation | 2 | Two databases on the same origin don't bleed data |
|
|
426
|
+
| Bulk insert stress | 3 | 1 000 concurrent sets, 500 mixed ops, compact under pressure |
|
|
427
|
+
| Multi-tab parallel stress | 4 | 3 tabs Γ 100 writes, ID collision safety, follower reads after burst, promotion under load |
|
|
382
428
|
|
|
383
429
|
**Total: 50 tests β all green.**
|
|
384
430
|
|
|
@@ -394,16 +440,13 @@ This monorepo contains the following packages:
|
|
|
394
440
|
## Roadmap
|
|
395
441
|
|
|
396
442
|
- [x] **Multi-Tab Sync:** Leader election for multiple tabs to share a single OPFS instance.
|
|
397
|
-
- [x] **Automatic Compaction:** Log compacts automatically at 500 entries or 5 MB.
|
|
398
443
|
- [x] **Rich Test Suite:** 50 unit, integration, and stress tests via Vitest.
|
|
399
|
-
- [
|
|
444
|
+
- [x] **React Adapter:** Official `@moltendb-web/react` package with `useQuery` hooks and real-time context providers.
|
|
400
445
|
- [x] **Angular Adapter:** Official `@moltendb-web/angular` package featuring Signal-based data fetching.
|
|
401
446
|
- [ ] **Delta Sync:** Automatic two-way sync with the MoltenDb Rust server.
|
|
402
447
|
- [x] **Data Encryption:** Transparent encryption-at-rest using hardware-backed keys (Argon2id + XChaCha20).
|
|
403
|
-
- [x] **Hybrid Bitcask:** Seamlessly handle datasets larger than RAM by paging docs to OPFS.
|
|
404
448
|
- [ ] **Analytics Functionality:** Run complex analytics queries straight in the browser without blocking the UI.
|
|
405
|
-
- [x] **Configurable Limits:** User-defined
|
|
406
|
-
|
|
449
|
+
- [x] **Configurable Limits:** User-defined request body sizes for edge and browser environments.
|
|
407
450
|
|
|
408
451
|
## Contributing & Feedback
|
|
409
452
|
|
|
@@ -415,6 +458,4 @@ Found a bug or have a feature request? Please open an issue on the [GitHub issue
|
|
|
415
458
|
|
|
416
459
|
The MoltenDb Web packages (`@moltendb-web/core` and `@moltendb-web/query`) are licensed under the MIT License.
|
|
417
460
|
|
|
418
|
-
The **MoltenDb Server** (Rust backend) remains under the Business Source License 1.1 (Free for organizations under $5M revenue, requires a license for managed services).
|
|
419
|
-
|
|
420
|
-
For commercial licensing or questions: [maximilian.both27@outlook.com](mailto:maximilian.both27@outlook.com)
|
|
461
|
+
The **MoltenDb Server** (Rust backend) remains under the Business Source License 1.1 (Free for organizations under $5M revenue, requires a license for managed services).
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface MoltenDbOptions {
|
|
|
4
4
|
/** Password for at-rest encryption. If not provided, data is stored as plain JSON. */
|
|
5
5
|
encryptionKey?: string;
|
|
6
6
|
/** Storage write mode: 'async' (default, high throughput) or 'sync' (durable). */
|
|
7
|
-
writeMode?:
|
|
7
|
+
writeMode?: "async" | "sync";
|
|
8
8
|
/** Maximum request body size in bytes. */
|
|
9
9
|
maxBodySize?: number;
|
|
10
10
|
/** Maximum number of keys allowed per JSON request. Default: 1000. */
|
|
@@ -21,8 +21,8 @@ export interface MoltenDbOptions {
|
|
|
21
21
|
inMemory?: boolean;
|
|
22
22
|
}
|
|
23
23
|
export interface DbEvent {
|
|
24
|
-
type:
|
|
25
|
-
event:
|
|
24
|
+
type: "event";
|
|
25
|
+
event: "change" | "delete" | "drop" | "kill" | "ttl_expiry";
|
|
26
26
|
collection: string;
|
|
27
27
|
key: string;
|
|
28
28
|
new_v: number | null;
|
|
@@ -32,12 +32,12 @@ export declare class MoltenDb {
|
|
|
32
32
|
readonly workerUrl?: string | URL;
|
|
33
33
|
readonly options: MoltenDbOptions;
|
|
34
34
|
worker: Worker | null;
|
|
35
|
-
private initPromise;
|
|
36
|
-
private pendingRequests;
|
|
37
35
|
isLeader: boolean;
|
|
38
|
-
private bc;
|
|
39
36
|
/** Legacy global hook. Use `subscribe()` for multi-component listeners. */
|
|
40
37
|
onEvent?: (event: DbEvent) => void;
|
|
38
|
+
private initPromise;
|
|
39
|
+
private pendingRequests;
|
|
40
|
+
private bc;
|
|
41
41
|
private eventListeners;
|
|
42
42
|
constructor(dbName?: string, options?: MoltenDbOptions);
|
|
43
43
|
/**
|
|
@@ -47,10 +47,7 @@ export declare class MoltenDb {
|
|
|
47
47
|
subscribe(listener: (event: DbEvent) => void): () => void;
|
|
48
48
|
/** Manually remove a specific listener */
|
|
49
49
|
unsubscribe(listener: (event: DbEvent) => void): void;
|
|
50
|
-
private dispatchEvent;
|
|
51
50
|
init(): Promise<void>;
|
|
52
|
-
private startAsLeader;
|
|
53
|
-
private startAsFollower;
|
|
54
51
|
sendMessage(action: string, payload?: Record<string, unknown>): Promise<any>;
|
|
55
52
|
set(collection: string, key: string, value: any): Promise<void>;
|
|
56
53
|
get(collection: string, key: string): Promise<unknown>;
|
|
@@ -58,19 +55,20 @@ export declare class MoltenDb {
|
|
|
58
55
|
delete(collection: string, key: string): Promise<void>;
|
|
59
56
|
compact(): Promise<unknown>;
|
|
60
57
|
/**
|
|
61
|
-
* Truncate and close the OPFS file
|
|
58
|
+
* Truncate and close the OPFS file handle, then remove the OPFS directory.
|
|
62
59
|
*
|
|
63
|
-
* Works from any tab β followers automatically route
|
|
64
|
-
* via BroadcastChannel, so the leader worker (which holds
|
|
65
|
-
* FileSystemSyncAccessHandle) is the one that actually closes
|
|
60
|
+
* Works from any tab β followers automatically route the `clear_opfs` message
|
|
61
|
+
* through the leader via BroadcastChannel, so the leader worker (which holds
|
|
62
|
+
* the exclusive FileSystemSyncAccessHandle) is the one that actually closes
|
|
63
|
+
* the file before the directory is removed.
|
|
66
64
|
*
|
|
67
|
-
* After this resolves, call
|
|
68
|
-
* `const root = await navigator.storage.getDirectory();`
|
|
69
|
-
* `const root = await navigator.storage.getDirectory();`
|
|
70
|
-
* `await root.removeEntry(dbName, { recursive: true });`
|
|
71
|
-
* then reload the page.
|
|
65
|
+
* After this resolves, call the `terminate` method and `location.reload()` if needed or re-initialize the database.
|
|
72
66
|
*/
|
|
73
|
-
clearOpfs(): Promise<
|
|
67
|
+
clearOpfs(): Promise<void>;
|
|
74
68
|
disconnect(): void;
|
|
69
|
+
/** Terminates the MoltenDb worker. Call after clearing OPFS storage. */
|
|
75
70
|
terminate(): void;
|
|
71
|
+
private dispatchEvent;
|
|
72
|
+
private startAsLeader;
|
|
73
|
+
private startAsFollower;
|
|
76
74
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,16 +4,16 @@ export class MoltenDb {
|
|
|
4
4
|
workerUrl;
|
|
5
5
|
options;
|
|
6
6
|
worker = null;
|
|
7
|
-
initPromise = null;
|
|
8
|
-
pendingRequests = new Map();
|
|
9
7
|
// Multi-tab Sync State
|
|
10
8
|
isLeader = false;
|
|
11
|
-
bc;
|
|
12
9
|
/** Legacy global hook. Use `subscribe()` for multi-component listeners. */
|
|
13
10
|
onEvent;
|
|
11
|
+
initPromise = null;
|
|
12
|
+
pendingRequests = new Map();
|
|
13
|
+
bc;
|
|
14
14
|
// ββ Multi-Subscriber Event System ββββββββββββββββββββββββββββββββββββββββββ
|
|
15
15
|
eventListeners = new Set();
|
|
16
|
-
constructor(dbName =
|
|
16
|
+
constructor(dbName = "moltendb", options = {}) {
|
|
17
17
|
this.dbName = dbName;
|
|
18
18
|
this.workerUrl = options.workerUrl;
|
|
19
19
|
this.options = options;
|
|
@@ -30,18 +30,6 @@ export class MoltenDb {
|
|
|
30
30
|
unsubscribe(listener) {
|
|
31
31
|
this.eventListeners.delete(listener);
|
|
32
32
|
}
|
|
33
|
-
dispatchEvent(event) {
|
|
34
|
-
// Fire all subscribed component handlers
|
|
35
|
-
for (const listener of this.eventListeners) {
|
|
36
|
-
try {
|
|
37
|
-
listener(event);
|
|
38
|
-
}
|
|
39
|
-
catch (err) {
|
|
40
|
-
console.error('[MoltenDb] Error in subscribed listener', err);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
45
33
|
init() {
|
|
46
34
|
// 1. If initialization has already started or finished, return the existing promise
|
|
47
35
|
if (this.initPromise)
|
|
@@ -49,9 +37,9 @@ export class MoltenDb {
|
|
|
49
37
|
// 2. When running in-memory, any tab refresh should wipe the shared RAM store.
|
|
50
38
|
// Broadcast a clear_all signal on beforeunload so the leader can wipe the Rust DashMap.
|
|
51
39
|
if (this.options.inMemory) {
|
|
52
|
-
window.addEventListener(
|
|
40
|
+
window.addEventListener("beforeunload", () => {
|
|
53
41
|
try {
|
|
54
|
-
this.bc?.postMessage({ type:
|
|
42
|
+
this.bc?.postMessage({ type: "clear_all" });
|
|
55
43
|
}
|
|
56
44
|
catch { }
|
|
57
45
|
});
|
|
@@ -84,118 +72,20 @@ export class MoltenDb {
|
|
|
84
72
|
});
|
|
85
73
|
return this.initPromise;
|
|
86
74
|
}
|
|
87
|
-
|
|
88
|
-
// Guard: OPFS is required
|
|
89
|
-
if (!this.options.inMemory) {
|
|
90
|
-
try {
|
|
91
|
-
await navigator.storage.getDirectory();
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
throw new Error('[MoltenDb] Origin Private File System (OPFS) is not available in this browser context. ' +
|
|
95
|
-
'Try a non-private window or a browser that supports OPFS (Chrome 102+, Firefox 111+, Safari 15.2+).');
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
this.isLeader = true;
|
|
99
|
-
if (this.worker)
|
|
100
|
-
this.worker.terminate();
|
|
101
|
-
const url = this.workerUrl || new URL('./moltendb-worker.js', import.meta.url);
|
|
102
|
-
this.worker = new Worker(url, { type: 'module', name: `moltendb-${this.dbName}-leader` });
|
|
103
|
-
this.worker.onmessage = (e) => {
|
|
104
|
-
const data = e.data;
|
|
105
|
-
if (data.type === 'event') {
|
|
106
|
-
this.dispatchEvent(data); // β¬
οΈ Trigger new dispatcher
|
|
107
|
-
this.bc.postMessage(data);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
const req = this.pendingRequests.get(data.id);
|
|
111
|
-
if (req) {
|
|
112
|
-
if (data.error)
|
|
113
|
-
req.reject(new Error(data.error));
|
|
114
|
-
else
|
|
115
|
-
req.resolve(data.result);
|
|
116
|
-
this.pendingRequests.delete(data.id);
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
// Wait for worker to boot
|
|
120
|
-
await this.sendMessage('init', {
|
|
121
|
-
dbName: this.dbName,
|
|
122
|
-
encryptionKey: this.options.encryptionKey,
|
|
123
|
-
inMemory: this.options.inMemory,
|
|
124
|
-
maxBodySize: this.options.maxBodySize,
|
|
125
|
-
maxKeysPerRequest: this.options.maxKeysPerRequest,
|
|
126
|
-
writeMode: this.options.writeMode,
|
|
127
|
-
});
|
|
128
|
-
this.bc.onmessage = async (e) => {
|
|
129
|
-
const msg = e.data;
|
|
130
|
-
// Any tab unloading in in-memory mode broadcasts this β wipe the shared RAM store.
|
|
131
|
-
if (msg.type === 'clear_all') {
|
|
132
|
-
try {
|
|
133
|
-
await this.sendMessage('clear', {});
|
|
134
|
-
this.bc.postMessage({ type: 'cleared' });
|
|
135
|
-
console.log('[MoltenDb] In-memory store wiped (tab unloaded).');
|
|
136
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
console.warn('[MoltenDb] Failed to clear in-memory store:', err);
|
|
139
|
-
}
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (msg.type === 'query' && msg.action) {
|
|
143
|
-
try {
|
|
144
|
-
const result = await this.sendMessage(msg.action, msg.payload);
|
|
145
|
-
this.bc.postMessage({ type: 'response', id: msg.id, result });
|
|
146
|
-
}
|
|
147
|
-
catch (err) {
|
|
148
|
-
this.bc.postMessage({ type: 'response', id: msg.id, error: err.message });
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
startAsFollower() {
|
|
154
|
-
this.isLeader = false;
|
|
155
|
-
if (this.worker) {
|
|
156
|
-
this.worker.terminate();
|
|
157
|
-
this.worker = null;
|
|
158
|
-
}
|
|
159
|
-
this.bc.onmessage = (e) => {
|
|
160
|
-
const data = e.data;
|
|
161
|
-
if (data.type === 'event') {
|
|
162
|
-
this.dispatchEvent(data); // β¬
οΈ Trigger new dispatcher
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
// In-memory wipe notification from leader β reject all in-flight requests.
|
|
166
|
-
if (data.type === 'cleared') {
|
|
167
|
-
console.log('[MoltenDb] In-memory store was wiped by another tab.');
|
|
168
|
-
for (const [id, req] of this.pendingRequests) {
|
|
169
|
-
req.reject(new Error('[MoltenDb] In-memory store was cleared by a tab reload.'));
|
|
170
|
-
this.pendingRequests.delete(id);
|
|
171
|
-
}
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (data.type === 'response') {
|
|
175
|
-
const req = this.pendingRequests.get(data.id);
|
|
176
|
-
if (req) {
|
|
177
|
-
if (data.error)
|
|
178
|
-
req.reject(new Error(data.error));
|
|
179
|
-
else
|
|
180
|
-
req.resolve(data.result);
|
|
181
|
-
this.pendingRequests.delete(data.id);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
}
|
|
75
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
186
76
|
async sendMessage(action, payload) {
|
|
187
77
|
// Wait for the engine to boot before routing the message.
|
|
188
78
|
// If the DB is already initialized, this resolves instantly.
|
|
189
|
-
if (action !==
|
|
79
|
+
if (action !== "init") {
|
|
190
80
|
if (this.initPromise) {
|
|
191
81
|
await this.initPromise;
|
|
192
82
|
}
|
|
193
83
|
else {
|
|
194
|
-
throw new Error(
|
|
84
|
+
throw new Error("[MoltenDb] You must call db.init() before querying the database.");
|
|
195
85
|
}
|
|
196
86
|
}
|
|
197
87
|
// 2. Generate a unique ID
|
|
198
|
-
const id =
|
|
88
|
+
const id = typeof crypto !== "undefined" && crypto.randomUUID
|
|
199
89
|
? crypto.randomUUID()
|
|
200
90
|
: Math.random().toString(36).substring(2, 9);
|
|
201
91
|
return new Promise((resolve, reject) => {
|
|
@@ -214,19 +104,25 @@ export class MoltenDb {
|
|
|
214
104
|
}
|
|
215
105
|
}, 10000);
|
|
216
106
|
this.pendingRequests.set(id, {
|
|
217
|
-
resolve: (res) => {
|
|
218
|
-
|
|
107
|
+
resolve: (res) => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
successHandler(res);
|
|
110
|
+
},
|
|
111
|
+
reject: (e) => {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
reject(e);
|
|
114
|
+
},
|
|
219
115
|
});
|
|
220
|
-
this.bc.postMessage({ type:
|
|
116
|
+
this.bc.postMessage({ type: "query", id, action, payload });
|
|
221
117
|
}
|
|
222
118
|
});
|
|
223
119
|
} // ββ Convenience CRUD helpers βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
224
120
|
async set(collection, key, value) {
|
|
225
|
-
await this.sendMessage(
|
|
121
|
+
await this.sendMessage("set", { collection, data: { [key]: value } });
|
|
226
122
|
}
|
|
227
123
|
async get(collection, key) {
|
|
228
124
|
try {
|
|
229
|
-
return await this.sendMessage(
|
|
125
|
+
return await this.sendMessage("get", { collection, keys: key });
|
|
230
126
|
}
|
|
231
127
|
catch (err) {
|
|
232
128
|
try {
|
|
@@ -240,7 +136,7 @@ export class MoltenDb {
|
|
|
240
136
|
}
|
|
241
137
|
async getAll(collection) {
|
|
242
138
|
try {
|
|
243
|
-
const result = await this.sendMessage(
|
|
139
|
+
const result = await this.sendMessage("get", { collection });
|
|
244
140
|
return result || [];
|
|
245
141
|
}
|
|
246
142
|
catch (err) {
|
|
@@ -254,36 +150,184 @@ export class MoltenDb {
|
|
|
254
150
|
}
|
|
255
151
|
}
|
|
256
152
|
async delete(collection, key) {
|
|
257
|
-
await this.sendMessage(
|
|
153
|
+
await this.sendMessage("delete", { collection, keys: key });
|
|
258
154
|
}
|
|
259
155
|
compact() {
|
|
260
|
-
return this.sendMessage(
|
|
156
|
+
return this.sendMessage("compact");
|
|
261
157
|
}
|
|
262
158
|
/**
|
|
263
|
-
* Truncate and close the OPFS file
|
|
159
|
+
* Truncate and close the OPFS file handle, then remove the OPFS directory.
|
|
264
160
|
*
|
|
265
|
-
* Works from any tab β followers automatically route
|
|
266
|
-
* via BroadcastChannel, so the leader worker (which holds
|
|
267
|
-
* FileSystemSyncAccessHandle) is the one that actually closes
|
|
161
|
+
* Works from any tab β followers automatically route the `clear_opfs` message
|
|
162
|
+
* through the leader via BroadcastChannel, so the leader worker (which holds
|
|
163
|
+
* the exclusive FileSystemSyncAccessHandle) is the one that actually closes
|
|
164
|
+
* the file before the directory is removed.
|
|
268
165
|
*
|
|
269
|
-
* After this resolves, call
|
|
270
|
-
* `const root = await navigator.storage.getDirectory();`
|
|
271
|
-
* `const root = await navigator.storage.getDirectory();`
|
|
272
|
-
* `await root.removeEntry(dbName, { recursive: true });`
|
|
273
|
-
* then reload the page.
|
|
166
|
+
* After this resolves, call the `terminate` method and `location.reload()` if needed or re-initialize the database.
|
|
274
167
|
*/
|
|
275
|
-
clearOpfs() {
|
|
276
|
-
|
|
168
|
+
async clearOpfs() {
|
|
169
|
+
// 1. Tell Rust to flush, truncate, and CLOSE the FileSystemSyncAccessHandle.
|
|
170
|
+
// Without this, removeEntry() throws "No modification allowed".
|
|
171
|
+
await this.sendMessage("clear_opfs");
|
|
172
|
+
// 2. Remove the OPFS directory β the handle is now closed, so this succeeds.
|
|
173
|
+
const root = await navigator.storage.getDirectory();
|
|
174
|
+
await root.removeEntry(this.dbName, { recursive: true });
|
|
175
|
+
// 3. Hit the Kill Switch for all other tabs
|
|
176
|
+
this.bc?.postMessage({ type: "kill_signal" });
|
|
277
177
|
}
|
|
278
178
|
disconnect() {
|
|
279
179
|
if (this.bc)
|
|
280
180
|
this.bc.close();
|
|
281
181
|
}
|
|
182
|
+
/** Terminates the MoltenDb worker. Call after clearing OPFS storage. */
|
|
282
183
|
terminate() {
|
|
283
184
|
this.disconnect();
|
|
284
185
|
if (this.worker) {
|
|
285
186
|
this.worker.terminate();
|
|
286
187
|
this.worker = null;
|
|
287
188
|
}
|
|
189
|
+
this.initPromise = null;
|
|
190
|
+
this.isLeader = false;
|
|
191
|
+
}
|
|
192
|
+
dispatchEvent(event) {
|
|
193
|
+
// Fire all subscribed component handlers
|
|
194
|
+
for (const listener of this.eventListeners) {
|
|
195
|
+
try {
|
|
196
|
+
listener(event);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
console.error("[MoltenDb] Error in subscribed listener", err);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async startAsLeader() {
|
|
204
|
+
// Guard: OPFS is required
|
|
205
|
+
if (!this.options.inMemory) {
|
|
206
|
+
try {
|
|
207
|
+
await navigator.storage.getDirectory();
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
throw new Error("[MoltenDb] Origin Private File System (OPFS) is not available in this browser context. " +
|
|
211
|
+
"Try a non-private window or a browser that supports OPFS (Chrome 102+, Firefox 111+, Safari 15.2+).");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
this.isLeader = true;
|
|
215
|
+
if (this.worker)
|
|
216
|
+
this.worker.terminate();
|
|
217
|
+
const url = this.workerUrl || new URL("./moltendb-worker.js", import.meta.url);
|
|
218
|
+
this.worker = new Worker(url, {
|
|
219
|
+
type: "module",
|
|
220
|
+
name: `moltendb-${this.dbName}-leader`,
|
|
221
|
+
});
|
|
222
|
+
this.worker.onmessage = (e) => {
|
|
223
|
+
const data = e.data;
|
|
224
|
+
if (data.type === "event") {
|
|
225
|
+
this.dispatchEvent(data); // β¬
οΈ Trigger new dispatcher
|
|
226
|
+
this.bc.postMessage(data);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const req = this.pendingRequests.get(data.id);
|
|
230
|
+
if (req) {
|
|
231
|
+
if (data.error)
|
|
232
|
+
req.reject(new Error(data.error));
|
|
233
|
+
else
|
|
234
|
+
req.resolve(data.result);
|
|
235
|
+
this.pendingRequests.delete(data.id);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
// Wait for worker to boot
|
|
239
|
+
await this.sendMessage("init", {
|
|
240
|
+
dbName: this.dbName,
|
|
241
|
+
encryptionKey: this.options.encryptionKey,
|
|
242
|
+
inMemory: this.options.inMemory,
|
|
243
|
+
maxBodySize: this.options.maxBodySize,
|
|
244
|
+
maxKeysPerRequest: this.options.maxKeysPerRequest,
|
|
245
|
+
writeMode: this.options.writeMode,
|
|
246
|
+
});
|
|
247
|
+
this.bc.onmessage = async (e) => {
|
|
248
|
+
const msg = e.data;
|
|
249
|
+
// --- THE KILL SWITCH ---
|
|
250
|
+
if (msg.type === "kill_signal") {
|
|
251
|
+
this.terminate(); // Kills zombie worker instantly
|
|
252
|
+
this.dispatchEvent({
|
|
253
|
+
type: "event",
|
|
254
|
+
event: "kill",
|
|
255
|
+
collection: "*",
|
|
256
|
+
key: "*",
|
|
257
|
+
new_v: null,
|
|
258
|
+
});
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Any tab unloading in in-memory mode broadcasts this β wipe the shared RAM store.
|
|
262
|
+
if (msg.type === "clear_all") {
|
|
263
|
+
try {
|
|
264
|
+
await this.sendMessage("clear", {});
|
|
265
|
+
this.bc.postMessage({ type: "cleared" });
|
|
266
|
+
console.log("[MoltenDb] In-memory store wiped (tab unloaded).");
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
console.warn("[MoltenDb] Failed to clear in-memory store:", err);
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (msg.type === "query" && msg.action) {
|
|
274
|
+
try {
|
|
275
|
+
const result = await this.sendMessage(msg.action, msg.payload);
|
|
276
|
+
this.bc.postMessage({ type: "response", id: msg.id, result });
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
this.bc.postMessage({
|
|
280
|
+
type: "response",
|
|
281
|
+
id: msg.id,
|
|
282
|
+
error: err.message,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
startAsFollower() {
|
|
289
|
+
this.isLeader = false;
|
|
290
|
+
if (this.worker) {
|
|
291
|
+
this.worker.terminate();
|
|
292
|
+
this.worker = null;
|
|
293
|
+
}
|
|
294
|
+
this.bc.onmessage = (e) => {
|
|
295
|
+
const data = e.data;
|
|
296
|
+
// --- THE KILKILL SWITCH ---
|
|
297
|
+
if (data.type === "kill_signal") {
|
|
298
|
+
this.terminate(); // Kills zombie worker instantly
|
|
299
|
+
this.dispatchEvent({
|
|
300
|
+
type: "event",
|
|
301
|
+
event: "kill",
|
|
302
|
+
collection: "*",
|
|
303
|
+
key: "*",
|
|
304
|
+
new_v: null,
|
|
305
|
+
});
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (data.type === "event") {
|
|
309
|
+
this.dispatchEvent(data); // β¬
οΈ Trigger new dispatcher
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// In-memory wipe notification from leader β reject all in-flight requests.
|
|
313
|
+
if (data.type === "cleared") {
|
|
314
|
+
console.log("[MoltenDb] In-memory store was wiped by another tab.");
|
|
315
|
+
for (const [id, req] of this.pendingRequests) {
|
|
316
|
+
req.reject(new Error("[MoltenDb] In-memory store was cleared by a tab reload."));
|
|
317
|
+
this.pendingRequests.delete(id);
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (data.type === "response") {
|
|
322
|
+
const req = this.pendingRequests.get(data.id);
|
|
323
|
+
if (req) {
|
|
324
|
+
if (data.error)
|
|
325
|
+
req.reject(new Error(data.error));
|
|
326
|
+
else
|
|
327
|
+
req.resolve(data.result);
|
|
328
|
+
this.pendingRequests.delete(data.id);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
288
332
|
}
|
|
289
333
|
}
|
package/dist/moltendb-worker.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import init, { WorkerDb } from
|
|
1
|
+
import init, { WorkerDb } from "./wasm/moltendb_core.js";
|
|
2
2
|
let db = null;
|
|
3
3
|
let initPromise = null;
|
|
4
4
|
self.onmessage = async (e) => {
|
|
5
5
|
const { id, action, ...payload } = e.data;
|
|
6
6
|
// --- 1. Initialization Phase ---
|
|
7
|
-
if (action ===
|
|
7
|
+
if (action === "init") {
|
|
8
8
|
if (!initPromise) {
|
|
9
9
|
initPromise = (async () => {
|
|
10
10
|
await init();
|
|
@@ -14,10 +14,10 @@ self.onmessage = async (e) => {
|
|
|
14
14
|
instance.subscribe((eventStr) => {
|
|
15
15
|
try {
|
|
16
16
|
const eventData = JSON.parse(eventStr);
|
|
17
|
-
self.postMessage({ type:
|
|
17
|
+
self.postMessage({ type: "event", ...eventData });
|
|
18
18
|
}
|
|
19
19
|
catch (err) {
|
|
20
|
-
console.error(
|
|
20
|
+
console.error("[MoltenDb Worker] Event parse error", err);
|
|
21
21
|
}
|
|
22
22
|
});
|
|
23
23
|
db = instance;
|
|
@@ -26,11 +26,11 @@ self.onmessage = async (e) => {
|
|
|
26
26
|
}
|
|
27
27
|
try {
|
|
28
28
|
await initPromise;
|
|
29
|
-
self.postMessage({ id, result: { status:
|
|
29
|
+
self.postMessage({ id, result: { status: "ok" } });
|
|
30
30
|
}
|
|
31
31
|
catch (error) {
|
|
32
32
|
// FIX: Handle Map-based errors from Rust correctly
|
|
33
|
-
const errorMsg =
|
|
33
|
+
const errorMsg = error instanceof Map
|
|
34
34
|
? JSON.stringify(Object.fromEntries(error))
|
|
35
35
|
: String(error);
|
|
36
36
|
self.postMessage({ id, error: errorMsg });
|
|
@@ -47,7 +47,7 @@ self.onmessage = async (e) => {
|
|
|
47
47
|
}
|
|
48
48
|
catch (error) {
|
|
49
49
|
// FIX: Handle Map-based errors here too
|
|
50
|
-
const errorMsg =
|
|
50
|
+
const errorMsg = error instanceof Map
|
|
51
51
|
? JSON.stringify(Object.fromEntries(error))
|
|
52
52
|
: String(error);
|
|
53
53
|
self.postMessage({ id, error: errorMsg });
|
|
@@ -58,7 +58,6 @@ export class WorkerDb {
|
|
|
58
58
|
* - "update" β patch/merge documents: { collection, data: { key: patch, ... } }
|
|
59
59
|
* - "delete" β delete documents or drop: { collection, keys: ... } or { drop: true }
|
|
60
60
|
* - "compact" β compact the OPFS log file
|
|
61
|
-
* - "get_size" β return current OPFS file size in bytes
|
|
62
61
|
* - "clear" β wipe all in-memory state (in-memory mode only)
|
|
63
62
|
*
|
|
64
63
|
* Returns a JsValue result on success, or a JsValue error string on failure.
|
|
@@ -80,8 +79,8 @@ export interface InitOutput {
|
|
|
80
79
|
readonly workerdb_create: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => number;
|
|
81
80
|
readonly workerdb_handle_message: (a: number, b: number, c: number) => void;
|
|
82
81
|
readonly workerdb_subscribe: (a: number, b: number) => void;
|
|
83
|
-
readonly
|
|
84
|
-
readonly
|
|
82
|
+
readonly __wasm_bindgen_func_elem_4166: (a: number, b: number, c: number, d: number) => void;
|
|
83
|
+
readonly __wasm_bindgen_func_elem_4178: (a: number, b: number, c: number, d: number) => void;
|
|
85
84
|
readonly __wbindgen_export: (a: number, b: number) => number;
|
|
86
85
|
readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
87
86
|
readonly __wbindgen_export3: (a: number) => void;
|
|
@@ -87,7 +87,6 @@ export class WorkerDb {
|
|
|
87
87
|
* - "update" β patch/merge documents: { collection, data: { key: patch, ... } }
|
|
88
88
|
* - "delete" β delete documents or drop: { collection, keys: ... } or { drop: true }
|
|
89
89
|
* - "compact" β compact the OPFS log file
|
|
90
|
-
* - "get_size" β return current OPFS file size in bytes
|
|
91
90
|
* - "clear" β wipe all in-memory state (in-memory mode only)
|
|
92
91
|
*
|
|
93
92
|
* Returns a JsValue result on success, or a JsValue error string on failure.
|
|
@@ -232,9 +231,6 @@ function __wbg_get_imports() {
|
|
|
232
231
|
const ret = Object.entries(getObject(arg0));
|
|
233
232
|
return addHeapObject(ret);
|
|
234
233
|
},
|
|
235
|
-
__wbg_error_2001591ad2463697: function(arg0) {
|
|
236
|
-
console.error(getObject(arg0));
|
|
237
|
-
},
|
|
238
234
|
__wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) {
|
|
239
235
|
let deferred0_0;
|
|
240
236
|
let deferred0_1;
|
|
@@ -377,7 +373,7 @@ function __wbg_get_imports() {
|
|
|
377
373
|
const a = state0.a;
|
|
378
374
|
state0.a = 0;
|
|
379
375
|
try {
|
|
380
|
-
return
|
|
376
|
+
return __wasm_bindgen_func_elem_4178(a, state0.b, arg0, arg1);
|
|
381
377
|
} finally {
|
|
382
378
|
state0.a = a;
|
|
383
379
|
}
|
|
@@ -516,8 +512,8 @@ function __wbg_get_imports() {
|
|
|
516
512
|
return ret;
|
|
517
513
|
}, arguments); },
|
|
518
514
|
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
|
519
|
-
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx:
|
|
520
|
-
const ret = makeMutClosure(arg0, arg1,
|
|
515
|
+
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 735, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
|
516
|
+
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_4166);
|
|
521
517
|
return addHeapObject(ret);
|
|
522
518
|
},
|
|
523
519
|
__wbindgen_cast_0000000000000002: function(arg0) {
|
|
@@ -559,10 +555,10 @@ function __wbg_get_imports() {
|
|
|
559
555
|
};
|
|
560
556
|
}
|
|
561
557
|
|
|
562
|
-
function
|
|
558
|
+
function __wasm_bindgen_func_elem_4166(arg0, arg1, arg2) {
|
|
563
559
|
try {
|
|
564
560
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
565
|
-
wasm.
|
|
561
|
+
wasm.__wasm_bindgen_func_elem_4166(retptr, arg0, arg1, addHeapObject(arg2));
|
|
566
562
|
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
567
563
|
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
568
564
|
if (r1) {
|
|
@@ -573,8 +569,8 @@ function __wasm_bindgen_func_elem_3769(arg0, arg1, arg2) {
|
|
|
573
569
|
}
|
|
574
570
|
}
|
|
575
571
|
|
|
576
|
-
function
|
|
577
|
-
wasm.
|
|
572
|
+
function __wasm_bindgen_func_elem_4178(arg0, arg1, arg2, arg3) {
|
|
573
|
+
wasm.__wasm_bindgen_func_elem_4178(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
|
|
578
574
|
}
|
|
579
575
|
|
|
580
576
|
const WorkerDbFinalization = (typeof FinalizationRegistry === 'undefined')
|
|
Binary file
|
|
@@ -5,8 +5,8 @@ export const __wbg_workerdb_free: (a: number, b: number) => void;
|
|
|
5
5
|
export const workerdb_create: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => number;
|
|
6
6
|
export const workerdb_handle_message: (a: number, b: number, c: number) => void;
|
|
7
7
|
export const workerdb_subscribe: (a: number, b: number) => void;
|
|
8
|
-
export const
|
|
9
|
-
export const
|
|
8
|
+
export const __wasm_bindgen_func_elem_4166: (a: number, b: number, c: number, d: number) => void;
|
|
9
|
+
export const __wasm_bindgen_func_elem_4178: (a: number, b: number, c: number, d: number) => void;
|
|
10
10
|
export const __wbindgen_export: (a: number, b: number) => number;
|
|
11
11
|
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
12
12
|
export const __wbindgen_export3: (a: number) => void;
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moltendb-web/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "MoltenDb WASM runtime β the database engine, Web Worker, and main-thread client in one package.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"author": "Maximilian Both <
|
|
6
|
+
"author": "Maximilian Both <admin@moltendb.dev",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@playwright/test": "^1.58.2",
|
|
45
45
|
"@vitest/coverage-v8": "^4.1.1",
|
|
46
|
+
"@vitest/utils": "^4.1.5",
|
|
46
47
|
"copyfiles": "^2.4.1",
|
|
47
48
|
"happy-dom": "^20.8.7",
|
|
48
49
|
"typescript": "^6.0.2",
|