@moltendb-web/core 0.1.0-alpha.9 → 0.1.0-beta.2
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 +27 -7
- package/dist/index.d.ts +50 -51
- package/dist/index.js +178 -135
- package/dist/moltendb-worker.d.ts +1 -0
- package/dist/moltendb-worker.js +29 -23
- package/dist/moltendb.d.ts +10 -3
- package/dist/moltendb.js +16 -7
- package/dist/moltendb_bg.wasm +0 -0
- package/dist/moltendb_bg.wasm.d.ts +4 -3
- package/package.json +8 -3
- package/dist/assets/logo.png +0 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# MoltenDB Web
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
|
-
<img src="
|
|
4
|
+
<img src="../../assets/logo.png" alt="MoltenDB Logo" width="64"/>
|
|
5
5
|
|
|
6
6
|
### 🌋 The Embedded Database for the Modern Web
|
|
7
7
|
**High-performance Rust engine compiled to WASM. Persistent storage via OPFS.**
|
|
@@ -50,7 +50,30 @@ npm install @moltendb-web/core
|
|
|
50
50
|
# Install the chainable query builder
|
|
51
51
|
npm install @moltendb-web/query
|
|
52
52
|
```
|
|
53
|
+
📦 **Bundler Setup**
|
|
53
54
|
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
**For Vite:**
|
|
58
|
+
Exclude the core package from pre-bundling in your vite.config.js:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
// vite.config.js`
|
|
62
|
+
export default defineConfig({
|
|
63
|
+
optimizeDeps: { exclude: ['@moltendb-web/core'] }
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**For Webpack 5 (Next.js, Create React App):**
|
|
68
|
+
Ensure Webpack treats the `.wasm` binary as a static resource in `webpack.config.js`:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
module.exports = {
|
|
72
|
+
module: {
|
|
73
|
+
rules: [{ test: /\.wasm$/, type: 'asset/resource' }]
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
```
|
|
54
77
|
---
|
|
55
78
|
|
|
56
79
|
# Quick Start
|
|
@@ -62,12 +85,11 @@ TypeScript
|
|
|
62
85
|
import { MoltenDB } from '@moltendb-web/core';
|
|
63
86
|
import { MoltenDBClient, WorkerTransport } from '@moltendb-web/query';
|
|
64
87
|
|
|
65
|
-
const
|
|
66
|
-
const db = new MoltenDB('moltendb_demo', { syncEnabled: false, workerUrl });
|
|
88
|
+
const db = new MoltenDB('moltendb_demo');
|
|
67
89
|
await db.init();
|
|
68
90
|
|
|
69
91
|
// Connect the query builder to the WASM worker
|
|
70
|
-
const client = new MoltenDBClient(
|
|
92
|
+
const client = new MoltenDBClient(db);
|
|
71
93
|
|
|
72
94
|
// 2. Insert and Query
|
|
73
95
|
|
|
@@ -243,7 +265,7 @@ This monorepo contains the following packages:
|
|
|
243
265
|
|
|
244
266
|
## Roadmap
|
|
245
267
|
|
|
246
|
-
- [ ]
|
|
268
|
+
- [ ] ~~**Multi-Tab Sync:** Leader election for multiple tabs to share a single OPFS instance.~~ ✅
|
|
247
269
|
- [ ] **Delta Sync:** Automatic two-way sync with the MoltenDB Rust server.
|
|
248
270
|
- [ ] **Analytics functionality:** Run analytics queries straight in the browser.
|
|
249
271
|
|
|
@@ -256,6 +278,4 @@ MoltenDB is licensed under the **Business Source License 1.1**.
|
|
|
256
278
|
|
|
257
279
|
For commercial licensing or questions: [maximilian.both27@outlook.com](mailto:maximilian.both27@outlook.com)
|
|
258
280
|
|
|
259
|
-
---
|
|
260
281
|
|
|
261
|
-
Created with 🌋 by Maximilian Both. Coming to the world on the Equinox.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,56 +1,55 @@
|
|
|
1
1
|
export interface MoltenDBOptions {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
/** URL or path to moltendb-worker.js. */
|
|
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
12
|
}
|
|
13
|
-
|
|
14
13
|
export type SyncCallback = (update: {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
event: 'change' | 'delete' | 'drop';
|
|
15
|
+
collection: string;
|
|
16
|
+
key: string;
|
|
17
|
+
new_v: number | null;
|
|
19
18
|
}) => void;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
19
|
+
export declare class MoltenDB {
|
|
20
|
+
readonly dbName: string;
|
|
21
|
+
readonly workerUrl?: string | URL;
|
|
22
|
+
worker: Worker | null;
|
|
23
|
+
private messageId;
|
|
24
|
+
private pendingRequests;
|
|
25
|
+
isLeader: boolean;
|
|
26
|
+
private bc;
|
|
27
|
+
private syncEnabled;
|
|
28
|
+
private serverUrl;
|
|
29
|
+
private syncIntervalMs;
|
|
30
|
+
private authToken?;
|
|
31
|
+
private ws;
|
|
32
|
+
private syncCallbacks;
|
|
33
|
+
private syncQueue;
|
|
34
|
+
private syncTimer;
|
|
35
|
+
/** ⚡ Hook to listen to native real-time DB mutations (works on all tabs) */
|
|
36
|
+
onEvent?: (event: any) => void;
|
|
37
|
+
constructor(dbName?: string, options?: MoltenDBOptions);
|
|
38
|
+
init(): Promise<void>;
|
|
39
|
+
private startAsLeader;
|
|
40
|
+
private startAsFollower;
|
|
41
|
+
sendMessage(action: string, payload?: Record<string, unknown>): Promise<any>;
|
|
42
|
+
set(collection: string, key: string, value: Record<string, unknown>, options?: {
|
|
43
|
+
skipSync?: boolean;
|
|
44
|
+
}): Promise<void>;
|
|
45
|
+
get(collection: string, key: string): Promise<unknown>;
|
|
46
|
+
getAll(collection: string): Promise<unknown>;
|
|
47
|
+
delete(collection: string, key: string, options?: {
|
|
48
|
+
skipSync?: boolean;
|
|
49
|
+
}): Promise<void>;
|
|
50
|
+
compact(): Promise<unknown>;
|
|
51
|
+
private startSync;
|
|
52
|
+
onSyncEvent(callback: SyncCallback): void;
|
|
53
|
+
disconnect(): void;
|
|
54
|
+
terminate(): void;
|
|
56
55
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,176 +1,219 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MoltenDB main-thread client.
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* import { MoltenDB } from 'moltendb-wasm';
|
|
6
|
-
*
|
|
7
|
-
* const db = new MoltenDB('my-db', {
|
|
8
|
-
* // Required: URL or path to the moltendb-worker.js file.
|
|
9
|
-
* // With a bundler: new URL('moltendb-wasm/worker', import.meta.url)
|
|
10
|
-
* // Plain script: '/node_modules/moltendb-wasm/dist/moltendb-worker.js'
|
|
11
|
-
* workerUrl: new URL('moltendb-wasm/worker', import.meta.url),
|
|
12
|
-
* });
|
|
13
|
-
* await db.init();
|
|
14
|
-
*/
|
|
15
1
|
export class MoltenDB {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
2
|
+
dbName;
|
|
3
|
+
workerUrl;
|
|
4
|
+
worker = null;
|
|
5
|
+
messageId = 0;
|
|
6
|
+
pendingRequests = new Map();
|
|
7
|
+
// Multi-tab Sync State
|
|
8
|
+
isLeader = false;
|
|
9
|
+
bc;
|
|
10
|
+
// Server Sync State
|
|
11
|
+
syncEnabled;
|
|
12
|
+
serverUrl;
|
|
13
|
+
syncIntervalMs;
|
|
14
|
+
authToken;
|
|
15
|
+
ws = null;
|
|
16
|
+
syncCallbacks = [];
|
|
17
|
+
syncQueue = [];
|
|
18
|
+
syncTimer = null;
|
|
19
|
+
/** ⚡ Hook to listen to native real-time DB mutations (works on all tabs) */
|
|
20
|
+
onEvent;
|
|
26
21
|
constructor(dbName = 'moltendb', options = {}) {
|
|
27
|
-
this.dbName
|
|
28
|
-
this.workerUrl
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
31
|
-
this.pendingRequests = new Map();
|
|
32
|
-
|
|
33
|
-
this.syncEnabled = options.syncEnabled ?? false;
|
|
34
|
-
this.serverUrl = options.serverUrl ?? 'wss://localhost:3000/ws';
|
|
22
|
+
this.dbName = dbName;
|
|
23
|
+
this.workerUrl = options.workerUrl;
|
|
24
|
+
this.syncEnabled = options.syncEnabled ?? false;
|
|
25
|
+
this.serverUrl = options.serverUrl ?? 'wss://localhost:3000/ws';
|
|
35
26
|
this.syncIntervalMs = options.syncIntervalMs ?? 5000;
|
|
36
|
-
this.authToken
|
|
37
|
-
|
|
38
|
-
this.ws = null;
|
|
39
|
-
this.syncCallbacks = new Set();
|
|
40
|
-
this.syncQueue = [];
|
|
41
|
-
this.syncTimer = null;
|
|
27
|
+
this.authToken = options.authToken;
|
|
42
28
|
}
|
|
43
|
-
|
|
44
|
-
/** Initialise the Web Worker and open the OPFS database. */
|
|
45
29
|
async init() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
30
|
+
this.bc = new BroadcastChannel(`moltendb_channel_${this.dbName}`);
|
|
31
|
+
return new Promise((resolveInit) => {
|
|
32
|
+
// 1. Try to grab the lock immediately (Leader Election)
|
|
33
|
+
navigator.locks.request(`moltendb_lock_${this.dbName}`, { ifAvailable: true }, async (lock) => {
|
|
34
|
+
if (lock) {
|
|
35
|
+
// We got the lock! We are the active DB host.
|
|
36
|
+
await this.startAsLeader();
|
|
37
|
+
resolveInit();
|
|
38
|
+
// Return a promise that never resolves to hold the lock until the tab closes
|
|
39
|
+
return new Promise(() => { });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// Lock is taken. We are a proxy follower.
|
|
43
|
+
this.startAsFollower();
|
|
44
|
+
resolveInit();
|
|
45
|
+
// 2. Queue up in the background. If the Leader tab closes, this lock resolves!
|
|
46
|
+
navigator.locks.request(`moltendb_lock_${this.dbName}`, async (fallbackLock) => {
|
|
47
|
+
console.log(`[MoltenDB] Previous leader disconnected. Promoting this tab to Leader.`);
|
|
48
|
+
await this.startAsLeader();
|
|
49
|
+
return new Promise(() => { }); // Hold lock
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
67
53
|
});
|
|
68
54
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
55
|
+
async startAsLeader() {
|
|
56
|
+
this.isLeader = true;
|
|
57
|
+
if (this.worker)
|
|
58
|
+
this.worker.terminate(); // Clean slate if promoted
|
|
59
|
+
// We must inline `new URL` directly inside `new Worker` so bundlers catch it!
|
|
60
|
+
if (this.workerUrl) {
|
|
61
|
+
this.worker = new Worker(this.workerUrl, { type: 'module', name: `moltendb-${this.dbName}-leader` });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
this.worker = new Worker(new URL('./moltendb-worker.js', import.meta.url), { type: 'module', name: `moltendb-${this.dbName}-leader` });
|
|
65
|
+
}
|
|
66
|
+
// Handle messages strictly from our local Worker
|
|
67
|
+
this.worker.onmessage = (e) => {
|
|
68
|
+
const data = e.data;
|
|
69
|
+
if (data.type === 'event') {
|
|
70
|
+
// Trigger local UI hook
|
|
71
|
+
if (this.onEvent)
|
|
72
|
+
this.onEvent(data);
|
|
73
|
+
// Broadcast the native event to all Follower tabs
|
|
74
|
+
this.bc.postMessage(data);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Resolve pending local promises
|
|
78
|
+
const req = this.pendingRequests.get(data.id);
|
|
79
|
+
if (req) {
|
|
80
|
+
if (data.error)
|
|
81
|
+
req.reject(new Error(data.error));
|
|
82
|
+
else
|
|
83
|
+
req.resolve(data.result);
|
|
84
|
+
this.pendingRequests.delete(data.id);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
// Initialize the WASM Engine
|
|
88
|
+
await new Promise((resolve, reject) => {
|
|
77
89
|
const id = this.messageId++;
|
|
78
90
|
this.pendingRequests.set(id, { resolve, reject });
|
|
79
|
-
this.worker.postMessage({ id, action,
|
|
91
|
+
this.worker.postMessage({ id, action: 'init', dbName: this.dbName });
|
|
80
92
|
});
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
this.ws.onopen = () => {
|
|
90
|
-
if (this.authToken) {
|
|
91
|
-
this.ws.send(JSON.stringify({ action: 'AUTH', token: this.authToken }));
|
|
93
|
+
// Listen to the BroadcastChannel for queries coming from Follower tabs
|
|
94
|
+
this.bc.onmessage = async (e) => {
|
|
95
|
+
const msg = e.data;
|
|
96
|
+
if (msg.type === 'query' && msg.action) {
|
|
97
|
+
try {
|
|
98
|
+
const result = await this.sendMessage(msg.action, msg.payload);
|
|
99
|
+
this.bc.postMessage({ type: 'response', id: msg.id, result });
|
|
92
100
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
this.ws.onclose = () => {
|
|
104
|
-
if (this.syncTimer) clearInterval(this.syncTimer);
|
|
105
|
-
setTimeout(() => this.connectSync(), 3000);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
this.ws.onerror = (err) => reject(err);
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
handleServerUpdate(update) {
|
|
113
|
-
if (update.event === 'change' && update.collection && update.key) {
|
|
114
|
-
// Re-fetch the updated document from the server and apply locally.
|
|
115
|
-
// (The WS push only carries the key + new _v, not the full document.)
|
|
101
|
+
catch (err) {
|
|
102
|
+
this.bc.postMessage({ type: 'response', id: msg.id, error: err.message });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
// If backend sync is enabled, only the Leader manages the WebSocket
|
|
107
|
+
if (this.syncEnabled) {
|
|
108
|
+
this.startSync();
|
|
116
109
|
}
|
|
117
|
-
this.syncCallbacks.forEach(cb => cb(update));
|
|
118
110
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.
|
|
123
|
-
|
|
111
|
+
startAsFollower() {
|
|
112
|
+
this.isLeader = false;
|
|
113
|
+
// We don't need a worker, we rely on the Leader.
|
|
114
|
+
if (this.worker) {
|
|
115
|
+
this.worker.terminate();
|
|
116
|
+
this.worker = null;
|
|
117
|
+
}
|
|
118
|
+
// Listen to the BroadcastChannel for answers from the Leader
|
|
119
|
+
this.bc.onmessage = (e) => {
|
|
120
|
+
const data = e.data;
|
|
121
|
+
if (data.type === 'event') {
|
|
122
|
+
// Trigger local UI hook as if it happened in this tab
|
|
123
|
+
if (this.onEvent)
|
|
124
|
+
this.onEvent(data);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (data.type === 'response') {
|
|
128
|
+
// Resolve our proxied promises
|
|
129
|
+
const req = this.pendingRequests.get(data.id);
|
|
130
|
+
if (req) {
|
|
131
|
+
if (data.error)
|
|
132
|
+
req.reject(new Error(data.error));
|
|
133
|
+
else
|
|
134
|
+
req.resolve(data.result);
|
|
135
|
+
this.pendingRequests.delete(data.id);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
124
139
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
async sendMessage(action, payload) {
|
|
141
|
+
const id = this.messageId++;
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
this.pendingRequests.set(id, { resolve, reject });
|
|
144
|
+
if (this.isLeader && this.worker) {
|
|
145
|
+
// Direct execution on the local Worker
|
|
146
|
+
this.worker.postMessage({ id, action, ...payload });
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Proxy the request to the Leader tab
|
|
150
|
+
this.bc.postMessage({ type: 'query', id, action, payload });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
130
153
|
}
|
|
131
|
-
|
|
132
154
|
// ── Convenience CRUD helpers ──────────────────────────────────────────────
|
|
133
|
-
|
|
134
|
-
/** Insert / upsert one document. */
|
|
135
155
|
async set(collection, key, value, options = {}) {
|
|
136
156
|
await this.sendMessage('set', { collection, data: { [key]: value } });
|
|
137
|
-
if (this.syncEnabled && !options.skipSync) {
|
|
157
|
+
if (this.syncEnabled && !options.skipSync && this.isLeader) {
|
|
138
158
|
this.syncQueue.push({ action: 'set', collection, data: { [key]: value } });
|
|
139
159
|
}
|
|
140
160
|
}
|
|
141
|
-
|
|
142
|
-
/** Fetch a single document by key. */
|
|
143
161
|
get(collection, key) {
|
|
144
162
|
return this.sendMessage('get', { collection, keys: key });
|
|
145
163
|
}
|
|
146
|
-
|
|
147
|
-
/** Fetch all documents in a collection. */
|
|
148
164
|
getAll(collection) {
|
|
149
165
|
return this.sendMessage('get', { collection });
|
|
150
166
|
}
|
|
151
|
-
|
|
152
|
-
/** Delete a document by key. */
|
|
153
167
|
async delete(collection, key, options = {}) {
|
|
154
168
|
await this.sendMessage('delete', { collection, keys: key });
|
|
155
|
-
if (this.syncEnabled && !options.skipSync) {
|
|
169
|
+
if (this.syncEnabled && !options.skipSync && this.isLeader) {
|
|
156
170
|
this.syncQueue.push({ action: 'delete', collection, keys: key });
|
|
157
171
|
}
|
|
158
172
|
}
|
|
159
|
-
|
|
160
|
-
/** Compact the OPFS log file. */
|
|
161
173
|
compact() {
|
|
162
174
|
return this.sendMessage('compact');
|
|
163
175
|
}
|
|
164
|
-
|
|
165
|
-
|
|
176
|
+
// ── Server Sync Implementation (Leader Only) ──────────────────────────────
|
|
177
|
+
startSync() {
|
|
178
|
+
this.ws = new WebSocket(this.serverUrl);
|
|
179
|
+
this.ws.onopen = () => {
|
|
180
|
+
if (this.authToken) {
|
|
181
|
+
this.ws?.send(JSON.stringify({ type: 'auth', token: this.authToken }));
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
this.ws.onmessage = (e) => {
|
|
185
|
+
try {
|
|
186
|
+
const msg = JSON.parse(e.data);
|
|
187
|
+
if (msg.event) {
|
|
188
|
+
for (const cb of this.syncCallbacks)
|
|
189
|
+
cb(msg);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) { }
|
|
193
|
+
};
|
|
194
|
+
this.syncTimer = setInterval(async () => {
|
|
195
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
196
|
+
return;
|
|
197
|
+
if (this.syncQueue.length === 0)
|
|
198
|
+
return;
|
|
199
|
+
const batch = this.syncQueue.splice(0, this.syncQueue.length);
|
|
200
|
+
this.ws.send(JSON.stringify({ type: 'batch', operations: batch }));
|
|
201
|
+
}, this.syncIntervalMs);
|
|
202
|
+
}
|
|
203
|
+
onSyncEvent(callback) {
|
|
204
|
+
this.syncCallbacks.push(callback);
|
|
205
|
+
}
|
|
166
206
|
disconnect() {
|
|
167
|
-
if (this.syncTimer)
|
|
168
|
-
|
|
207
|
+
if (this.syncTimer)
|
|
208
|
+
clearInterval(this.syncTimer);
|
|
209
|
+
if (this.ws)
|
|
210
|
+
this.ws.close();
|
|
211
|
+
if (this.bc)
|
|
212
|
+
this.bc.close();
|
|
169
213
|
}
|
|
170
|
-
|
|
171
|
-
/** Terminate the Web Worker (and disconnect sync). */
|
|
172
214
|
terminate() {
|
|
173
215
|
this.disconnect();
|
|
174
|
-
if (this.worker)
|
|
216
|
+
if (this.worker)
|
|
217
|
+
this.worker.terminate();
|
|
175
218
|
}
|
|
176
219
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/moltendb-worker.js
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
|
-
// MoltenDB Web Worker — auto-generated by moltendb-wasm build script
|
|
2
|
-
// Do not edit directly; edit moltendb-wasm/scripts/build.mjs instead.
|
|
3
1
|
import init, { WorkerDb } from './moltendb.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
if (!db && action === 'init') {
|
|
2
|
+
let db;
|
|
3
|
+
self.onmessage = async (e) => {
|
|
4
|
+
const { id, action, ...payload } = e.data;
|
|
5
|
+
// --- Initialization Phase ---
|
|
6
|
+
if (action === 'init') {
|
|
7
|
+
try {
|
|
12
8
|
await init();
|
|
13
|
-
db = await new WorkerDb(
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
});
|
|
21
|
+
self.postMessage({ id, result: { status: 'ok' } });
|
|
16
22
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
throw new Error('Database not initialized. Send an "init" message first.');
|
|
23
|
+
catch (error) {
|
|
24
|
+
self.postMessage({ id, error: String(error) });
|
|
20
25
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// --- Standard Request/Response Phase ---
|
|
29
|
+
try {
|
|
30
|
+
const result = db.handle_message({ action, ...payload });
|
|
25
31
|
self.postMessage({ id, result });
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
self.postMessage({ id, error: error
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
self.postMessage({ id, error: String(error) });
|
|
29
35
|
}
|
|
30
36
|
};
|
package/dist/moltendb.d.ts
CHANGED
|
@@ -76,6 +76,12 @@ export class WorkerDb {
|
|
|
76
76
|
* Each unique name is a separate database file in the browser's OPFS storage.
|
|
77
77
|
*/
|
|
78
78
|
constructor(db_name: string);
|
|
79
|
+
/**
|
|
80
|
+
* Subscribe to real-time database changes.
|
|
81
|
+
* The provided JavaScript function will be called with a JSON string
|
|
82
|
+
* representing the mutation event.
|
|
83
|
+
*/
|
|
84
|
+
subscribe(callback: Function): void;
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
|
@@ -86,9 +92,10 @@ export interface InitOutput {
|
|
|
86
92
|
readonly workerdb_analytics: (a: number, b: number, c: number, d: number) => void;
|
|
87
93
|
readonly workerdb_handle_message: (a: number, b: number, c: number) => void;
|
|
88
94
|
readonly workerdb_new: (a: number, b: number) => number;
|
|
89
|
-
readonly
|
|
90
|
-
readonly
|
|
91
|
-
readonly
|
|
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;
|
|
92
99
|
readonly __wbindgen_export: (a: number, b: number) => number;
|
|
93
100
|
readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
94
101
|
readonly __wbindgen_export3: (a: number) => void;
|
package/dist/moltendb.js
CHANGED
|
@@ -131,6 +131,15 @@ export class WorkerDb {
|
|
|
131
131
|
const ret = wasm.workerdb_new(ptr0, len0);
|
|
132
132
|
return takeObject(ret);
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Subscribe to real-time database changes.
|
|
136
|
+
* The provided JavaScript function will be called with a JSON string
|
|
137
|
+
* representing the mutation event.
|
|
138
|
+
* @param {Function} callback
|
|
139
|
+
*/
|
|
140
|
+
subscribe(callback) {
|
|
141
|
+
wasm.workerdb_subscribe(this.__wbg_ptr, addHeapObject(callback));
|
|
142
|
+
}
|
|
134
143
|
}
|
|
135
144
|
if (Symbol.dispose) WorkerDb.prototype[Symbol.dispose] = WorkerDb.prototype.free;
|
|
136
145
|
|
|
@@ -377,7 +386,7 @@ function __wbg_get_imports() {
|
|
|
377
386
|
const a = state0.a;
|
|
378
387
|
state0.a = 0;
|
|
379
388
|
try {
|
|
380
|
-
return
|
|
389
|
+
return __wasm_bindgen_func_elem_3716(a, state0.b, arg0, arg1);
|
|
381
390
|
} finally {
|
|
382
391
|
state0.a = a;
|
|
383
392
|
}
|
|
@@ -497,8 +506,8 @@ function __wbg_get_imports() {
|
|
|
497
506
|
return ret;
|
|
498
507
|
}, arguments); },
|
|
499
508
|
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
|
500
|
-
// Cast intrinsic for `Closure(Closure { dtor_idx:
|
|
501
|
-
const ret = makeMutClosure(arg0, arg1, wasm.
|
|
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);
|
|
502
511
|
return addHeapObject(ret);
|
|
503
512
|
},
|
|
504
513
|
__wbindgen_cast_0000000000000002: function(arg0) {
|
|
@@ -535,10 +544,10 @@ function __wbg_get_imports() {
|
|
|
535
544
|
};
|
|
536
545
|
}
|
|
537
546
|
|
|
538
|
-
function
|
|
547
|
+
function __wasm_bindgen_func_elem_3703(arg0, arg1, arg2) {
|
|
539
548
|
try {
|
|
540
549
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
541
|
-
wasm.
|
|
550
|
+
wasm.__wasm_bindgen_func_elem_3703(retptr, arg0, arg1, addHeapObject(arg2));
|
|
542
551
|
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
543
552
|
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
544
553
|
if (r1) {
|
|
@@ -549,8 +558,8 @@ function __wasm_bindgen_func_elem_3677(arg0, arg1, arg2) {
|
|
|
549
558
|
}
|
|
550
559
|
}
|
|
551
560
|
|
|
552
|
-
function
|
|
553
|
-
wasm.
|
|
561
|
+
function __wasm_bindgen_func_elem_3716(arg0, arg1, arg2, arg3) {
|
|
562
|
+
wasm.__wasm_bindgen_func_elem_3716(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
|
|
554
563
|
}
|
|
555
564
|
|
|
556
565
|
const WorkerDbFinalization = (typeof FinalizationRegistry === 'undefined')
|
package/dist/moltendb_bg.wasm
CHANGED
|
Binary file
|
|
@@ -5,9 +5,10 @@ export const __wbg_workerdb_free: (a: number, b: number) => void;
|
|
|
5
5
|
export const workerdb_analytics: (a: number, b: number, c: number, d: number) => void;
|
|
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
|
-
export const
|
|
9
|
-
export const
|
|
10
|
-
export const
|
|
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;
|
|
11
12
|
export const __wbindgen_export: (a: number, b: number) => number;
|
|
12
13
|
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
13
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-
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
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>",
|
|
@@ -25,8 +25,13 @@
|
|
|
25
25
|
},
|
|
26
26
|
"main": "./dist/index.js",
|
|
27
27
|
"types": "./dist/index.d.ts",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"dev": "tsc --watch",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
},
|
|
28
33
|
"devDependencies": {
|
|
29
|
-
"typescript": "^6.0.
|
|
34
|
+
"typescript": "^6.0.2"
|
|
30
35
|
},
|
|
31
36
|
"keywords": [
|
|
32
37
|
"database",
|
|
@@ -38,4 +43,4 @@
|
|
|
38
43
|
"indexeddb",
|
|
39
44
|
"embedded"
|
|
40
45
|
]
|
|
41
|
-
}
|
|
46
|
+
}
|
package/dist/assets/logo.png
DELETED
|
Binary file
|