@moltendb-web/core 0.1.0-beta.1 → 0.1.0-rc.1

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/LICENSE.md CHANGED
@@ -1,35 +1,21 @@
1
- # MoltenDB License
2
-
3
- MoltenDB is dual-licensed:
4
-
5
- 1. **Free for Startups & Personal Use:** Licensed under the Business Source License (BSL) 1.1 with an Additional Use Grant (see below).
6
- 2. **Commercial/Enterprise Use:** For companies exceeding the revenue threshold, or companies wishing to provide MoltenDB as a managed cloud service, a commercial license is required. Contact: [maximilian.both27@outlook.com]
7
-
8
- ---
9
-
10
- ## Business Source License 1.1
11
-
12
- License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
13
- "Business Source License" is a trademark of MariaDB Corporation Ab.
14
-
15
- **Licensor:** Maximilian Both.
16
- **Licensed Work:** The MoltenDB source code and compiled binaries.
17
- **Change Date:** 3 years after the publication of each specific version/release.
18
- **Change License:** MIT License
19
-
20
- ### Additional Use Grant
21
-
22
- You may make use of the Licensed Work (including running it in production) provided that your organization (including any parent companies, subsidiaries, and affiliates) meets **ALL** of the following conditions:
23
-
24
- 1. **Revenue Limit:** Your organization's total annual gross revenue does not exceed $5,000,000 USD (or equivalent).
25
- 2. **Not a Managed Service:** You do not offer the Licensed Work to third parties as a hosted or managed service (e.g., Database-as-a-Service, Backend-as-a-Service), regardless of your company's revenue.
26
-
27
- If you meet these conditions, the Licensor grants you a non-exclusive, royalty-free, worldwide license to reproduce, use, merge, publish, distribute, and modify the Licensed Work.
28
-
29
- If your organization exceeds the Revenue Limit, or if you wish to provide the Licensed Work as a managed service, you must purchase a Commercial License from the Licensor.
30
-
31
- ### BSL General Terms
32
-
33
- If your use of the Licensed Work does not comply with the Additional Use Grant, you may still use the Licensed Work strictly for non-production, internal testing, and development purposes.
34
-
35
- Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License (MIT License), and the terms of this Business Source License will terminate for that specific version.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maximilian Both
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -9,8 +9,9 @@
9
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)
10
10
 
11
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-BSL%201.1-blue?style=flat-square)](LICENSE.md)
12
+ [![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)](LICENSE.md)
13
13
  [![WASM](https://img.shields.io/badge/wasm-optimized-magenta?style=flat-square)](https://webassembly.org/)
14
+ [![Status](https://img.shields.io/badge/status-release%20candidate-brightgreen?style=flat-square)]()
14
15
 
15
16
  </div>
16
17
 
@@ -20,6 +21,8 @@
20
21
 
21
22
  MoltenDB is a JSON document database written in Rust that runs directly in your browser. Unlike traditional browser databases limited by `localStorage` quotas or IndexedDB's complex API, MoltenDB leverages the **Origin Private File System (OPFS)** to provide a high-performance, append-only storage engine.
22
23
 
24
+ > **🚀 Release Candidate** — The core engine, multi-tab sync, and storage layer are feature-complete and stabilised for v1. Server sync, encryption and analytics are planned for a future release.
25
+
23
26
  ### 🎮 Explore the Full Functionality
24
27
 
25
28
  The best way to experience MoltenDB is through the **[Interactive Demo on StackBlitz](https://stackblitz.com/~/github.com/maximilian27/moltendb-wasm-demo?file=package.json)**. It provides a complete, live environment where you can test query builder expressions, perform mutations, and see real-time events with zero local setup.
@@ -32,6 +35,9 @@ Prefer to run it in your own environment? You can **[clone the demo repository](
32
35
  - **Pure Rust Engine:** The same query logic used in our server binary, compiled to WebAssembly.
33
36
  - **OPFS Persistence:** Data persists across page reloads in a dedicated, high-speed sandbox.
34
37
  - **Worker-Threaded:** The database runs entirely inside a Web Worker—zero impact on your UI thread.
38
+ - **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.
39
+ - **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.
40
+ - **Real-Time Pub/Sub (`onEvent`):** Every write and delete emits a typed `DBEvent` to all open tabs instantly — no polling, no extra infrastructure.
35
41
  - **GraphQL-style Selection:** Request only the fields you need (even deeply nested ones) to save memory and CPU.
36
42
  - **Auto-Indexing:** The engine monitors your queries and automatically creates indexes for frequently filtered fields.
37
43
  - **Conflict Resolution:** Incoming writes with `_v ≤ stored _v` are silently skipped.
@@ -50,7 +56,30 @@ npm install @moltendb-web/core
50
56
  # Install the chainable query builder
51
57
  npm install @moltendb-web/query
52
58
  ```
59
+ 📦 **Bundler Setup**
60
+
61
+ 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.
53
62
 
63
+ **For Vite:**
64
+ Exclude the core package from pre-bundling in your vite.config.js:
65
+
66
+ ```js
67
+ // vite.config.js`
68
+ export default defineConfig({
69
+ optimizeDeps: { exclude: ['@moltendb-web/core'] }
70
+ });
71
+ ```
72
+
73
+ **For Webpack 5 (Next.js, Create React App):**
74
+ Ensure Webpack treats the `.wasm` binary as a static resource in `webpack.config.js`:
75
+
76
+ ```js
77
+ module.exports = {
78
+ module: {
79
+ rules: [{ test: /\.wasm$/, type: 'asset/resource' }]
80
+ }
81
+ };
82
+ ```
54
83
  ---
55
84
 
56
85
  # Quick Start
@@ -62,12 +91,11 @@ TypeScript
62
91
  import { MoltenDB } from '@moltendb-web/core';
63
92
  import { MoltenDBClient, WorkerTransport } from '@moltendb-web/query';
64
93
 
65
- const workerUrl = new URL('@moltendb-web/core/worker', import.meta.url).href;
66
- const db = new MoltenDB('moltendb_demo', { syncEnabled: false, workerUrl });
94
+ const db = new MoltenDB('moltendb_demo');
67
95
  await db.init();
68
96
 
69
97
  // Connect the query builder to the WASM worker
70
- const client = new MoltenDBClient(new WorkerTransport(db.worker));
98
+ const client = new MoltenDBClient(db);
71
99
 
72
100
  // 2. Insert and Query
73
101
 
@@ -222,9 +250,63 @@ await client.collection('laptops')
222
250
 
223
251
  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.
224
252
 
225
- - **Compaction:** When the log exceeds 5MB or 500 entries, the engine automatically "squashes" the log, removing old versions of documents to save space.
253
+ - **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.
226
254
  - **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.
227
255
 
256
+ ### Multi-Tab Sync
257
+
258
+ MoltenDB uses the **Web Locks API** for leader election. The first tab to acquire the lock becomes the *leader* and owns the OPFS file handle directly. Every subsequent tab becomes a *follower* and proxies all reads and writes through a `BroadcastChannel` to the leader.
259
+
260
+ When the leader tab is closed, the next queued follower automatically acquires the lock and promotes itself to leader — no data loss, no manual reconnection required.
261
+
262
+ ```
263
+ Tab 1 (Leader) ──owns──▶ Web Worker ──▶ WASM Engine ──▶ OPFS
264
+
265
+ └── BroadcastChannel ──▶ Tab 2 (Follower)
266
+ ──▶ Tab 3 (Follower)
267
+ ```
268
+
269
+ ### Real-Time Events (Pub/Sub)
270
+
271
+ MoltenDB has a built-in pub/sub system that automatically notifies **all open tabs** whenever a document is created, updated, or deleted — no polling required.
272
+
273
+ Assign a callback to `onEvent` after calling `init()`:
274
+
275
+ ```ts
276
+ const db = new MoltenDB('my-app');
277
+ await db.init();
278
+
279
+ db.onEvent = (event) => {
280
+ console.log(event.event); // 'change' | 'delete' | 'drop'
281
+ console.log(event.collection); // e.g. 'laptops'
282
+ console.log(event.key); // e.g. 'lp1'
283
+ console.log(event.new_v); // new version number, or null on delete
284
+ };
285
+ ```
286
+
287
+ 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:
288
+
289
+ ```ts
290
+ db.onEvent = ({ event, collection, key }) => {
291
+ if (collection === 'laptops') {
292
+ refreshLaptopList(); // re-query and re-render
293
+ }
294
+ };
295
+ ```
296
+
297
+ The `DBEvent` type is exported from the package for full TypeScript support:
298
+
299
+ ```ts
300
+ import { MoltenDB, DBEvent } from '@moltendb-web/core';
301
+
302
+ const db = new MoltenDB('my-app');
303
+ await db.init();
304
+
305
+ db.onEvent = (e: DBEvent) => { /* fully typed */ };
306
+ ```
307
+
308
+ ---
309
+
228
310
  ### Performance Note
229
311
 
230
312
  Because MoltenDB uses OPFS, your browser must support `SharedArrayBuffer`. Most modern browsers support this, but your server must send the following headers:
@@ -234,6 +316,39 @@ Cross-Origin-Opener-Policy: same-origin
234
316
  Cross-Origin-Embedder-Policy: require-corp
235
317
  ```
236
318
 
319
+ ---
320
+
321
+ ## Testing
322
+
323
+ The core package ships with a comprehensive test suite built on **Vitest**:
324
+
325
+ ```bash
326
+ cd packages/core
327
+ npm test # run all unit & integration tests
328
+ npm run test:coverage # with coverage report
329
+ ```
330
+
331
+ ### What's covered
332
+
333
+ | Suite | Tests | What it verifies |
334
+ |---|---|---|
335
+ | `init()` | 5 | Leader election, idempotency, worker error propagation |
336
+ | CRUD — leader | 9 | set/get/delete/getAll round-trips, collection isolation |
337
+ | CRUD — follower | 3 | BroadcastChannel proxy path for all mutations |
338
+ | Worker error handling | 3 | Transient errors, unknown actions, request isolation |
339
+ | Leader promotion | 2 | Follower takes over when leader tab closes |
340
+ | `onEvent` hook | 2 | Real-time event delivery to leader and followers |
341
+ | Follower timeout | 1 | Pending requests reject after 10 s if leader disappears |
342
+ | `terminate` / `disconnect` | 3 | Worker cleanup, timer teardown |
343
+ | Stress — rapid writes | 3 | 100 sequential, 50 concurrent, interleaved set/delete |
344
+ | BC name isolation | 2 | Two databases on the same origin don't bleed data |
345
+ | Bulk insert stress | 3 | 1 000 concurrent sets, 500 mixed ops, compact under pressure |
346
+ | Multi-tab parallel stress | 4 | 3 tabs × 100 writes, ID collision safety, follower reads after burst, promotion under load |
347
+
348
+ **Total: 50 tests — all green.**
349
+
350
+ ---
351
+
237
352
  ## Project Structure
238
353
 
239
354
  This monorepo contains the following packages:
@@ -243,17 +358,26 @@ This monorepo contains the following packages:
243
358
 
244
359
  ## Roadmap
245
360
 
246
- - [ ] ~~**Multi-Tab Sync:** Leader election for multiple tabs to share a single OPFS instance.~~
361
+ - [x] **Multi-Tab Sync:** Leader election for multiple tabs to share a single OPFS instance — **stabilised in RC1**.
362
+ - [x] **Automatic Compaction:** Log compacts automatically at 500 entries or 5 MB — **stabilised in RC1**.
363
+ - [x] **Rich Test Suite:** 50 unit, integration, and stress tests via Vitest — **stabilised in RC1**.
364
+ - [ ] **React Adapter:** Official `@moltendb-web/react` package with `useQuery` hooks and real-time context providers.
365
+ - [ ] **Angular Adapter:** Official `@moltendb-web/angular` package featuring RxJS observables and Signal-based data fetching.
247
366
  - [ ] **Delta Sync:** Automatic two-way sync with the MoltenDB Rust server.
248
- - [ ] **Analytics functionality:** Run analytics queries straight in the browser.
367
+ - [ ] **Data Encryption:** Transparent encryption-at-rest using hardware-backed keys (Web Crypto API).
368
+ - [ ] **Analytics Functionality:** Run complex analytics queries straight in the browser without blocking the UI.
249
369
 
250
- ## License
251
370
 
252
- MoltenDB is licensed under the **Business Source License 1.1**.
371
+ ## Contributing & Feedback
253
372
 
254
- - Free for personal use and organizations with annual revenue under $5 million USD.
255
- - Converts to MIT automatically 3 years after each version's release date.
373
+ Found a bug or have a feature request? Please open an issue on the [GitHub issue tracker](https://github.com/maximilian27/moltendb-web/issues).
256
374
 
257
- For commercial licensing or questions: [maximilian.both27@outlook.com](mailto:maximilian.both27@outlook.com)
375
+ ---
376
+
377
+ ## License
378
+
379
+ The MoltenDB Web packages (`@moltendb-web/core` and `@moltendb-web/query`) are licensed under the MIT License.
258
380
 
381
+ 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).
259
382
 
383
+ For commercial licensing or questions: [maximilian.both27@outlook.com](mailto:maximilian.both27@outlook.com)
package/dist/index.d.ts CHANGED
@@ -9,6 +9,8 @@ export interface MoltenDBOptions {
9
9
  syncIntervalMs?: number;
10
10
  /** JWT token for WebSocket authentication. */
11
11
  authToken?: string;
12
+ /** Called whenever a DB mutation event is broadcast (all tabs). */
13
+ onEvent?: (event: DBEvent) => void;
12
14
  }
13
15
  export type SyncCallback = (update: {
14
16
  event: 'change' | 'delete' | 'drop';
@@ -16,11 +18,17 @@ export type SyncCallback = (update: {
16
18
  key: string;
17
19
  new_v: number | null;
18
20
  }) => void;
21
+ export interface DBEvent {
22
+ type: 'event';
23
+ event: 'change' | 'delete' | 'drop';
24
+ collection: string;
25
+ key: string;
26
+ new_v: number | null;
27
+ }
19
28
  export declare class MoltenDB {
20
29
  readonly dbName: string;
21
- readonly workerUrl: string | URL;
30
+ readonly workerUrl?: string | URL;
22
31
  worker: Worker | null;
23
- private messageId;
24
32
  private pendingRequests;
25
33
  isLeader: boolean;
26
34
  private bc;
@@ -33,17 +41,18 @@ export declare class MoltenDB {
33
41
  private syncQueue;
34
42
  private syncTimer;
35
43
  /** ⚡ Hook to listen to native real-time DB mutations (works on all tabs) */
36
- onEvent?: (event: any) => void;
44
+ onEvent?: (event: DBEvent) => void;
37
45
  constructor(dbName?: string, options?: MoltenDBOptions);
46
+ private initialized;
38
47
  init(): Promise<void>;
39
48
  private startAsLeader;
40
49
  private startAsFollower;
41
50
  sendMessage(action: string, payload?: Record<string, unknown>): Promise<any>;
42
- set(collection: string, key: string, value: Record<string, unknown>, options?: {
51
+ set(collection: string, key: string, value: any, options?: {
43
52
  skipSync?: boolean;
44
53
  }): Promise<void>;
45
54
  get(collection: string, key: string): Promise<unknown>;
46
- getAll(collection: string): Promise<unknown>;
55
+ getAll(collection: string): Promise<unknown[]>;
47
56
  delete(collection: string, key: string, options?: {
48
57
  skipSync?: boolean;
49
58
  }): Promise<void>;
package/dist/index.js CHANGED
@@ -2,7 +2,6 @@ export class MoltenDB {
2
2
  dbName;
3
3
  workerUrl;
4
4
  worker = null;
5
- messageId = 0;
6
5
  pendingRequests = new Map();
7
6
  // Multi-tab Sync State
8
7
  isLeader = false;
@@ -20,32 +19,37 @@ export class MoltenDB {
20
19
  onEvent;
21
20
  constructor(dbName = 'moltendb', options = {}) {
22
21
  this.dbName = dbName;
23
- // Zero-config default worker resolution
24
- this.workerUrl = options.workerUrl ?? new URL('./moltendb-worker.js', import.meta.url).href;
22
+ this.workerUrl = options.workerUrl;
25
23
  this.syncEnabled = options.syncEnabled ?? false;
26
24
  this.serverUrl = options.serverUrl ?? 'wss://localhost:3000/ws';
27
25
  this.syncIntervalMs = options.syncIntervalMs ?? 5000;
28
26
  this.authToken = options.authToken;
27
+ if (options.onEvent)
28
+ this.onEvent = options.onEvent;
29
29
  }
30
+ initialized = false;
30
31
  async init() {
32
+ if (this.initialized)
33
+ return;
34
+ this.initialized = true;
31
35
  this.bc = new BroadcastChannel(`moltendb_channel_${this.dbName}`);
32
- return new Promise((resolveInit) => {
33
- // 1. Try to grab the lock immediately (Leader Election)
36
+ return new Promise((resolveInit, rejectInit) => {
34
37
  navigator.locks.request(`moltendb_lock_${this.dbName}`, { ifAvailable: true }, async (lock) => {
35
38
  if (lock) {
36
- // We got the lock! We are the active DB host.
37
- await this.startAsLeader();
38
- resolveInit();
39
- // Return a promise that never resolves to hold the lock until the tab closes
40
- return new Promise(() => { });
39
+ try {
40
+ await this.startAsLeader();
41
+ resolveInit();
42
+ }
43
+ catch (err) {
44
+ rejectInit(err);
45
+ }
46
+ return new Promise(() => { }); // Hold lock
41
47
  }
42
48
  else {
43
- // Lock is taken. We are a proxy follower.
44
49
  this.startAsFollower();
45
50
  resolveInit();
46
- // 2. Queue up in the background. If the Leader tab closes, this lock resolves!
47
- navigator.locks.request(`moltendb_lock_${this.dbName}`, async (fallbackLock) => {
48
- console.log(`[MoltenDB] Previous leader disconnected. Promoting this tab to Leader.`);
51
+ navigator.locks.request(`moltendb_lock_${this.dbName}`, async () => {
52
+ console.log(`[MoltenDB] Promoting this tab to Leader.`);
49
53
  await this.startAsLeader();
50
54
  return new Promise(() => { }); // Hold lock
51
55
  });
@@ -54,22 +58,27 @@ export class MoltenDB {
54
58
  });
55
59
  }
56
60
  async startAsLeader() {
61
+ // Guard: OPFS is required
62
+ try {
63
+ await navigator.storage.getDirectory();
64
+ }
65
+ catch {
66
+ throw new Error('[MoltenDB] Origin Private File System (OPFS) is not available in this browser context. ' +
67
+ 'Try a non-private window or a browser that supports OPFS (Chrome 102+, Firefox 111+, Safari 15.2+).');
68
+ }
57
69
  this.isLeader = true;
58
70
  if (this.worker)
59
- this.worker.terminate(); // Clean slate if promoted
60
- this.worker = new Worker(this.workerUrl, { type: 'module', name: `moltendb-${this.dbName}-leader` });
61
- // Handle messages strictly from our local Worker
71
+ this.worker.terminate();
72
+ const url = this.workerUrl || new URL('./moltendb-worker.js', import.meta.url);
73
+ this.worker = new Worker(url, { type: 'module', name: `moltendb-${this.dbName}-leader` });
62
74
  this.worker.onmessage = (e) => {
63
75
  const data = e.data;
64
76
  if (data.type === 'event') {
65
- // Trigger local UI hook
66
77
  if (this.onEvent)
67
78
  this.onEvent(data);
68
- // Broadcast the native event to all Follower tabs
69
79
  this.bc.postMessage(data);
70
80
  return;
71
81
  }
72
- // Resolve pending local promises
73
82
  const req = this.pendingRequests.get(data.id);
74
83
  if (req) {
75
84
  if (data.error)
@@ -79,13 +88,8 @@ export class MoltenDB {
79
88
  this.pendingRequests.delete(data.id);
80
89
  }
81
90
  };
82
- // Initialize the WASM Engine
83
- await new Promise((resolve, reject) => {
84
- const id = this.messageId++;
85
- this.pendingRequests.set(id, { resolve, reject });
86
- this.worker.postMessage({ id, action: 'init', dbName: this.dbName, workerUrl: this.workerUrl });
87
- });
88
- // Listen to the BroadcastChannel for queries coming from Follower tabs
91
+ // Wait for worker to boot
92
+ await this.sendMessage('init', { dbName: this.dbName });
89
93
  this.bc.onmessage = async (e) => {
90
94
  const msg = e.data;
91
95
  if (msg.type === 'query' && msg.action) {
@@ -98,29 +102,23 @@ export class MoltenDB {
98
102
  }
99
103
  }
100
104
  };
101
- // If backend sync is enabled, only the Leader manages the WebSocket
102
- if (this.syncEnabled) {
105
+ if (this.syncEnabled)
103
106
  this.startSync();
104
- }
105
107
  }
106
108
  startAsFollower() {
107
109
  this.isLeader = false;
108
- // We don't need a worker, we rely on the Leader.
109
110
  if (this.worker) {
110
111
  this.worker.terminate();
111
112
  this.worker = null;
112
113
  }
113
- // Listen to the BroadcastChannel for answers from the Leader
114
114
  this.bc.onmessage = (e) => {
115
115
  const data = e.data;
116
116
  if (data.type === 'event') {
117
- // Trigger local UI hook as if it happened in this tab
118
117
  if (this.onEvent)
119
118
  this.onEvent(data);
120
119
  return;
121
120
  }
122
121
  if (data.type === 'response') {
123
- // Resolve our proxied promises
124
122
  const req = this.pendingRequests.get(data.id);
125
123
  if (req) {
126
124
  if (data.error)
@@ -133,31 +131,63 @@ export class MoltenDB {
133
131
  };
134
132
  }
135
133
  async sendMessage(action, payload) {
136
- const id = this.messageId++;
134
+ // FIX: Use random UUIDs so tabs don't collide on message IDs
135
+ const id = crypto.randomUUID();
137
136
  return new Promise((resolve, reject) => {
138
- this.pendingRequests.set(id, { resolve, reject });
139
137
  if (this.isLeader && this.worker) {
140
- // Direct execution on the local Worker
138
+ this.pendingRequests.set(id, { resolve, reject });
141
139
  this.worker.postMessage({ id, action, ...payload });
142
140
  }
143
141
  else {
144
- // Proxy the request to the Leader tab
142
+ const timer = setTimeout(() => {
143
+ if (this.pendingRequests.has(id)) {
144
+ this.pendingRequests.delete(id);
145
+ reject(new Error(`[MoltenDB] Request "${action}" timed out.`));
146
+ }
147
+ }, 10000);
148
+ this.pendingRequests.set(id, {
149
+ resolve: (v) => { clearTimeout(timer); resolve(v); },
150
+ reject: (e) => { clearTimeout(timer); reject(e); }
151
+ });
145
152
  this.bc.postMessage({ type: 'query', id, action, payload });
146
153
  }
147
154
  });
148
155
  }
149
- // ── Convenience CRUD helpers ──────────────────────────────────────────────
156
+ // ── Convenience CRUD helpers (CLEANED - NO DUPLICATES) ─────────────────────
150
157
  async set(collection, key, value, options = {}) {
151
158
  await this.sendMessage('set', { collection, data: { [key]: value } });
152
159
  if (this.syncEnabled && !options.skipSync && this.isLeader) {
153
160
  this.syncQueue.push({ action: 'set', collection, data: { [key]: value } });
154
161
  }
155
162
  }
156
- get(collection, key) {
157
- return this.sendMessage('get', { collection, keys: key });
163
+ async get(collection, key) {
164
+ try {
165
+ return await this.sendMessage('get', { collection, keys: key });
166
+ }
167
+ catch (err) {
168
+ try {
169
+ const errorData = JSON.parse(err.message);
170
+ if (errorData.statusCode === 404)
171
+ return null;
172
+ }
173
+ catch { }
174
+ throw err;
175
+ }
158
176
  }
159
- getAll(collection) {
160
- return this.sendMessage('get', { collection });
177
+ async getAll(collection) {
178
+ try {
179
+ const result = await this.sendMessage('get', { collection });
180
+ return result || [];
181
+ }
182
+ catch (err) {
183
+ try {
184
+ const errorData = JSON.parse(err.message);
185
+ if (errorData.statusCode === 404)
186
+ return [];
187
+ }
188
+ catch { }
189
+ throw err;
190
+ }
161
191
  }
162
192
  async delete(collection, key, options = {}) {
163
193
  await this.sendMessage('delete', { collection, keys: key });
@@ -184,7 +214,8 @@ export class MoltenDB {
184
214
  cb(msg);
185
215
  }
186
216
  }
187
- catch (err) { }
217
+ catch (err) {
218
+ }
188
219
  };
189
220
  this.syncTimer = setInterval(async () => {
190
221
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
@@ -208,7 +239,9 @@ export class MoltenDB {
208
239
  }
209
240
  terminate() {
210
241
  this.disconnect();
211
- if (this.worker)
242
+ if (this.worker) {
212
243
  this.worker.terminate();
244
+ this.worker = null;
245
+ }
213
246
  }
214
247
  }
@@ -1,36 +1,55 @@
1
1
  import init, { WorkerDb } from './moltendb.js';
2
- let db;
2
+ let db = null;
3
+ let initPromise = null;
3
4
  self.onmessage = async (e) => {
4
5
  const { id, action, ...payload } = e.data;
5
- // --- Initialization Phase ---
6
+ // --- 1. Initialization Phase ---
6
7
  if (action === 'init') {
8
+ if (!initPromise) {
9
+ initPromise = (async () => {
10
+ await init();
11
+ // FIX: You must await the async constructor
12
+ const instance = await new WorkerDb(payload.dbName);
13
+ // Listen to Rust and broadcast events
14
+ instance.subscribe((eventStr) => {
15
+ try {
16
+ const eventData = JSON.parse(eventStr);
17
+ self.postMessage({ type: 'event', ...eventData });
18
+ }
19
+ catch (err) {
20
+ console.error('[MoltenDB Worker] Event parse error', err);
21
+ }
22
+ });
23
+ db = instance;
24
+ return instance;
25
+ })();
26
+ }
7
27
  try {
8
- await init();
9
- db = await new WorkerDb(payload.dbName);
10
- // THE NATIVE FEED: Listen to Rust and broadcast to the main thread
11
- db.subscribe((eventStr) => {
12
- try {
13
- const eventData = JSON.parse(eventStr);
14
- // Use type: 'event' so the transport knows it's an unsolicited broadcast
15
- self.postMessage({ type: 'event', ...eventData });
16
- }
17
- catch (err) {
18
- console.error('[MoltenDB Worker] Failed to parse event', err);
19
- }
20
- });
28
+ await initPromise;
21
29
  self.postMessage({ id, result: { status: 'ok' } });
22
30
  }
23
31
  catch (error) {
24
- self.postMessage({ id, error: String(error) });
32
+ // FIX: Handle Map-based errors from Rust correctly
33
+ const errorMsg = (error instanceof Map)
34
+ ? JSON.stringify(Object.fromEntries(error))
35
+ : String(error);
36
+ self.postMessage({ id, error: errorMsg });
25
37
  }
26
38
  return;
27
39
  }
28
- // --- Standard Request/Response Phase ---
40
+ // --- 2. Standard Request/Response Phase ---
29
41
  try {
30
- const result = db.handle_message({ action, ...payload });
42
+ if (!initPromise)
43
+ throw new Error("Worker not initialized");
44
+ const currentDb = await initPromise;
45
+ const result = currentDb.handle_message({ action, ...payload });
31
46
  self.postMessage({ id, result });
32
47
  }
33
48
  catch (error) {
34
- self.postMessage({ id, error: String(error) });
49
+ // FIX: Handle Map-based errors here too
50
+ const errorMsg = (error instanceof Map)
51
+ ? JSON.stringify(Object.fromEntries(error))
52
+ : String(error);
53
+ self.postMessage({ id, error: errorMsg });
35
54
  }
36
55
  };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@moltendb-web/core",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-rc.1",
4
4
  "description": "MoltenDB WASM runtime — the database engine, Web Worker, and main-thread client in one package.",
5
5
  "type": "module",
6
6
  "author": "Maximilian Both <maximilian.both27@outlook.com>",
7
- "license": "LICENSE.md",
7
+ "license": "MIT",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/maximilian27/moltendb-web/tree/master/packages/core"
@@ -19,7 +19,10 @@
19
19
  "import": "./dist/index.js",
20
20
  "types": "./dist/index.d.ts"
21
21
  },
22
- "./worker": "./dist/moltendb-worker.js",
22
+ "./worker": {
23
+ "import": "./dist/moltendb-worker.js",
24
+ "types": "./dist/moltendb-worker.d.ts"
25
+ },
23
26
  "./wasm": "./dist/moltendb.js",
24
27
  "./wasm-bg": "./dist/moltendb_bg.wasm"
25
28
  },
@@ -28,10 +31,20 @@
28
31
  "scripts": {
29
32
  "build": "tsc",
30
33
  "dev": "tsc --watch",
31
- "typecheck": "tsc --noEmit"
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest",
37
+ "test:coverage": "vitest run --coverage",
38
+ "prepublishOnly": "npm run build",
39
+ "test:e2e": "playwright test"
32
40
  },
33
41
  "devDependencies": {
34
- "typescript": "^6.0.1-rc"
42
+ "@playwright/test": "^1.58.2",
43
+ "@vitest/coverage-v8": "^4.1.1",
44
+ "happy-dom": "^20.8.7",
45
+ "typescript": "^6.0.2",
46
+ "vite": "^8.0.2",
47
+ "vitest": "^4.1.1"
35
48
  },
36
49
  "keywords": [
37
50
  "database",
@@ -43,4 +56,4 @@
43
56
  "indexeddb",
44
57
  "embedded"
45
58
  ]
46
- }
59
+ }