@moltendb-web/core 0.1.0-alpha.10
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 +35 -0
- package/README.md +261 -0
- package/dist/assets/logo.png +0 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +176 -0
- package/dist/moltendb-worker.js +30 -0
- package/dist/moltendb.d.ts +119 -0
- package/dist/moltendb.js +879 -0
- package/dist/moltendb_bg.wasm +0 -0
- package/dist/moltendb_bg.wasm.d.ts +15 -0
- package/package.json +41 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# MoltenDB Web
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<img src="./dist/assets/logo.png" alt="MoltenDB Logo" width="64"/>
|
|
5
|
+
|
|
6
|
+
### 🌋 The Embedded Database for the Modern Web
|
|
7
|
+
**High-performance Rust engine compiled to WASM. Persistent storage via OPFS.**
|
|
8
|
+
|
|
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
|
+
|
|
11
|
+
[](https://www.npmjs.com/package/@moltendb-web/core)
|
|
12
|
+
[](LICENSE.md)
|
|
13
|
+
[](https://webassembly.org/)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## What is MoltenDB Web?
|
|
20
|
+
|
|
21
|
+
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
|
+
### 🎮 Explore the Full Functionality
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
Prefer to run it in your own environment? You can **[clone the demo repository](https://github.com/maximilian27/moltendb-wasm-demo)** to inspect the source code, run the explorers locally, and experiment with your own schema.
|
|
28
|
+
|
|
29
|
+
**⚠️ 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.
|
|
30
|
+
|
|
31
|
+
### Core Features
|
|
32
|
+
- **Pure Rust Engine:** The same query logic used in our server binary, compiled to WebAssembly.
|
|
33
|
+
- **OPFS Persistence:** Data persists across page reloads in a dedicated, high-speed sandbox.
|
|
34
|
+
- **Worker-Threaded:** The database runs entirely inside a Web Worker—zero impact on your UI thread.
|
|
35
|
+
- **GraphQL-style Selection:** Request only the fields you need (even deeply nested ones) to save memory and CPU.
|
|
36
|
+
- **Auto-Indexing:** The engine monitors your queries and automatically creates indexes for frequently filtered fields.
|
|
37
|
+
- **Conflict Resolution:** Incoming writes with `_v ≤ stored _v` are silently skipped.
|
|
38
|
+
- **Inline reference embedding (`extends`):** Embed data from another collection at insert time.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
MoltenDB is split into two packages: the core engine and the type-safe, chainable query builder.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Install the core engine and WASM artifacts
|
|
48
|
+
npm install @moltendb-web/core
|
|
49
|
+
|
|
50
|
+
# Install the chainable query builder
|
|
51
|
+
npm install @moltendb-web/query
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
# Quick Start
|
|
57
|
+
1. Initialize the Client
|
|
58
|
+
|
|
59
|
+
MoltenDB handles the Web Worker and WASM instantiation for you.
|
|
60
|
+
TypeScript
|
|
61
|
+
```ts
|
|
62
|
+
import { MoltenDB } from '@moltendb-web/core';
|
|
63
|
+
import { MoltenDBClient, WorkerTransport } from '@moltendb-web/query';
|
|
64
|
+
|
|
65
|
+
const workerUrl = new URL('@moltendb-web/core/worker', import.meta.url).href;
|
|
66
|
+
const db = new MoltenDB('moltendb_demo', { syncEnabled: false, workerUrl });
|
|
67
|
+
await db.init();
|
|
68
|
+
|
|
69
|
+
// Connect the query builder to the WASM worker
|
|
70
|
+
const client = new MoltenDBClient(new WorkerTransport(db.worker));
|
|
71
|
+
|
|
72
|
+
// 2. Insert and Query
|
|
73
|
+
|
|
74
|
+
// Use the @moltendb-web/query builder for a type-safe experience.
|
|
75
|
+
|
|
76
|
+
// Insert data
|
|
77
|
+
await client.collection('laptops').set({
|
|
78
|
+
lp1: {
|
|
79
|
+
brand: "Apple",
|
|
80
|
+
model: "MacBook Pro",
|
|
81
|
+
price: 1999,
|
|
82
|
+
in_stock: true,
|
|
83
|
+
memory_id: 'mem1',
|
|
84
|
+
specs: {
|
|
85
|
+
cpu: {
|
|
86
|
+
cores: 8,
|
|
87
|
+
clock_speed: 3.5,
|
|
88
|
+
},
|
|
89
|
+
display: {
|
|
90
|
+
refresh_hz: 60,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
lp2: {
|
|
95
|
+
brand: "Apple",
|
|
96
|
+
model: "MacBook Air",
|
|
97
|
+
price: 900,
|
|
98
|
+
in_stock: true,
|
|
99
|
+
memory_id: 'mem2',
|
|
100
|
+
specs: {
|
|
101
|
+
cpu: {
|
|
102
|
+
cores: 4,
|
|
103
|
+
clock_speed: 3.5,
|
|
104
|
+
},
|
|
105
|
+
display: {
|
|
106
|
+
refresh_hz: 60,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}).exec();
|
|
111
|
+
|
|
112
|
+
await client.collection('memory').set({
|
|
113
|
+
mem1: {
|
|
114
|
+
capacity_gb: 16,
|
|
115
|
+
type: 'DDR4',
|
|
116
|
+
speed_mhz: 4800,
|
|
117
|
+
upgradeable: false
|
|
118
|
+
},
|
|
119
|
+
mem2: {
|
|
120
|
+
capacity_gb: 64,
|
|
121
|
+
type: 'DDR5',
|
|
122
|
+
speed_mhz: 5600,
|
|
123
|
+
upgradeable: true
|
|
124
|
+
},
|
|
125
|
+
}).exec();
|
|
126
|
+
|
|
127
|
+
// Query with field selection
|
|
128
|
+
const results = await client.collection('laptops')
|
|
129
|
+
.get()
|
|
130
|
+
.where({ brand: { $in: ["Apple", "Dell"] }, in_stock: true }) // Using $in operator
|
|
131
|
+
.fields(['model', 'price']) // Only return these specific fields
|
|
132
|
+
.sort([{ field: 'price', order: 'desc' }])
|
|
133
|
+
.exec();
|
|
134
|
+
|
|
135
|
+
console.log(results);
|
|
136
|
+
// [
|
|
137
|
+
// {
|
|
138
|
+
// "_key": "lp1",
|
|
139
|
+
// "model": "MacBook Pro",
|
|
140
|
+
// "price": 1999
|
|
141
|
+
// },
|
|
142
|
+
// {
|
|
143
|
+
// "_key": "lp2",
|
|
144
|
+
// "model": "MacBook Air",
|
|
145
|
+
// "price": 900
|
|
146
|
+
// }
|
|
147
|
+
// ]
|
|
148
|
+
|
|
149
|
+
// Powerful Query Capabilities
|
|
150
|
+
// GraphQL-style Field Selection
|
|
151
|
+
|
|
152
|
+
// Never over-fetch data again. Use dot-notation to extract deeply nested values.
|
|
153
|
+
|
|
154
|
+
await client.collection('laptops')
|
|
155
|
+
.get()
|
|
156
|
+
.fields(["brand", "specs.cpu.cores", "specs.display.refresh_hz"])
|
|
157
|
+
.exec();
|
|
158
|
+
|
|
159
|
+
// Inline Joins
|
|
160
|
+
|
|
161
|
+
// Resolve relationships between collections at query time.
|
|
162
|
+
|
|
163
|
+
await client.collection('laptops')
|
|
164
|
+
.get()
|
|
165
|
+
.joins([{
|
|
166
|
+
alias: 'ram',
|
|
167
|
+
from: 'memory',
|
|
168
|
+
on: 'memory_id',
|
|
169
|
+
fields: ['capacity_gb', 'type']
|
|
170
|
+
}])
|
|
171
|
+
.exec();
|
|
172
|
+
|
|
173
|
+
// Supported Query Operators
|
|
174
|
+
|
|
175
|
+
MoltenDB supports a variety of operators in the `where` clause:
|
|
176
|
+
|
|
177
|
+
| Operator | Aliases | Description |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| `$eq` | `$equals` | Exact equality |
|
|
180
|
+
| `$ne` | `$notEquals` | Not equal |
|
|
181
|
+
| `$gt` | `$greaterThan` | Greater than (numeric) |
|
|
182
|
+
| `$gte` | | Greater than or equal |
|
|
183
|
+
| `$lt` | `$lessThan` | Less than (numeric) |
|
|
184
|
+
| `$lte` | | Less than or equal |
|
|
185
|
+
| `$contains` | `$ct` | Substring check (string) or membership check (array) |
|
|
186
|
+
| `$in` | `$oneOf` | Field value is one of a list |
|
|
187
|
+
| `$nin` | `$notIn` | Field value is not in a list |
|
|
188
|
+
|
|
189
|
+
// Inline reference embedding (`extends`)
|
|
190
|
+
|
|
191
|
+
The `extends` key embeds data from another collection directly into the stored document at insert time — no join needed on reads.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
await client.collection('laptops')
|
|
195
|
+
.set({
|
|
196
|
+
lp7: {
|
|
197
|
+
brand: "MSI",
|
|
198
|
+
model: "Titan GT77",
|
|
199
|
+
price: 3299,
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
.extends({
|
|
203
|
+
ram: "memory.mem4",
|
|
204
|
+
screen: "display.dsp3"
|
|
205
|
+
})
|
|
206
|
+
.exec();
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**When to use `extends` vs `joins`:**
|
|
210
|
+
|
|
211
|
+
| | `extends` | `joins` |
|
|
212
|
+
|---|---|---|
|
|
213
|
+
| Resolved at | Insert time (once) | Query time (every request) |
|
|
214
|
+
| Data freshness | Snapshot — may become stale | Always live |
|
|
215
|
+
| Read cost | O(1) — data already embedded | O(1) per join per document |
|
|
216
|
+
| Use when | Data rarely changes, fast reads matter | Data changes frequently, freshness matters |
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
## Storage Architecture
|
|
220
|
+
|
|
221
|
+
### How the Log Works
|
|
222
|
+
|
|
223
|
+
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
|
+
|
|
225
|
+
- **Compaction:** When the log exceeds 5MB or 500 entries, the engine automatically "squashes" the log, removing old versions of documents to save space.
|
|
226
|
+
- **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
|
+
|
|
228
|
+
### Performance Note
|
|
229
|
+
|
|
230
|
+
Because MoltenDB uses OPFS, your browser must support `SharedArrayBuffer`. Most modern browsers support this, but your server must send the following headers:
|
|
231
|
+
|
|
232
|
+
```http
|
|
233
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
234
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Project Structure
|
|
238
|
+
|
|
239
|
+
This monorepo contains the following packages:
|
|
240
|
+
|
|
241
|
+
- **`packages/core`:** The core WASM engine, Web Worker logic, and the MoltenDB main client.
|
|
242
|
+
- **`packages/query`:** The type-safe, chainable Query Builder.
|
|
243
|
+
|
|
244
|
+
## Roadmap
|
|
245
|
+
|
|
246
|
+
- [ ] **Multi-Tab Sync:** Leader election for multiple tabs to share a single OPFS instance.
|
|
247
|
+
- [ ] **Delta Sync:** Automatic two-way sync with the MoltenDB Rust server.
|
|
248
|
+
- [ ] **Analytics functionality:** Run analytics queries straight in the browser.
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
MoltenDB is licensed under the **Business Source License 1.1**.
|
|
253
|
+
|
|
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.
|
|
256
|
+
|
|
257
|
+
For commercial licensing or questions: [maximilian.both27@outlook.com](mailto:maximilian.both27@outlook.com)
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
Created with 🌋 by Maximilian Both. Coming to the world on the Equinox.
|
|
Binary file
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface MoltenDBOptions {
|
|
2
|
+
/** URL or path to moltendb-worker.js. Defaults 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:3000/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
|
+
}
|
|
13
|
+
|
|
14
|
+
export type SyncCallback = (update: {
|
|
15
|
+
event: 'change' | 'delete' | 'drop';
|
|
16
|
+
collection: string;
|
|
17
|
+
key: string;
|
|
18
|
+
new_v: number | null;
|
|
19
|
+
}) => void;
|
|
20
|
+
|
|
21
|
+
export class MoltenDB {
|
|
22
|
+
readonly dbName: string;
|
|
23
|
+
readonly worker: Worker | null;
|
|
24
|
+
|
|
25
|
+
constructor(dbName?: string, options?: MoltenDBOptions);
|
|
26
|
+
|
|
27
|
+
/** Initialise the Web Worker and open the OPFS database. */
|
|
28
|
+
init(): Promise<void>;
|
|
29
|
+
|
|
30
|
+
/** Send a raw message to the worker. */
|
|
31
|
+
sendMessage(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
32
|
+
|
|
33
|
+
/** Insert / upsert one document. */
|
|
34
|
+
set(collection: string, key: string, value: Record<string, unknown>, options?: { skipSync?: boolean }): Promise<void>;
|
|
35
|
+
|
|
36
|
+
/** Fetch a single document by key. */
|
|
37
|
+
get(collection: string, key: string): Promise<unknown>;
|
|
38
|
+
|
|
39
|
+
/** Fetch all documents in a collection. */
|
|
40
|
+
getAll(collection: string): Promise<unknown>;
|
|
41
|
+
|
|
42
|
+
/** Delete a document by key. */
|
|
43
|
+
delete(collection: string, key: string, options?: { skipSync?: boolean }): Promise<void>;
|
|
44
|
+
|
|
45
|
+
/** Compact the OPFS log file. */
|
|
46
|
+
compact(): Promise<unknown>;
|
|
47
|
+
|
|
48
|
+
/** Subscribe to real-time server push events. Returns an unsubscribe function. */
|
|
49
|
+
onSync(callback: SyncCallback): () => void;
|
|
50
|
+
|
|
51
|
+
/** Close the WebSocket connection and stop the sync timer. */
|
|
52
|
+
disconnect(): void;
|
|
53
|
+
|
|
54
|
+
/** Terminate the Web Worker (and disconnect sync). */
|
|
55
|
+
terminate(): void;
|
|
56
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
export class MoltenDB {
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} dbName - OPFS file name (unique per database).
|
|
18
|
+
* @param {object} [options]
|
|
19
|
+
* @param {string|URL} [options.workerUrl] - URL to moltendb-worker.js.
|
|
20
|
+
* Defaults to './moltendb-worker.js' (works when served from the same directory).
|
|
21
|
+
* @param {boolean} [options.syncEnabled=false] - Enable WebSocket sync.
|
|
22
|
+
* @param {string} [options.serverUrl='wss://localhost:3000/ws'] - WS server URL.
|
|
23
|
+
* @param {number} [options.syncIntervalMs=5000] - Sync batch flush interval.
|
|
24
|
+
* @param {string} [options.authToken] - JWT token for WS authentication.
|
|
25
|
+
*/
|
|
26
|
+
constructor(dbName = 'moltendb', options = {}) {
|
|
27
|
+
this.dbName = dbName;
|
|
28
|
+
this.workerUrl = options.workerUrl ?? './moltendb-worker.js';
|
|
29
|
+
this.worker = null;
|
|
30
|
+
this.messageId = 0;
|
|
31
|
+
this.pendingRequests = new Map();
|
|
32
|
+
|
|
33
|
+
this.syncEnabled = options.syncEnabled ?? false;
|
|
34
|
+
this.serverUrl = options.serverUrl ?? 'wss://localhost:3000/ws';
|
|
35
|
+
this.syncIntervalMs = options.syncIntervalMs ?? 5000;
|
|
36
|
+
this.authToken = options.authToken ?? null;
|
|
37
|
+
|
|
38
|
+
this.ws = null;
|
|
39
|
+
this.syncCallbacks = new Set();
|
|
40
|
+
this.syncQueue = [];
|
|
41
|
+
this.syncTimer = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Initialise the Web Worker and open the OPFS database. */
|
|
45
|
+
async init() {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
this.worker = new Worker(this.workerUrl, { type: 'module' });
|
|
48
|
+
|
|
49
|
+
this.worker.onmessage = (event) => {
|
|
50
|
+
const { id, result, error } = event.data;
|
|
51
|
+
const pending = this.pendingRequests.get(id);
|
|
52
|
+
if (!pending) return;
|
|
53
|
+
this.pendingRequests.delete(id);
|
|
54
|
+
if (error) pending.reject(new Error(error));
|
|
55
|
+
else pending.resolve(result);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
this.worker.onerror = (err) => {
|
|
59
|
+
console.error('[MoltenDB] Worker error:', err);
|
|
60
|
+
reject(err);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
this.sendMessage('init', { dbName: this.dbName })
|
|
64
|
+
.then(() => this.syncEnabled ? this.connectSync() : undefined)
|
|
65
|
+
.then(resolve)
|
|
66
|
+
.catch(reject);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Send a raw message to the worker and return a Promise for the result.
|
|
72
|
+
* @param {string} action
|
|
73
|
+
* @param {object} [params]
|
|
74
|
+
*/
|
|
75
|
+
sendMessage(action, params = {}) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const id = this.messageId++;
|
|
78
|
+
this.pendingRequests.set(id, { resolve, reject });
|
|
79
|
+
this.worker.postMessage({ id, action, ...params });
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── WebSocket sync ────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
connectSync() {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
this.ws = new WebSocket(this.serverUrl);
|
|
88
|
+
|
|
89
|
+
this.ws.onopen = () => {
|
|
90
|
+
if (this.authToken) {
|
|
91
|
+
this.ws.send(JSON.stringify({ action: 'AUTH', token: this.authToken }));
|
|
92
|
+
}
|
|
93
|
+
if (this.syncTimer) clearInterval(this.syncTimer);
|
|
94
|
+
this.syncTimer = setInterval(() => this.flushSyncQueue(), this.syncIntervalMs);
|
|
95
|
+
resolve();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
this.ws.onmessage = (event) => {
|
|
99
|
+
try { this.handleServerUpdate(JSON.parse(event.data)); }
|
|
100
|
+
catch (e) { console.error('[MoltenDB] Failed to parse server message:', e); }
|
|
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.)
|
|
116
|
+
}
|
|
117
|
+
this.syncCallbacks.forEach(cb => cb(update));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Subscribe to real-time server push events. Returns an unsubscribe fn. */
|
|
121
|
+
onSync(callback) {
|
|
122
|
+
this.syncCallbacks.add(callback);
|
|
123
|
+
return () => this.syncCallbacks.delete(callback);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
flushSyncQueue() {
|
|
127
|
+
if (!this.syncQueue.length || !this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
128
|
+
const batch = this.syncQueue.splice(0);
|
|
129
|
+
this.ws.send(JSON.stringify({ action: 'set', operations: batch }));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Convenience CRUD helpers ──────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/** Insert / upsert one document. */
|
|
135
|
+
async set(collection, key, value, options = {}) {
|
|
136
|
+
await this.sendMessage('set', { collection, data: { [key]: value } });
|
|
137
|
+
if (this.syncEnabled && !options.skipSync) {
|
|
138
|
+
this.syncQueue.push({ action: 'set', collection, data: { [key]: value } });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Fetch a single document by key. */
|
|
143
|
+
get(collection, key) {
|
|
144
|
+
return this.sendMessage('get', { collection, keys: key });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Fetch all documents in a collection. */
|
|
148
|
+
getAll(collection) {
|
|
149
|
+
return this.sendMessage('get', { collection });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Delete a document by key. */
|
|
153
|
+
async delete(collection, key, options = {}) {
|
|
154
|
+
await this.sendMessage('delete', { collection, keys: key });
|
|
155
|
+
if (this.syncEnabled && !options.skipSync) {
|
|
156
|
+
this.syncQueue.push({ action: 'delete', collection, keys: key });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Compact the OPFS log file. */
|
|
161
|
+
compact() {
|
|
162
|
+
return this.sendMessage('compact');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Close the WebSocket connection and stop the sync timer. */
|
|
166
|
+
disconnect() {
|
|
167
|
+
if (this.syncTimer) clearInterval(this.syncTimer);
|
|
168
|
+
if (this.ws) this.ws.close();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Terminate the Web Worker (and disconnect sync). */
|
|
172
|
+
terminate() {
|
|
173
|
+
this.disconnect();
|
|
174
|
+
if (this.worker) this.worker.terminate();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// MoltenDB Web Worker — auto-generated by moltendb-wasm build script
|
|
2
|
+
// Do not edit directly; edit moltendb-wasm/scripts/build.mjs instead.
|
|
3
|
+
import init, { WorkerDb } from './moltendb.js';
|
|
4
|
+
|
|
5
|
+
let db = null;
|
|
6
|
+
|
|
7
|
+
self.onmessage = async (event) => {
|
|
8
|
+
const { id, action, ...params } = event.data;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
if (!db && action === 'init') {
|
|
12
|
+
await init();
|
|
13
|
+
db = await new WorkerDb(params.dbName || 'moltendb');
|
|
14
|
+
self.postMessage({ id, result: { status: 'initialized' } });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!db) {
|
|
19
|
+
throw new Error('Database not initialized. Send an "init" message first.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Pass action + params but NOT id so the Rust validator does not see
|
|
23
|
+
// 'id' as an unknown property.
|
|
24
|
+
const result = db.handle_message({ action, ...params });
|
|
25
|
+
self.postMessage({ id, result });
|
|
26
|
+
|
|
27
|
+
} catch (error) {
|
|
28
|
+
self.postMessage({ id, error: error.toString() });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The WASM-exposed database handle used by the JavaScript Web Worker.
|
|
6
|
+
*
|
|
7
|
+
* `#[wasm_bindgen]` on the struct makes it visible to JavaScript.
|
|
8
|
+
* JavaScript creates an instance with: `const db = await new WorkerDb("mydb")`
|
|
9
|
+
*
|
|
10
|
+
* The struct wraps a `Db` — the same engine used on the server.
|
|
11
|
+
* All methods on this struct are thin adapters that:
|
|
12
|
+
* 1. Convert JavaScript values (JsValue) to Rust types.
|
|
13
|
+
* 2. Call the underlying Db methods.
|
|
14
|
+
* 3. Convert the result back to JsValue for JavaScript.
|
|
15
|
+
*/
|
|
16
|
+
export class WorkerDb {
|
|
17
|
+
free(): void;
|
|
18
|
+
[Symbol.dispose](): void;
|
|
19
|
+
/**
|
|
20
|
+
* Execute an analytics query and return the result as a JSON string.
|
|
21
|
+
*
|
|
22
|
+
* This is the method called by the dashboard's auto-refresh loop:
|
|
23
|
+
* `const resultStr = db.analytics(JSON.stringify(query))`
|
|
24
|
+
*
|
|
25
|
+
* Takes a JSON string (not a JsValue) because the analytics query format
|
|
26
|
+
* is complex and easier to pass as a pre-serialized string from JavaScript.
|
|
27
|
+
*
|
|
28
|
+
* Returns a JSON string (not a JsValue) so JavaScript can parse it with
|
|
29
|
+
* `JSON.parse(resultStr)` and access `result` and `metadata`.
|
|
30
|
+
*
|
|
31
|
+
* Example input:
|
|
32
|
+
* `'{"collection":"events","metric":{"type":"COUNT"},"where":{"event_type":"button_click"}}'`
|
|
33
|
+
*
|
|
34
|
+
* Example output:
|
|
35
|
+
* `'{"result":42,"metadata":{"execution_time_ms":0,"rows_scanned":42}}'`
|
|
36
|
+
*
|
|
37
|
+
* `#[wasm_bindgen(js_name = analytics)]` sets the JavaScript method name to
|
|
38
|
+
* "analytics" (matching the call in analytics-worker.js).
|
|
39
|
+
*/
|
|
40
|
+
analytics(query_json: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Route an incoming message from the JavaScript worker to the correct handler.
|
|
43
|
+
*
|
|
44
|
+
* Called from moltendb-worker.js as:
|
|
45
|
+
* `db.handle_message({ action: 'get', collection: 'laptops', keys: 'lp1' })`
|
|
46
|
+
*
|
|
47
|
+
* The `data` parameter is a plain JavaScript object (not a MessageEvent wrapper).
|
|
48
|
+
* It must have an `action` field that determines which operation to perform.
|
|
49
|
+
*
|
|
50
|
+
* Supported actions — identical to the HTTP server endpoints:
|
|
51
|
+
* - "get" → query documents (single key, batch, full collection, WHERE, joins, sort, pagination)
|
|
52
|
+
* - "set" → insert or upsert documents: { collection, data: { key: doc, ... } }
|
|
53
|
+
* - "update" → patch/merge documents: { collection, data: { key: patch, ... } }
|
|
54
|
+
* - "delete" → delete documents or drop: { collection, keys: ... } or { drop: true }
|
|
55
|
+
* - "compact" → compact the OPFS log file
|
|
56
|
+
* - "get_size" → return current OPFS file size in bytes
|
|
57
|
+
*
|
|
58
|
+
* Returns a JsValue result on success, or a JsValue error string on failure.
|
|
59
|
+
*/
|
|
60
|
+
handle_message(data: any): any;
|
|
61
|
+
/**
|
|
62
|
+
* Initialize the database and open (or create) the OPFS storage file.
|
|
63
|
+
*
|
|
64
|
+
* This is the constructor — called from JavaScript as:
|
|
65
|
+
* `const db = await new WorkerDb("click_analytics_db")`
|
|
66
|
+
*
|
|
67
|
+
* `#[wasm_bindgen(constructor)]` tells wasm-bindgen that this function
|
|
68
|
+
* is the JavaScript `new WorkerDb(...)` constructor.
|
|
69
|
+
*
|
|
70
|
+
* `async` because opening the OPFS file handle is an async browser API.
|
|
71
|
+
* Returns `Result<WorkerDb, JsValue>` — on error, the JsValue becomes a
|
|
72
|
+
* JavaScript exception that the worker's try/catch can handle.
|
|
73
|
+
*
|
|
74
|
+
* # Arguments
|
|
75
|
+
* * `db_name` — The name of the OPFS file to open (e.g. "click_analytics_db").
|
|
76
|
+
* Each unique name is a separate database file in the browser's OPFS storage.
|
|
77
|
+
*/
|
|
78
|
+
constructor(db_name: string);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
|
82
|
+
|
|
83
|
+
export interface InitOutput {
|
|
84
|
+
readonly memory: WebAssembly.Memory;
|
|
85
|
+
readonly __wbg_workerdb_free: (a: number, b: number) => void;
|
|
86
|
+
readonly workerdb_analytics: (a: number, b: number, c: number, d: number) => void;
|
|
87
|
+
readonly workerdb_handle_message: (a: number, b: number, c: number) => void;
|
|
88
|
+
readonly workerdb_new: (a: number, b: number) => number;
|
|
89
|
+
readonly __wasm_bindgen_func_elem_3598: (a: number, b: number) => void;
|
|
90
|
+
readonly __wasm_bindgen_func_elem_3677: (a: number, b: number, c: number, d: number) => void;
|
|
91
|
+
readonly __wasm_bindgen_func_elem_3690: (a: number, b: number, c: number, d: number) => void;
|
|
92
|
+
readonly __wbindgen_export: (a: number, b: number) => number;
|
|
93
|
+
readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
94
|
+
readonly __wbindgen_export3: (a: number) => void;
|
|
95
|
+
readonly __wbindgen_export4: (a: number, b: number, c: number) => void;
|
|
96
|
+
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Instantiates the given `module`, which can either be bytes or
|
|
103
|
+
* a precompiled `WebAssembly.Module`.
|
|
104
|
+
*
|
|
105
|
+
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
|
106
|
+
*
|
|
107
|
+
* @returns {InitOutput}
|
|
108
|
+
*/
|
|
109
|
+
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
|
113
|
+
* for everything else, calls `WebAssembly.instantiate` directly.
|
|
114
|
+
*
|
|
115
|
+
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
|
116
|
+
*
|
|
117
|
+
* @returns {Promise<InitOutput>}
|
|
118
|
+
*/
|
|
119
|
+
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|