@moltendb-web/core 1.7.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 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
- ### πŸŒ‹ The Embedded Database for the Modern Web
7
- **High-performance Rust engine compiled to WASM. Persistent storage via OPFS.**
6
+ ### πŸŒ‹ The Embedded Database for the Modern Web
8
7
 
9
- [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)
8
+ **High-performance Rust engine compiled to WASM. Persistent storage via OPFS.**
10
9
 
11
- [![NPM Version](https://img.shields.io/npm/v/@moltendb-web/core?style=flat-square&color=orange)](https://www.npmjs.com/package/@moltendb-web/core)
12
- [![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)](LICENSE.md)
13
- [![WASM](https://img.shields.io/badge/wasm-optimized-magenta?style=flat-square)](https://webassembly.org/)
14
- [![Status](https://img.shields.io/badge/status-stable-brightgreen?style=flat-square)]()
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
+ [![NPM Version](https://img.shields.io/npm/v/@moltendb-web/core?style=flat-square&color=orange)](https://www.npmjs.com/package/@moltendb-web/core)
13
+ [![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)](LICENSE.md)
14
+ [![WASM](https://img.shields.io/badge/wasm-optimized-magenta?style=flat-square)](https://webassembly.org/)
15
+ [![Status](https://img.shields.io/badge/status-stable-brightgreen?style=flat-square)]()
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
- - **Hybrid Bitcask Storage:** The same query logic used in our server binary, compiled to WebAssembly. Data is paged between RAM and OPFS to handle datasets larger than memory.
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 (stabilised):** 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.
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: { exclude: ['@moltendb-web/core'] }
87
+ optimizeDeps: {exclude: ['@moltendb-web/core']}
73
88
  });
74
89
  ```
75
90
 
@@ -79,23 +94,25 @@ 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: [{ test: /\.wasm$/, type: 'asset/resource' }]
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 { MoltenDb } from '@moltendb-web/core';
95
- import { MoltenDbClient, WorkerTransport } from '@moltendb-web/query';
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
- hotThreshold: 25000, // Keep 25k docs in RAM per collection
99
116
  encryptionKey: 'my-secret', // Enable transparent at-rest encryption
100
117
  writeMode: 'sync' // Ensure every write is flushed to OPFS
101
118
  });
@@ -103,36 +120,38 @@ await db.init();
103
120
 
104
121
  // Connect the query builder to the WASM worker
105
122
  const client = new MoltenDbClient(db);
123
+ ```
106
124
 
107
- // 2. Insert and Query
125
+ 2. Insert and Query
108
126
 
109
- // Use the @moltendb-web/query builder for a type-safe experience.
127
+ Use the `@moltendb-web/query` builder for a type-safe experience.
110
128
 
129
+ ```ts
111
130
  // Insert data
112
131
  await client.collection('laptops').set({
113
132
  lp1: {
114
133
  brand: "Apple",
115
- model: "MacBook Pro",
116
- price: 1999,
117
- in_stock: true,
118
- memory_id: 'mem1',
119
- specs: {
120
- cpu: {
121
- cores: 8,
122
- clock_speed: 3.5,
123
- },
124
- display: {
125
- refresh_hz: 60,
126
- }
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,
127
145
  }
146
+ }
128
147
  },
129
148
  lp2: {
130
149
  brand: "Apple",
131
150
  model: "MacBook Air",
132
151
  price: 900,
133
152
  in_stock: true,
134
- memory_id: 'mem2',
135
- specs: {
153
+ memory_id: 'mem2',
154
+ specs: {
136
155
  cpu: {
137
156
  cores: 4,
138
157
  clock_speed: 3.5,
@@ -145,29 +164,29 @@ await client.collection('laptops').set({
145
164
  }).exec();
146
165
 
147
166
  await client.collection('memory').set({
148
- mem1: {
149
- capacity_gb: 16,
150
- type: 'DDR4',
151
- speed_mhz: 4800,
152
- upgradeable: false
153
- },
154
- mem2: {
155
- capacity_gb: 64,
156
- type: 'DDR5',
157
- speed_mhz: 5600,
158
- upgradeable: true
159
- },
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
+ },
160
179
  }).exec();
161
180
 
162
181
  // Query with field selection
163
182
  const results = await client.collection('laptops')
164
- .get()
165
- .where({ brand: { $in: ["Apple", "Dell"] }, in_stock: true }) // Using $in operator
166
- .fields(['model', 'price']) // Only return these specific fields
167
- .sort([{ field: 'price', order: 'desc' }])
168
- .exec();
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();
169
188
 
170
- console.log(results);
189
+ console.log(results);
171
190
  // [
172
191
  // {
173
192
  // "_key": "lp1",
@@ -180,50 +199,57 @@ console.log(results);
180
199
  // "price": 900
181
200
  // }
182
201
  // ]
202
+ ```
183
203
 
184
- // Powerful Query Capabilities
185
- // GraphQL-style Field Selection
204
+ Powerful Query Capabilities
205
+ GraphQL-style Field Selection
186
206
 
187
- // Never over-fetch data again. Use dot-notation to extract deeply nested values.
207
+ Never over-fetch data again. Use dot-notation to extract deeply nested values.
188
208
 
209
+ ```ts
189
210
  await client.collection('laptops')
190
- .get()
191
- .fields(["brand", "specs.cpu.cores", "specs.display.refresh_hz"])
192
- .exec();
211
+ .get()
212
+ .fields(["brand", "specs.cpu.cores", "specs.display.refresh_hz"])
213
+ .exec();
214
+ ```
193
215
 
194
- // Inline Joins
216
+ Inline Joins
195
217
 
196
- // Resolve relationships between collections at query time.
218
+ Resolve relationships between collections at query time.
197
219
 
220
+ ```ts
198
221
  await client.collection('laptops')
199
- .get()
200
- .joins([{
201
- alias: 'ram',
202
- from: 'memory',
203
- on: 'memory_id',
204
- fields: ['capacity_gb', 'type']
205
- }])
206
- .exec();
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
+ ---
207
233
 
208
- // Supported Query Operators
234
+ ## Supported Query Operators
209
235
 
210
236
  MoltenDb supports a variety of operators in the `where` clause:
211
237
 
212
- | Operator | Aliases | Description |
213
- |---|---|---|
214
- | `$eq` | `$equals` | Exact equality |
215
- | `$ne` | `$notEquals` | Not equal |
216
- | `$gt` | `$greaterThan` | Greater than (numeric) |
217
- | `$gte` | | Greater than or equal |
218
- | `$lt` | `$lessThan` | Less than (numeric) |
219
- | `$lte` | | Less than or equal |
220
- | `$contains` | `$ct` | Substring check (string) or membership check (array) |
221
- | `$in` | `$oneOf` | Field value is one of a list |
222
- | `$nin` | `$notIn` | Field value is not in a list |
223
- | `$or` | | At least one of the sub-conditions must match (array of where-style objects) |
224
- | `$and` | | All sub-conditions must match (array of where-style objects) |
225
-
226
- // Inline reference embedding (`extends`)
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`)
227
253
 
228
254
  The `extends` key embeds data from another collection directly into the stored document at insert time β€” no join needed on reads.
229
255
 
@@ -243,23 +269,24 @@ await client.collection('laptops')
243
269
  .exec();
244
270
  ```
245
271
 
272
+ ---
246
273
  **When to use `extends` vs `joins`:**
247
274
 
248
- | | `extends` | `joins` |
249
- |---|---|---|
250
- | Resolved at | Insert time (once) | Query time (every request) |
251
- | Data freshness | Snapshot β€” may become stale | Always live |
252
- | Read cost | O(1) β€” data already embedded | O(1) per join per document |
253
- | Use when | Data rarely changes, fast reads matter | Data changes frequently, freshness matters |
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 |
254
281
 
255
282
  ---
283
+
256
284
  ## Configuration
257
285
 
258
286
  You can customise the database behavior by passing an options object to the `MoltenDb` constructor.
259
287
 
260
288
  ```ts
261
289
  const db = new MoltenDb('my-app', {
262
- hotThreshold: 25000, // Page to disk after 25k docs
263
290
  encryptionKey: 'user-secret', // Secure at-rest storage in OPFS
264
291
  writeMode: 'sync' // Ensure durability on every write
265
292
  });
@@ -268,25 +295,24 @@ await db.init();
268
295
 
269
296
  ### Options Reference
270
297
 
271
- | Property | Type | Default | Description |
272
- | :--- | :--- | :--- |:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
273
- | `hotThreshold` | `number` | `50000` | **Hybrid Bitcask Limit:** Maximum documents per collection to keep in RAM. When exceeded, the oldest documents are paged out to OPFS to save memory. |
274
- | `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. |
275
- | `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. |
276
- | `workerUrl` | `string \| URL` | `undefined` | Custom path to the Web Worker script. |
277
- | `maxBodySize` | `number` | `10485760` | **Payload Limit:** Max body size in bytes. Prevents memory spikes from large messages. |
278
- | `maxKeysPerRequest` | `number` | `1000` | **Batch Limit:** Maximum number of keys allowed per JSON request. |
279
- | `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. |
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. |
280
306
 
281
307
  ---
308
+
282
309
  ## Storage Architecture
283
310
 
284
311
  ### How the Log Works
285
312
 
286
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.
287
314
 
288
- - **Automatic Compaction:** When the log exceeds **500 entries or 5 MB**, the engine automatically "squashes" the log, removing superseded document versions to reclaim space. No manual `compact()` calls are needed in normal operation.
289
- - **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.
290
316
 
291
317
  ### Multi-Tab Sync
292
318
 
@@ -326,7 +352,7 @@ unsubscribe();
326
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:
327
353
 
328
354
  ```ts
329
- db.subscribe(({ event, collection, key }) => {
355
+ db.subscribe(({event, collection, key}) => {
330
356
  if (collection === 'laptops') {
331
357
  refreshLaptopList(); // re-query and re-render
332
358
  }
@@ -336,12 +362,13 @@ db.subscribe(({ event, collection, key }) => {
336
362
  The `DbEvent` type is exported from the package for full TypeScript support:
337
363
 
338
364
  ```ts
339
- import { MoltenDb, DbEvent } from '@moltendb-web/core';
365
+ import {MoltenDb, DbEvent} from '@moltendb-web/core';
340
366
 
341
367
  const db = new MoltenDb('my-app');
342
368
  await db.init();
343
369
 
344
- db.subscribe((e: DbEvent) => { /* fully typed */ });```
370
+ db.subscribe((e: DbEvent) => { /* fully typed */ });
371
+ ```
345
372
 
346
373
  ---
347
374
 
@@ -356,6 +383,22 @@ Cross-Origin-Embedder-Policy: require-corp
356
383
 
357
384
  ---
358
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
+
359
402
  ## Testing
360
403
 
361
404
  The core package ships with a comprehensive test suite built on **Vitest**:
@@ -368,20 +411,20 @@ npm run test:coverage # with coverage report
368
411
 
369
412
  ### What's covered
370
413
 
371
- | Suite | Tests | What it verifies |
372
- |---|---|---|
373
- | `init()` | 5 | Leader election, idempotency, worker error propagation |
374
- | CRUD β€” leader | 9 | set/get/delete/getAll round-trips, collection isolation |
375
- | CRUD β€” follower | 3 | BroadcastChannel proxy path for all mutations |
376
- | Worker error handling | 3 | Transient errors, unknown actions, request isolation |
377
- | Leader promotion | 2 | Follower takes over when leader tab closes |
378
- | `Pub/Sub (subscribe)` | 2 | Multi-subscriber event delivery across tabs |
379
- | Follower timeout | 1 | Pending requests reject after 10 s if leader disappears |
380
- | `terminate` / `disconnect` | 3 | Worker cleanup, timer teardown |
381
- | Stress β€” rapid writes | 3 | 100 sequential, 50 concurrent, interleaved set/delete |
382
- | BC name isolation | 2 | Two databases on the same origin don't bleed data |
383
- | Bulk insert stress | 3 | 1 000 concurrent sets, 500 mixed ops, compact under pressure |
384
- | Multi-tab parallel stress | 4 | 3 tabs Γ— 100 writes, ID collision safety, follower reads after burst, promotion under load |
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 |
385
428
 
386
429
  **Total: 50 tests β€” all green.**
387
430
 
@@ -397,16 +440,13 @@ This monorepo contains the following packages:
397
440
  ## Roadmap
398
441
 
399
442
  - [x] **Multi-Tab Sync:** Leader election for multiple tabs to share a single OPFS instance.
400
- - [x] **Automatic Compaction:** Log compacts automatically at 500 entries or 5 MB.
401
443
  - [x] **Rich Test Suite:** 50 unit, integration, and stress tests via Vitest.
402
- - [ ] **React Adapter:** Official `@moltendb-web/react` package with `useQuery` hooks and real-time context providers.
444
+ - [x] **React Adapter:** Official `@moltendb-web/react` package with `useQuery` hooks and real-time context providers.
403
445
  - [x] **Angular Adapter:** Official `@moltendb-web/angular` package featuring Signal-based data fetching.
404
446
  - [ ] **Delta Sync:** Automatic two-way sync with the MoltenDb Rust server.
405
447
  - [x] **Data Encryption:** Transparent encryption-at-rest using hardware-backed keys (Argon2id + XChaCha20).
406
- - [x] **Hybrid Bitcask:** Seamlessly handle datasets larger than RAM by paging docs to OPFS.
407
448
  - [ ] **Analytics Functionality:** Run complex analytics queries straight in the browser without blocking the UI.
408
- - [x] **Configurable Limits:** User-defined RAM thresholds and request body sizes for edge and browser environments.
409
-
449
+ - [x] **Configurable Limits:** User-defined request body sizes for edge and browser environments.
410
450
 
411
451
  ## Contributing & Feedback
412
452
 
@@ -418,6 +458,4 @@ Found a bug or have a feature request? Please open an issue on the [GitHub issue
418
458
 
419
459
  The MoltenDb Web packages (`@moltendb-web/core` and `@moltendb-web/query`) are licensed under the MIT License.
420
460
 
421
- 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).
422
-
423
- 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
@@ -1,12 +1,10 @@
1
1
  export interface MoltenDbOptions {
2
2
  /** URL or path to moltendb-worker.js. */
3
3
  workerUrl?: string | URL;
4
- /** Maximum documents per collection to keep in RAM. Default: 50,000. */
5
- hotThreshold?: number;
6
4
  /** Password for at-rest encryption. If not provided, data is stored as plain JSON. */
7
5
  encryptionKey?: string;
8
6
  /** Storage write mode: 'async' (default, high throughput) or 'sync' (durable). */
9
- writeMode?: 'async' | 'sync';
7
+ writeMode?: "async" | "sync";
10
8
  /** Maximum request body size in bytes. */
11
9
  maxBodySize?: number;
12
10
  /** Maximum number of keys allowed per JSON request. Default: 1000. */
@@ -23,8 +21,8 @@ export interface MoltenDbOptions {
23
21
  inMemory?: boolean;
24
22
  }
25
23
  export interface DbEvent {
26
- type: 'event';
27
- event: 'change' | 'delete' | 'drop';
24
+ type: "event";
25
+ event: "change" | "delete" | "drop" | "kill" | "ttl_expiry";
28
26
  collection: string;
29
27
  key: string;
30
28
  new_v: number | null;
@@ -34,12 +32,12 @@ export declare class MoltenDb {
34
32
  readonly workerUrl?: string | URL;
35
33
  readonly options: MoltenDbOptions;
36
34
  worker: Worker | null;
37
- private initPromise;
38
- private pendingRequests;
39
35
  isLeader: boolean;
40
- private bc;
41
36
  /** Legacy global hook. Use `subscribe()` for multi-component listeners. */
42
37
  onEvent?: (event: DbEvent) => void;
38
+ private initPromise;
39
+ private pendingRequests;
40
+ private bc;
43
41
  private eventListeners;
44
42
  constructor(dbName?: string, options?: MoltenDbOptions);
45
43
  /**
@@ -49,16 +47,28 @@ export declare class MoltenDb {
49
47
  subscribe(listener: (event: DbEvent) => void): () => void;
50
48
  /** Manually remove a specific listener */
51
49
  unsubscribe(listener: (event: DbEvent) => void): void;
52
- private dispatchEvent;
53
50
  init(): Promise<void>;
54
- private startAsLeader;
55
- private startAsFollower;
56
51
  sendMessage(action: string, payload?: Record<string, unknown>): Promise<any>;
57
52
  set(collection: string, key: string, value: any): Promise<void>;
58
53
  get(collection: string, key: string): Promise<unknown>;
59
54
  getAll(collection: string): Promise<unknown[]>;
60
55
  delete(collection: string, key: string): Promise<void>;
61
56
  compact(): Promise<unknown>;
57
+ /**
58
+ * Truncate and close the OPFS file handle, then remove the OPFS directory.
59
+ *
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.
64
+ *
65
+ * After this resolves, call the `terminate` method and `location.reload()` if needed or re-initialize the database.
66
+ */
67
+ clearOpfs(): Promise<void>;
62
68
  disconnect(): void;
69
+ /** Terminates the MoltenDb worker. Call after clearing OPFS storage. */
63
70
  terminate(): void;
71
+ private dispatchEvent;
72
+ private startAsLeader;
73
+ private startAsFollower;
64
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 = 'moltendb', options = {}) {
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('beforeunload', () => {
40
+ window.addEventListener("beforeunload", () => {
53
41
  try {
54
- this.bc?.postMessage({ type: 'clear_all' });
42
+ this.bc?.postMessage({ type: "clear_all" });
55
43
  }
56
44
  catch { }
57
45
  });
@@ -84,6 +72,134 @@ export class MoltenDb {
84
72
  });
85
73
  return this.initPromise;
86
74
  }
75
+ // ───────────────────────────────────────────────────────────────────────────
76
+ async sendMessage(action, payload) {
77
+ // Wait for the engine to boot before routing the message.
78
+ // If the DB is already initialized, this resolves instantly.
79
+ if (action !== "init") {
80
+ if (this.initPromise) {
81
+ await this.initPromise;
82
+ }
83
+ else {
84
+ throw new Error("[MoltenDb] You must call db.init() before querying the database.");
85
+ }
86
+ }
87
+ // 2. Generate a unique ID
88
+ const id = typeof crypto !== "undefined" && crypto.randomUUID
89
+ ? crypto.randomUUID()
90
+ : Math.random().toString(36).substring(2, 9);
91
+ return new Promise((resolve, reject) => {
92
+ const successHandler = (res) => resolve(mapToObj(res));
93
+ // 3. We are now GUARANTEED that isLeader, worker, and bc are accurately set
94
+ if (this.isLeader && this.worker) {
95
+ this.pendingRequests.set(id, { resolve: successHandler, reject });
96
+ this.worker.postMessage({ id, action, ...payload });
97
+ }
98
+ else {
99
+ // Follower routing via BroadcastChannel
100
+ const timer = setTimeout(() => {
101
+ if (this.pendingRequests.has(id)) {
102
+ this.pendingRequests.delete(id);
103
+ reject(new Error(`[MoltenDb] Request "${action}" timed out after 10s.`));
104
+ }
105
+ }, 10000);
106
+ this.pendingRequests.set(id, {
107
+ resolve: (res) => {
108
+ clearTimeout(timer);
109
+ successHandler(res);
110
+ },
111
+ reject: (e) => {
112
+ clearTimeout(timer);
113
+ reject(e);
114
+ },
115
+ });
116
+ this.bc.postMessage({ type: "query", id, action, payload });
117
+ }
118
+ });
119
+ } // ── Convenience CRUD helpers ───────────────────────────────────────────────
120
+ async set(collection, key, value) {
121
+ await this.sendMessage("set", { collection, data: { [key]: value } });
122
+ }
123
+ async get(collection, key) {
124
+ try {
125
+ return await this.sendMessage("get", { collection, keys: key });
126
+ }
127
+ catch (err) {
128
+ try {
129
+ const errorData = JSON.parse(err.message);
130
+ if (errorData.statusCode === 404)
131
+ return null;
132
+ }
133
+ catch { }
134
+ throw err;
135
+ }
136
+ }
137
+ async getAll(collection) {
138
+ try {
139
+ const result = await this.sendMessage("get", { collection });
140
+ return result || [];
141
+ }
142
+ catch (err) {
143
+ try {
144
+ const errorData = JSON.parse(err.message);
145
+ if (errorData.statusCode === 404)
146
+ return [];
147
+ }
148
+ catch { }
149
+ throw err;
150
+ }
151
+ }
152
+ async delete(collection, key) {
153
+ await this.sendMessage("delete", { collection, keys: key });
154
+ }
155
+ compact() {
156
+ return this.sendMessage("compact");
157
+ }
158
+ /**
159
+ * Truncate and close the OPFS file handle, then remove the OPFS directory.
160
+ *
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.
165
+ *
166
+ * After this resolves, call the `terminate` method and `location.reload()` if needed or re-initialize the database.
167
+ */
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" });
177
+ }
178
+ disconnect() {
179
+ if (this.bc)
180
+ this.bc.close();
181
+ }
182
+ /** Terminates the MoltenDb worker. Call after clearing OPFS storage. */
183
+ terminate() {
184
+ this.disconnect();
185
+ if (this.worker) {
186
+ this.worker.terminate();
187
+ this.worker = null;
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
+ }
87
203
  async startAsLeader() {
88
204
  // Guard: OPFS is required
89
205
  if (!this.options.inMemory) {
@@ -91,18 +207,21 @@ export class MoltenDb {
91
207
  await navigator.storage.getDirectory();
92
208
  }
93
209
  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+).');
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+).");
96
212
  }
97
213
  }
98
214
  this.isLeader = true;
99
215
  if (this.worker)
100
216
  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` });
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
+ });
103
222
  this.worker.onmessage = (e) => {
104
223
  const data = e.data;
105
- if (data.type === 'event') {
224
+ if (data.type === "event") {
106
225
  this.dispatchEvent(data); // ⬅️ Trigger new dispatcher
107
226
  this.bc.postMessage(data);
108
227
  return;
@@ -117,10 +236,9 @@ export class MoltenDb {
117
236
  }
118
237
  };
119
238
  // Wait for worker to boot
120
- await this.sendMessage('init', {
239
+ await this.sendMessage("init", {
121
240
  dbName: this.dbName,
122
241
  encryptionKey: this.options.encryptionKey,
123
- hotThreshold: this.options.hotThreshold,
124
242
  inMemory: this.options.inMemory,
125
243
  maxBodySize: this.options.maxBodySize,
126
244
  maxKeysPerRequest: this.options.maxKeysPerRequest,
@@ -128,25 +246,41 @@ export class MoltenDb {
128
246
  });
129
247
  this.bc.onmessage = async (e) => {
130
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
+ }
131
261
  // Any tab unloading in in-memory mode broadcasts this β€” wipe the shared RAM store.
132
- if (msg.type === 'clear_all') {
262
+ if (msg.type === "clear_all") {
133
263
  try {
134
- await this.sendMessage('clear', {});
135
- this.bc.postMessage({ type: 'cleared' });
136
- console.log('[MoltenDb] In-memory store wiped (tab unloaded).');
264
+ await this.sendMessage("clear", {});
265
+ this.bc.postMessage({ type: "cleared" });
266
+ console.log("[MoltenDb] In-memory store wiped (tab unloaded).");
137
267
  }
138
268
  catch (err) {
139
- console.warn('[MoltenDb] Failed to clear in-memory store:', err);
269
+ console.warn("[MoltenDb] Failed to clear in-memory store:", err);
140
270
  }
141
271
  return;
142
272
  }
143
- if (msg.type === 'query' && msg.action) {
273
+ if (msg.type === "query" && msg.action) {
144
274
  try {
145
275
  const result = await this.sendMessage(msg.action, msg.payload);
146
- this.bc.postMessage({ type: 'response', id: msg.id, result });
276
+ this.bc.postMessage({ type: "response", id: msg.id, result });
147
277
  }
148
278
  catch (err) {
149
- this.bc.postMessage({ type: 'response', id: msg.id, error: err.message });
279
+ this.bc.postMessage({
280
+ type: "response",
281
+ id: msg.id,
282
+ error: err.message,
283
+ });
150
284
  }
151
285
  }
152
286
  };
@@ -159,20 +293,32 @@ export class MoltenDb {
159
293
  }
160
294
  this.bc.onmessage = (e) => {
161
295
  const data = e.data;
162
- if (data.type === 'event') {
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") {
163
309
  this.dispatchEvent(data); // ⬅️ Trigger new dispatcher
164
310
  return;
165
311
  }
166
312
  // In-memory wipe notification from leader β€” reject all in-flight requests.
167
- if (data.type === 'cleared') {
168
- console.log('[MoltenDb] In-memory store was wiped by another tab.');
313
+ if (data.type === "cleared") {
314
+ console.log("[MoltenDb] In-memory store was wiped by another tab.");
169
315
  for (const [id, req] of this.pendingRequests) {
170
- req.reject(new Error('[MoltenDb] In-memory store was cleared by a tab reload.'));
316
+ req.reject(new Error("[MoltenDb] In-memory store was cleared by a tab reload."));
171
317
  this.pendingRequests.delete(id);
172
318
  }
173
319
  return;
174
320
  }
175
- if (data.type === 'response') {
321
+ if (data.type === "response") {
176
322
  const req = this.pendingRequests.get(data.id);
177
323
  if (req) {
178
324
  if (data.error)
@@ -184,91 +330,4 @@ export class MoltenDb {
184
330
  }
185
331
  };
186
332
  }
187
- async sendMessage(action, payload) {
188
- // Wait for the engine to boot before routing the message.
189
- // If the DB is already initialized, this resolves instantly.
190
- if (action !== 'init') {
191
- if (this.initPromise) {
192
- await this.initPromise;
193
- }
194
- else {
195
- throw new Error('[MoltenDb] You must call db.init() before querying the database.');
196
- }
197
- }
198
- // 2. Generate a unique ID
199
- const id = (typeof crypto !== 'undefined' && crypto.randomUUID)
200
- ? crypto.randomUUID()
201
- : Math.random().toString(36).substring(2, 9);
202
- return new Promise((resolve, reject) => {
203
- const successHandler = (res) => resolve(mapToObj(res));
204
- // 3. We are now GUARANTEED that isLeader, worker, and bc are accurately set
205
- if (this.isLeader && this.worker) {
206
- this.pendingRequests.set(id, { resolve: successHandler, reject });
207
- this.worker.postMessage({ id, action, ...payload });
208
- }
209
- else {
210
- // Follower routing via BroadcastChannel
211
- const timer = setTimeout(() => {
212
- if (this.pendingRequests.has(id)) {
213
- this.pendingRequests.delete(id);
214
- reject(new Error(`[MoltenDb] Request "${action}" timed out after 10s.`));
215
- }
216
- }, 10000);
217
- this.pendingRequests.set(id, {
218
- resolve: (res) => { clearTimeout(timer); successHandler(res); },
219
- reject: (e) => { clearTimeout(timer); reject(e); }
220
- });
221
- this.bc.postMessage({ type: 'query', id, action, payload });
222
- }
223
- });
224
- } // ── Convenience CRUD helpers ───────────────────────────────────────────────
225
- async set(collection, key, value) {
226
- await this.sendMessage('set', { collection, data: { [key]: value } });
227
- }
228
- async get(collection, key) {
229
- try {
230
- return await this.sendMessage('get', { collection, keys: key });
231
- }
232
- catch (err) {
233
- try {
234
- const errorData = JSON.parse(err.message);
235
- if (errorData.statusCode === 404)
236
- return null;
237
- }
238
- catch { }
239
- throw err;
240
- }
241
- }
242
- async getAll(collection) {
243
- try {
244
- const result = await this.sendMessage('get', { collection });
245
- return result || [];
246
- }
247
- catch (err) {
248
- try {
249
- const errorData = JSON.parse(err.message);
250
- if (errorData.statusCode === 404)
251
- return [];
252
- }
253
- catch { }
254
- throw err;
255
- }
256
- }
257
- async delete(collection, key) {
258
- await this.sendMessage('delete', { collection, keys: key });
259
- }
260
- compact() {
261
- return this.sendMessage('compact');
262
- }
263
- disconnect() {
264
- if (this.bc)
265
- this.bc.close();
266
- }
267
- terminate() {
268
- this.disconnect();
269
- if (this.worker) {
270
- this.worker.terminate();
271
- this.worker = null;
272
- }
273
- }
274
333
  }
@@ -1,23 +1,23 @@
1
- import init, { WorkerDb } from './wasm/moltendb_core.js';
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 === 'init') {
7
+ if (action === "init") {
8
8
  if (!initPromise) {
9
9
  initPromise = (async () => {
10
10
  await init();
11
11
  // Pass all config flags to Rust
12
- const instance = await WorkerDb.create(payload.dbName, payload.hotThreshold, payload.encryptionKey, payload.writeMode, payload.maxBodySize, payload.maxKeysPerRequest, payload.inMemory);
12
+ const instance = await WorkerDb.create(payload.dbName, payload.encryptionKey, payload.writeMode, payload.maxBodySize, payload.maxKeysPerRequest, payload.inMemory);
13
13
  // Listen to Rust and broadcast events
14
14
  instance.subscribe((eventStr) => {
15
15
  try {
16
16
  const eventData = JSON.parse(eventStr);
17
- self.postMessage({ type: 'event', ...eventData });
17
+ self.postMessage({ type: "event", ...eventData });
18
18
  }
19
19
  catch (err) {
20
- console.error('[MoltenDb Worker] Event parse error', err);
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: 'ok' } });
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 = (error instanceof Map)
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 = (error instanceof Map)
50
+ const errorMsg = error instanceof Map
51
51
  ? JSON.stringify(Object.fromEntries(error))
52
52
  : String(error);
53
53
  self.postMessage({ id, error: errorMsg });
@@ -17,28 +17,6 @@ export class WorkerDb {
17
17
  private constructor();
18
18
  free(): void;
19
19
  [Symbol.dispose](): void;
20
- /**
21
- * Execute an analytics query and return the result as a JSON string.
22
- *
23
- * This is the method called by the dashboard's auto-refresh loop:
24
- * `const resultStr = db.analytics(JSON.stringify(query))`
25
- *
26
- * Takes a JSON string (not a JsValue) because the analytics query format
27
- * is complex and easier to pass as a pre-serialized string from JavaScript.
28
- *
29
- * Returns a JSON string (not a JsValue) so JavaScript can parse it with
30
- * `JSON.parse(resultStr)` and access `result` and `metadata`.
31
- *
32
- * Example input:
33
- * `'{"collection":"events","metric":{"type":"COUNT"},"where":{"event_type":"button_click"}}'`
34
- *
35
- * Example output:
36
- * `'{"result":42,"metadata":{"execution_time_ms":0,"rows_scanned":42}}'`
37
- *
38
- * `#[wasm_bindgen(js_name = analytics)]` sets the JavaScript method name to
39
- * "analytics" (matching the call in analytics-worker.js).
40
- */
41
- analytics(query_json: string): string;
42
20
  /**
43
21
  * Initialize the database and open (or create) the OPFS storage file.
44
22
  *
@@ -56,7 +34,6 @@ export class WorkerDb {
56
34
  * # Arguments
57
35
  * * `db_name` β€” The name of the OPFS file to open (e.g. "click_analytics_db").
58
36
  * Each unique name is a separate database file in the browser's OPFS storage.
59
- * * `hot_threshold` β€” Optional maximum documents per collection to keep in RAM (default: 50,000).
60
37
  * * `encryption_key` β€” Optional password for at-rest encryption.
61
38
  * * `write_mode` β€” Optional write mode: "async" (default) or "sync".
62
39
  * * `max_body_size` β€” Optional maximum request body size in bytes (default: 10MB).
@@ -65,7 +42,7 @@ export class WorkerDb {
65
42
  * When `true`, all data is lost when the worker is terminated β€” useful for ephemeral
66
43
  * session caches or testing without touching OPFS storage.
67
44
  */
68
- static create(db_name: string, hot_threshold?: number | null, encryption_key?: string | null, write_mode?: string | null, max_body_size?: number | null, max_keys_per_request?: number | null, in_memory?: boolean | null): Promise<WorkerDb>;
45
+ static create(db_name: string, encryption_key?: string | null, write_mode?: string | null, max_body_size?: number | null, max_keys_per_request?: number | null, in_memory?: boolean | null): Promise<WorkerDb>;
69
46
  /**
70
47
  * Route an incoming message from the JavaScript worker to the correct handler.
71
48
  *
@@ -81,7 +58,6 @@ export class WorkerDb {
81
58
  * - "update" β†’ patch/merge documents: { collection, data: { key: patch, ... } }
82
59
  * - "delete" β†’ delete documents or drop: { collection, keys: ... } or { drop: true }
83
60
  * - "compact" β†’ compact the OPFS log file
84
- * - "get_size" β†’ return current OPFS file size in bytes
85
61
  * - "clear" β†’ wipe all in-memory state (in-memory mode only)
86
62
  *
87
63
  * Returns a JsValue result on success, or a JsValue error string on failure.
@@ -100,12 +76,11 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
100
76
  export interface InitOutput {
101
77
  readonly memory: WebAssembly.Memory;
102
78
  readonly __wbg_workerdb_free: (a: number, b: number) => void;
103
- readonly workerdb_analytics: (a: number, b: number, c: number, d: number) => void;
104
- readonly workerdb_create: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => number;
79
+ readonly workerdb_create: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => number;
105
80
  readonly workerdb_handle_message: (a: number, b: number, c: number) => void;
106
81
  readonly workerdb_subscribe: (a: number, b: number) => void;
107
- readonly __wasm_bindgen_func_elem_4220: (a: number, b: number, c: number, d: number) => void;
108
- readonly __wasm_bindgen_func_elem_4230: (a: number, b: number, c: number, d: number) => void;
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;
109
84
  readonly __wbindgen_export: (a: number, b: number) => number;
110
85
  readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
111
86
  readonly __wbindgen_export3: (a: number) => void;
@@ -30,47 +30,6 @@ export class WorkerDb {
30
30
  const ptr = this.__destroy_into_raw();
31
31
  wasm.__wbg_workerdb_free(ptr, 0);
32
32
  }
33
- /**
34
- * Execute an analytics query and return the result as a JSON string.
35
- *
36
- * This is the method called by the dashboard's auto-refresh loop:
37
- * `const resultStr = db.analytics(JSON.stringify(query))`
38
- *
39
- * Takes a JSON string (not a JsValue) because the analytics query format
40
- * is complex and easier to pass as a pre-serialized string from JavaScript.
41
- *
42
- * Returns a JSON string (not a JsValue) so JavaScript can parse it with
43
- * `JSON.parse(resultStr)` and access `result` and `metadata`.
44
- *
45
- * Example input:
46
- * `'{"collection":"events","metric":{"type":"COUNT"},"where":{"event_type":"button_click"}}'`
47
- *
48
- * Example output:
49
- * `'{"result":42,"metadata":{"execution_time_ms":0,"rows_scanned":42}}'`
50
- *
51
- * `#[wasm_bindgen(js_name = analytics)]` sets the JavaScript method name to
52
- * "analytics" (matching the call in analytics-worker.js).
53
- * @param {string} query_json
54
- * @returns {string}
55
- */
56
- analytics(query_json) {
57
- let deferred2_0;
58
- let deferred2_1;
59
- try {
60
- const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
61
- const ptr0 = passStringToWasm0(query_json, wasm.__wbindgen_export, wasm.__wbindgen_export2);
62
- const len0 = WASM_VECTOR_LEN;
63
- wasm.workerdb_analytics(retptr, this.__wbg_ptr, ptr0, len0);
64
- var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
65
- var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
66
- deferred2_0 = r0;
67
- deferred2_1 = r1;
68
- return getStringFromWasm0(r0, r1);
69
- } finally {
70
- wasm.__wbindgen_add_to_stack_pointer(16);
71
- wasm.__wbindgen_export4(deferred2_0, deferred2_1, 1);
72
- }
73
- }
74
33
  /**
75
34
  * Initialize the database and open (or create) the OPFS storage file.
76
35
  *
@@ -88,7 +47,6 @@ export class WorkerDb {
88
47
  * # Arguments
89
48
  * * `db_name` β€” The name of the OPFS file to open (e.g. "click_analytics_db").
90
49
  * Each unique name is a separate database file in the browser's OPFS storage.
91
- * * `hot_threshold` β€” Optional maximum documents per collection to keep in RAM (default: 50,000).
92
50
  * * `encryption_key` β€” Optional password for at-rest encryption.
93
51
  * * `write_mode` β€” Optional write mode: "async" (default) or "sync".
94
52
  * * `max_body_size` β€” Optional maximum request body size in bytes (default: 10MB).
@@ -97,7 +55,6 @@ export class WorkerDb {
97
55
  * When `true`, all data is lost when the worker is terminated β€” useful for ephemeral
98
56
  * session caches or testing without touching OPFS storage.
99
57
  * @param {string} db_name
100
- * @param {number | null} [hot_threshold]
101
58
  * @param {string | null} [encryption_key]
102
59
  * @param {string | null} [write_mode]
103
60
  * @param {number | null} [max_body_size]
@@ -105,14 +62,14 @@ export class WorkerDb {
105
62
  * @param {boolean | null} [in_memory]
106
63
  * @returns {Promise<WorkerDb>}
107
64
  */
108
- static create(db_name, hot_threshold, encryption_key, write_mode, max_body_size, max_keys_per_request, in_memory) {
65
+ static create(db_name, encryption_key, write_mode, max_body_size, max_keys_per_request, in_memory) {
109
66
  const ptr0 = passStringToWasm0(db_name, wasm.__wbindgen_export, wasm.__wbindgen_export2);
110
67
  const len0 = WASM_VECTOR_LEN;
111
68
  var ptr1 = isLikeNone(encryption_key) ? 0 : passStringToWasm0(encryption_key, wasm.__wbindgen_export, wasm.__wbindgen_export2);
112
69
  var len1 = WASM_VECTOR_LEN;
113
70
  var ptr2 = isLikeNone(write_mode) ? 0 : passStringToWasm0(write_mode, wasm.__wbindgen_export, wasm.__wbindgen_export2);
114
71
  var len2 = WASM_VECTOR_LEN;
115
- const ret = wasm.workerdb_create(ptr0, len0, isLikeNone(hot_threshold) ? 0x100000001 : (hot_threshold) >>> 0, ptr1, len1, ptr2, len2, isLikeNone(max_body_size) ? 0x100000001 : (max_body_size) >>> 0, isLikeNone(max_keys_per_request) ? 0x100000001 : (max_keys_per_request) >>> 0, isLikeNone(in_memory) ? 0xFFFFFF : in_memory ? 1 : 0);
72
+ const ret = wasm.workerdb_create(ptr0, len0, ptr1, len1, ptr2, len2, isLikeNone(max_body_size) ? 0x100000001 : (max_body_size) >>> 0, isLikeNone(max_keys_per_request) ? 0x100000001 : (max_keys_per_request) >>> 0, isLikeNone(in_memory) ? 0xFFFFFF : in_memory ? 1 : 0);
116
73
  return takeObject(ret);
117
74
  }
118
75
  /**
@@ -130,7 +87,6 @@ export class WorkerDb {
130
87
  * - "update" β†’ patch/merge documents: { collection, data: { key: patch, ... } }
131
88
  * - "delete" β†’ delete documents or drop: { collection, keys: ... } or { drop: true }
132
89
  * - "compact" β†’ compact the OPFS log file
133
- * - "get_size" β†’ return current OPFS file size in bytes
134
90
  * - "clear" β†’ wipe all in-memory state (in-memory mode only)
135
91
  *
136
92
  * Returns a JsValue result on success, or a JsValue error string on failure.
@@ -275,9 +231,6 @@ function __wbg_get_imports() {
275
231
  const ret = Object.entries(getObject(arg0));
276
232
  return addHeapObject(ret);
277
233
  },
278
- __wbg_error_2001591ad2463697: function(arg0) {
279
- console.error(getObject(arg0));
280
- },
281
234
  __wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) {
282
235
  let deferred0_0;
283
236
  let deferred0_1;
@@ -420,7 +373,7 @@ function __wbg_get_imports() {
420
373
  const a = state0.a;
421
374
  state0.a = 0;
422
375
  try {
423
- return __wasm_bindgen_func_elem_4230(a, state0.b, arg0, arg1);
376
+ return __wasm_bindgen_func_elem_4178(a, state0.b, arg0, arg1);
424
377
  } finally {
425
378
  state0.a = a;
426
379
  }
@@ -455,14 +408,6 @@ function __wbg_get_imports() {
455
408
  const ret = Date.now();
456
409
  return ret;
457
410
  },
458
- __wbg_now_e7c6795a7f81e10f: function(arg0) {
459
- const ret = getObject(arg0).now();
460
- return ret;
461
- },
462
- __wbg_performance_3fcf6e32a7e1ed0a: function(arg0) {
463
- const ret = getObject(arg0).performance;
464
- return addHeapObject(ret);
465
- },
466
411
  __wbg_process_44c7a14e11e9f69e: function(arg0) {
467
412
  const ret = getObject(arg0).process;
468
413
  return addHeapObject(ret);
@@ -567,8 +512,8 @@ function __wbg_get_imports() {
567
512
  return ret;
568
513
  }, arguments); },
569
514
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
570
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 774, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
571
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_4220);
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);
572
517
  return addHeapObject(ret);
573
518
  },
574
519
  __wbindgen_cast_0000000000000002: function(arg0) {
@@ -610,10 +555,10 @@ function __wbg_get_imports() {
610
555
  };
611
556
  }
612
557
 
613
- function __wasm_bindgen_func_elem_4220(arg0, arg1, arg2) {
558
+ function __wasm_bindgen_func_elem_4166(arg0, arg1, arg2) {
614
559
  try {
615
560
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
616
- wasm.__wasm_bindgen_func_elem_4220(retptr, arg0, arg1, addHeapObject(arg2));
561
+ wasm.__wasm_bindgen_func_elem_4166(retptr, arg0, arg1, addHeapObject(arg2));
617
562
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
618
563
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
619
564
  if (r1) {
@@ -624,8 +569,8 @@ function __wasm_bindgen_func_elem_4220(arg0, arg1, arg2) {
624
569
  }
625
570
  }
626
571
 
627
- function __wasm_bindgen_func_elem_4230(arg0, arg1, arg2, arg3) {
628
- wasm.__wasm_bindgen_func_elem_4230(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
572
+ function __wasm_bindgen_func_elem_4178(arg0, arg1, arg2, arg3) {
573
+ wasm.__wasm_bindgen_func_elem_4178(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
629
574
  }
630
575
 
631
576
  const WorkerDbFinalization = (typeof FinalizationRegistry === 'undefined')
Binary file
@@ -2,12 +2,11 @@
2
2
  /* eslint-disable */
3
3
  export const memory: WebAssembly.Memory;
4
4
  export const __wbg_workerdb_free: (a: number, b: number) => void;
5
- export const workerdb_analytics: (a: number, b: number, c: number, d: number) => void;
6
- export const workerdb_create: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => number;
5
+ export const workerdb_create: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => number;
7
6
  export const workerdb_handle_message: (a: number, b: number, c: number) => void;
8
7
  export const workerdb_subscribe: (a: number, b: number) => void;
9
- export const __wasm_bindgen_func_elem_4220: (a: number, b: number, c: number, d: number) => void;
10
- export const __wasm_bindgen_func_elem_4230: (a: number, b: number, c: number, d: number) => void;
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;
11
10
  export const __wbindgen_export: (a: number, b: number) => number;
12
11
  export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
13
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": "1.7.0",
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 <maximilian.both27@outlook.com>",
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",