@moltendb-web/core 0.1.0-rc.1 → 1.0.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/README.md CHANGED
@@ -37,7 +37,7 @@ Prefer to run it in your own environment? You can **[clone the demo repository](
37
37
  - **Worker-Threaded:** The database runs entirely inside a Web Worker—zero impact on your UI thread.
38
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
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.
40
+ - **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.
41
41
  - **GraphQL-style Selection:** Request only the fields you need (even deeply nested ones) to save memory and CPU.
42
42
  - **Auto-Indexing:** The engine monitors your queries and automatically creates indexes for frequently filtered fields.
43
43
  - **Conflict Resolution:** Incoming writes with `_v ≤ stored _v` are silently skipped.
@@ -270,28 +270,32 @@ Tab 1 (Leader) ──owns──▶ Web Worker ──▶ WASM Engine ──▶ OP
270
270
 
271
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
272
 
273
- Assign a callback to `onEvent` after calling `init()`:
273
+ You can attach multiple independent listeners using the subscribe() method, making it trivial to keep different UI components (like React hooks or Angular signals) in sync without memory leaks:
274
274
 
275
275
  ```ts
276
276
  const db = new MoltenDB('my-app');
277
277
  await db.init();
278
278
 
279
- db.onEvent = (event) => {
279
+ // Attach a listener (Returns an unsubscribe function)
280
+ const unsubscribe = db.subscribe((event) => {
280
281
  console.log(event.event); // 'change' | 'delete' | 'drop'
281
282
  console.log(event.collection); // e.g. 'laptops'
282
283
  console.log(event.key); // e.g. 'lp1'
283
284
  console.log(event.new_v); // new version number, or null on delete
284
- };
285
+ });
286
+
287
+ // Later, when the UI component unmounts:
288
+ unsubscribe();
285
289
  ```
286
290
 
287
291
  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
292
 
289
293
  ```ts
290
- db.onEvent = ({ event, collection, key }) => {
294
+ db.subscribe(({ event, collection, key }) => {
291
295
  if (collection === 'laptops') {
292
296
  refreshLaptopList(); // re-query and re-render
293
297
  }
294
- };
298
+ });
295
299
  ```
296
300
 
297
301
  The `DBEvent` type is exported from the package for full TypeScript support:
@@ -302,8 +306,7 @@ import { MoltenDB, DBEvent } from '@moltendb-web/core';
302
306
  const db = new MoltenDB('my-app');
303
307
  await db.init();
304
308
 
305
- db.onEvent = (e: DBEvent) => { /* fully typed */ };
306
- ```
309
+ db.subscribe((e: DBEvent) => { /* fully typed */ });```
307
310
 
308
311
  ---
309
312
 
@@ -337,7 +340,7 @@ npm run test:coverage # with coverage report
337
340
  | CRUD — follower | 3 | BroadcastChannel proxy path for all mutations |
338
341
  | Worker error handling | 3 | Transient errors, unknown actions, request isolation |
339
342
  | Leader promotion | 2 | Follower takes over when leader tab closes |
340
- | `onEvent` hook | 2 | Real-time event delivery to leader and followers |
343
+ | `Pub/Sub (subscribe)` | 2 | Multi-subscriber event delivery across tabs |
341
344
  | Follower timeout | 1 | Pending requests reject after 10 s if leader disappears |
342
345
  | `terminate` / `disconnect` | 3 | Worker cleanup, timer teardown |
343
346
  | Stress — rapid writes | 3 | 100 sequential, 50 concurrent, interleaved set/delete |
package/dist/index.d.ts CHANGED
@@ -1,23 +1,7 @@
1
1
  export interface MoltenDBOptions {
2
2
  /** URL or path to moltendb-worker.js. */
3
3
  workerUrl?: string | URL;
4
- /** Enable WebSocket sync with a MoltenDB server. Default: false. */
5
- syncEnabled?: boolean;
6
- /** WebSocket server URL. Default: 'wss://localhost:1538/ws'. */
7
- serverUrl?: string;
8
- /** Sync batch flush interval in ms. Default: 5000. */
9
- syncIntervalMs?: number;
10
- /** JWT token for WebSocket authentication. */
11
- authToken?: string;
12
- /** Called whenever a DB mutation event is broadcast (all tabs). */
13
- onEvent?: (event: DBEvent) => void;
14
4
  }
15
- export type SyncCallback = (update: {
16
- event: 'change' | 'delete' | 'drop';
17
- collection: string;
18
- key: string;
19
- new_v: number | null;
20
- }) => void;
21
5
  export interface DBEvent {
22
6
  type: 'event';
23
7
  event: 'change' | 'delete' | 'drop';
@@ -32,33 +16,28 @@ export declare class MoltenDB {
32
16
  private pendingRequests;
33
17
  isLeader: boolean;
34
18
  private bc;
35
- private syncEnabled;
36
- private serverUrl;
37
- private syncIntervalMs;
38
- private authToken?;
39
- private ws;
40
- private syncCallbacks;
41
- private syncQueue;
42
- private syncTimer;
43
- /** ⚡ Hook to listen to native real-time DB mutations (works on all tabs) */
19
+ /** Legacy global hook. Use `subscribe()` for multi-component listeners. */
44
20
  onEvent?: (event: DBEvent) => void;
21
+ private eventListeners;
45
22
  constructor(dbName?: string, options?: MoltenDBOptions);
23
+ /**
24
+ * ⚡ Subscribe to real-time DB mutations.
25
+ * @returns An unsubscribe function to prevent memory leaks in UI frameworks.
26
+ */
27
+ subscribe(listener: (event: DBEvent) => void): () => void;
28
+ /** Manually remove a specific listener */
29
+ unsubscribe(listener: (event: DBEvent) => void): void;
30
+ private dispatchEvent;
46
31
  private initialized;
47
32
  init(): Promise<void>;
48
33
  private startAsLeader;
49
34
  private startAsFollower;
50
35
  sendMessage(action: string, payload?: Record<string, unknown>): Promise<any>;
51
- set(collection: string, key: string, value: any, options?: {
52
- skipSync?: boolean;
53
- }): Promise<void>;
36
+ set(collection: string, key: string, value: any): Promise<void>;
54
37
  get(collection: string, key: string): Promise<unknown>;
55
38
  getAll(collection: string): Promise<unknown[]>;
56
- delete(collection: string, key: string, options?: {
57
- skipSync?: boolean;
58
- }): Promise<void>;
39
+ delete(collection: string, key: string): Promise<void>;
59
40
  compact(): Promise<unknown>;
60
- private startSync;
61
- onSyncEvent(callback: SyncCallback): void;
62
41
  disconnect(): void;
63
42
  terminate(): void;
64
43
  }
package/dist/index.js CHANGED
@@ -6,27 +6,38 @@ export class MoltenDB {
6
6
  // Multi-tab Sync State
7
7
  isLeader = false;
8
8
  bc;
9
- // Server Sync State
10
- syncEnabled;
11
- serverUrl;
12
- syncIntervalMs;
13
- authToken;
14
- ws = null;
15
- syncCallbacks = [];
16
- syncQueue = [];
17
- syncTimer = null;
18
- /** ⚡ Hook to listen to native real-time DB mutations (works on all tabs) */
9
+ /** Legacy global hook. Use `subscribe()` for multi-component listeners. */
19
10
  onEvent;
11
+ // ── Multi-Subscriber Event System ──────────────────────────────────────────
12
+ eventListeners = new Set();
20
13
  constructor(dbName = 'moltendb', options = {}) {
21
14
  this.dbName = dbName;
22
15
  this.workerUrl = options.workerUrl;
23
- this.syncEnabled = options.syncEnabled ?? false;
24
- this.serverUrl = options.serverUrl ?? 'wss://localhost:3000/ws';
25
- this.syncIntervalMs = options.syncIntervalMs ?? 5000;
26
- this.authToken = options.authToken;
27
- if (options.onEvent)
28
- this.onEvent = options.onEvent;
29
16
  }
17
+ /**
18
+ * ⚡ Subscribe to real-time DB mutations.
19
+ * @returns An unsubscribe function to prevent memory leaks in UI frameworks.
20
+ */
21
+ subscribe(listener) {
22
+ this.eventListeners.add(listener);
23
+ return () => this.eventListeners.delete(listener);
24
+ }
25
+ /** Manually remove a specific listener */
26
+ unsubscribe(listener) {
27
+ this.eventListeners.delete(listener);
28
+ }
29
+ dispatchEvent(event) {
30
+ // Fire all subscribed component handlers
31
+ for (const listener of this.eventListeners) {
32
+ try {
33
+ listener(event);
34
+ }
35
+ catch (err) {
36
+ console.error('[MoltenDB] Error in subscribed listener', err);
37
+ }
38
+ }
39
+ }
40
+ // ───────────────────────────────────────────────────────────────────────────
30
41
  initialized = false;
31
42
  async init() {
32
43
  if (this.initialized)
@@ -74,8 +85,7 @@ export class MoltenDB {
74
85
  this.worker.onmessage = (e) => {
75
86
  const data = e.data;
76
87
  if (data.type === 'event') {
77
- if (this.onEvent)
78
- this.onEvent(data);
88
+ this.dispatchEvent(data); // ⬅️ Trigger new dispatcher
79
89
  this.bc.postMessage(data);
80
90
  return;
81
91
  }
@@ -102,8 +112,6 @@ export class MoltenDB {
102
112
  }
103
113
  }
104
114
  };
105
- if (this.syncEnabled)
106
- this.startSync();
107
115
  }
108
116
  startAsFollower() {
109
117
  this.isLeader = false;
@@ -114,8 +122,7 @@ export class MoltenDB {
114
122
  this.bc.onmessage = (e) => {
115
123
  const data = e.data;
116
124
  if (data.type === 'event') {
117
- if (this.onEvent)
118
- this.onEvent(data);
125
+ this.dispatchEvent(data); // ⬅️ Trigger new dispatcher
119
126
  return;
120
127
  }
121
128
  if (data.type === 'response') {
@@ -131,7 +138,6 @@ export class MoltenDB {
131
138
  };
132
139
  }
133
140
  async sendMessage(action, payload) {
134
- // FIX: Use random UUIDs so tabs don't collide on message IDs
135
141
  const id = crypto.randomUUID();
136
142
  return new Promise((resolve, reject) => {
137
143
  if (this.isLeader && this.worker) {
@@ -153,12 +159,9 @@ export class MoltenDB {
153
159
  }
154
160
  });
155
161
  }
156
- // ── Convenience CRUD helpers (CLEANED - NO DUPLICATES) ─────────────────────
157
- async set(collection, key, value, options = {}) {
162
+ // ── Convenience CRUD helpers ───────────────────────────────────────────────
163
+ async set(collection, key, value) {
158
164
  await this.sendMessage('set', { collection, data: { [key]: value } });
159
- if (this.syncEnabled && !options.skipSync && this.isLeader) {
160
- this.syncQueue.push({ action: 'set', collection, data: { [key]: value } });
161
- }
162
165
  }
163
166
  async get(collection, key) {
164
167
  try {
@@ -189,51 +192,13 @@ export class MoltenDB {
189
192
  throw err;
190
193
  }
191
194
  }
192
- async delete(collection, key, options = {}) {
195
+ async delete(collection, key) {
193
196
  await this.sendMessage('delete', { collection, keys: key });
194
- if (this.syncEnabled && !options.skipSync && this.isLeader) {
195
- this.syncQueue.push({ action: 'delete', collection, keys: key });
196
- }
197
197
  }
198
198
  compact() {
199
199
  return this.sendMessage('compact');
200
200
  }
201
- // ── Server Sync Implementation (Leader Only) ──────────────────────────────
202
- startSync() {
203
- this.ws = new WebSocket(this.serverUrl);
204
- this.ws.onopen = () => {
205
- if (this.authToken) {
206
- this.ws?.send(JSON.stringify({ type: 'auth', token: this.authToken }));
207
- }
208
- };
209
- this.ws.onmessage = (e) => {
210
- try {
211
- const msg = JSON.parse(e.data);
212
- if (msg.event) {
213
- for (const cb of this.syncCallbacks)
214
- cb(msg);
215
- }
216
- }
217
- catch (err) {
218
- }
219
- };
220
- this.syncTimer = setInterval(async () => {
221
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
222
- return;
223
- if (this.syncQueue.length === 0)
224
- return;
225
- const batch = this.syncQueue.splice(0, this.syncQueue.length);
226
- this.ws.send(JSON.stringify({ type: 'batch', operations: batch }));
227
- }, this.syncIntervalMs);
228
- }
229
- onSyncEvent(callback) {
230
- this.syncCallbacks.push(callback);
231
- }
232
201
  disconnect() {
233
- if (this.syncTimer)
234
- clearInterval(this.syncTimer);
235
- if (this.ws)
236
- this.ws.close();
237
202
  if (this.bc)
238
203
  this.bc.close();
239
204
  }
@@ -93,9 +93,9 @@ export interface InitOutput {
93
93
  readonly workerdb_handle_message: (a: number, b: number, c: number) => void;
94
94
  readonly workerdb_new: (a: number, b: number) => number;
95
95
  readonly workerdb_subscribe: (a: number, b: number) => void;
96
- readonly __wasm_bindgen_func_elem_3624: (a: number, b: number) => void;
97
- readonly __wasm_bindgen_func_elem_3703: (a: number, b: number, c: number, d: number) => void;
98
- readonly __wasm_bindgen_func_elem_3716: (a: number, b: number, c: number, d: number) => void;
96
+ readonly __wasm_bindgen_func_elem_3613: (a: number, b: number) => void;
97
+ readonly __wasm_bindgen_func_elem_3697: (a: number, b: number, c: number, d: number) => void;
98
+ readonly __wasm_bindgen_func_elem_3702: (a: number, b: number, c: number, d: number) => void;
99
99
  readonly __wbindgen_export: (a: number, b: number) => number;
100
100
  readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
101
101
  readonly __wbindgen_export3: (a: number) => void;
package/dist/moltendb.js CHANGED
@@ -251,6 +251,9 @@ function __wbg_get_imports() {
251
251
  const ret = Object.entries(getObject(arg0));
252
252
  return addHeapObject(ret);
253
253
  },
254
+ __wbg_error_8d9a8e04cd1d3588: function(arg0) {
255
+ console.error(getObject(arg0));
256
+ },
254
257
  __wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) {
255
258
  let deferred0_0;
256
259
  let deferred0_1;
@@ -386,7 +389,7 @@ function __wbg_get_imports() {
386
389
  const a = state0.a;
387
390
  state0.a = 0;
388
391
  try {
389
- return __wasm_bindgen_func_elem_3716(a, state0.b, arg0, arg1);
392
+ return __wasm_bindgen_func_elem_3702(a, state0.b, arg0, arg1);
390
393
  } finally {
391
394
  state0.a = a;
392
395
  }
@@ -506,8 +509,8 @@ function __wbg_get_imports() {
506
509
  return ret;
507
510
  }, arguments); },
508
511
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
509
- // Cast intrinsic for `Closure(Closure { dtor_idx: 663, function: Function { arguments: [Externref], shim_idx: 674, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
510
- const ret = makeMutClosure(arg0, arg1, wasm.__wasm_bindgen_func_elem_3624, __wasm_bindgen_func_elem_3703);
512
+ // Cast intrinsic for `Closure(Closure { dtor_idx: 664, function: Function { arguments: [Externref], shim_idx: 677, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
513
+ const ret = makeMutClosure(arg0, arg1, wasm.__wasm_bindgen_func_elem_3613, __wasm_bindgen_func_elem_3697);
511
514
  return addHeapObject(ret);
512
515
  },
513
516
  __wbindgen_cast_0000000000000002: function(arg0) {
@@ -544,10 +547,10 @@ function __wbg_get_imports() {
544
547
  };
545
548
  }
546
549
 
547
- function __wasm_bindgen_func_elem_3703(arg0, arg1, arg2) {
550
+ function __wasm_bindgen_func_elem_3697(arg0, arg1, arg2) {
548
551
  try {
549
552
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
550
- wasm.__wasm_bindgen_func_elem_3703(retptr, arg0, arg1, addHeapObject(arg2));
553
+ wasm.__wasm_bindgen_func_elem_3697(retptr, arg0, arg1, addHeapObject(arg2));
551
554
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
552
555
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
553
556
  if (r1) {
@@ -558,8 +561,8 @@ function __wasm_bindgen_func_elem_3703(arg0, arg1, arg2) {
558
561
  }
559
562
  }
560
563
 
561
- function __wasm_bindgen_func_elem_3716(arg0, arg1, arg2, arg3) {
562
- wasm.__wasm_bindgen_func_elem_3716(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
564
+ function __wasm_bindgen_func_elem_3702(arg0, arg1, arg2, arg3) {
565
+ wasm.__wasm_bindgen_func_elem_3702(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
563
566
  }
564
567
 
565
568
  const WorkerDbFinalization = (typeof FinalizationRegistry === 'undefined')
Binary file
@@ -6,9 +6,9 @@ export const workerdb_analytics: (a: number, b: number, c: number, d: number) =>
6
6
  export const workerdb_handle_message: (a: number, b: number, c: number) => void;
7
7
  export const workerdb_new: (a: number, b: number) => number;
8
8
  export const workerdb_subscribe: (a: number, b: number) => void;
9
- export const __wasm_bindgen_func_elem_3624: (a: number, b: number) => void;
10
- export const __wasm_bindgen_func_elem_3703: (a: number, b: number, c: number, d: number) => void;
11
- export const __wasm_bindgen_func_elem_3716: (a: number, b: number, c: number, d: number) => void;
9
+ export const __wasm_bindgen_func_elem_3613: (a: number, b: number) => void;
10
+ export const __wasm_bindgen_func_elem_3697: (a: number, b: number, c: number, d: number) => void;
11
+ export const __wasm_bindgen_func_elem_3702: (a: number, b: number, c: number, d: number) => void;
12
12
  export const __wbindgen_export: (a: number, b: number) => number;
13
13
  export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
14
14
  export const __wbindgen_export3: (a: number) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moltendb-web/core",
3
- "version": "0.1.0-rc.1",
3
+ "version": "1.0.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>",