@taladb/web 0.2.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/pkg/package.json +1 -1
- package/pkg/taladb_web_bg.wasm +0 -0
- package/worker/taladb.worker.js +91 -47
package/package.json
CHANGED
package/pkg/package.json
CHANGED
package/pkg/taladb_web_bg.wasm
CHANGED
|
Binary file
|
package/worker/taladb.worker.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TalaDB
|
|
2
|
+
* TalaDB Worker
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* a
|
|
4
|
+
* Runs as a Dedicated Worker. Each tab spawns its own worker instance.
|
|
5
|
+
* Multi-tab write safety is provided by the Web Locks API — only one worker
|
|
6
|
+
* holds the exclusive lock on the OPFS file at a time. Other workers queue up
|
|
7
|
+
* and acquire the lock automatically when the current holder closes.
|
|
7
8
|
*
|
|
8
|
-
* Why
|
|
9
|
+
* Why DedicatedWorker (not SharedWorker)?
|
|
9
10
|
* ----------------------------------------
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* createSyncAccessHandle() — required for synchronous OPFS I/O — is only
|
|
12
|
+
* available in DedicatedWorkerGlobalScope per the WHATWG File System spec.
|
|
13
|
+
* SharedWorker cannot call it; Chrome throws "is not a function".
|
|
14
|
+
*
|
|
15
|
+
* Why Web Locks?
|
|
16
|
+
* --------------
|
|
17
|
+
* Without coordination, two tabs opening the same OPFS file would race.
|
|
18
|
+
* navigator.locks.request() gives us an exclusive named lock. The first tab
|
|
19
|
+
* acquires it immediately; subsequent tabs block until the holder's worker is
|
|
20
|
+
* terminated (tab closed / navigated) or db.close() is called explicitly.
|
|
21
|
+
* If Web Locks is unavailable the worker opens the file directly and logs a
|
|
22
|
+
* warning (safe for single-tab use).
|
|
13
23
|
*
|
|
14
24
|
* Message protocol
|
|
15
25
|
* ----------------
|
|
@@ -17,9 +27,6 @@
|
|
|
17
27
|
* Response → { id: number, result: unknown }
|
|
18
28
|
* | { id: number, error: string }
|
|
19
29
|
*
|
|
20
|
-
* The `id` field lets the main thread match async responses to pending
|
|
21
|
-
* Promise resolvers even when operations complete out of order.
|
|
22
|
-
*
|
|
23
30
|
* Supported ops
|
|
24
31
|
* -------------
|
|
25
32
|
* init { dbName }
|
|
@@ -50,10 +57,7 @@
|
|
|
50
57
|
let db = null;
|
|
51
58
|
|
|
52
59
|
/**
|
|
53
|
-
*
|
|
54
|
-
* - Concurrent init calls for the same dbName share one promise (deduplicated).
|
|
55
|
-
* - Init calls for different dbNames are rejected after the first succeeds,
|
|
56
|
-
* because a SharedWorker instance owns exactly one database file.
|
|
60
|
+
* Deduplicates concurrent init calls for the same dbName within one worker.
|
|
57
61
|
* @type {Map<string, Promise<void>>}
|
|
58
62
|
*/
|
|
59
63
|
const initPromises = new Map();
|
|
@@ -61,24 +65,29 @@ const initPromises = new Map();
|
|
|
61
65
|
/** The dbName that was successfully initialised (or is being initialised). */
|
|
62
66
|
let activeDbName = null;
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const isDev = typeof location !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1');
|
|
69
|
+
const log = isDev ? console.log.bind(console, '[TalaDB Worker]') : () => {};
|
|
70
|
+
const warn = isDev ? console.warn.bind(console, '[TalaDB Worker]') : () => {};
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Resolving this releases the Web Lock and closes the sync handle.
|
|
74
|
+
* Set inside doInit; called by the 'close' op or when the worker terminates.
|
|
75
|
+
* @type {(() => void) | null}
|
|
76
|
+
*/
|
|
77
|
+
let releaseLock = null;
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const result = await dispatch(op, args);
|
|
75
|
-
port.postMessage({ id, result: result ?? null });
|
|
76
|
-
} catch (err) {
|
|
77
|
-
port.postMessage({ id, error: String(err?.message ?? err) });
|
|
78
|
-
}
|
|
79
|
-
};
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Dedicated Worker message handler
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
self.onmessage = async (e) => {
|
|
84
|
+
const { id, op, ...args } = e.data;
|
|
85
|
+
try {
|
|
86
|
+
const result = await dispatch(op, args);
|
|
87
|
+
self.postMessage({ id, result: result ?? null });
|
|
88
|
+
} catch (err) {
|
|
89
|
+
self.postMessage({ id, error: String(err?.message ?? err) });
|
|
90
|
+
}
|
|
82
91
|
};
|
|
83
92
|
|
|
84
93
|
// ---------------------------------------------------------------------------
|
|
@@ -89,16 +98,13 @@ async function dispatch(op, args) {
|
|
|
89
98
|
if (op === 'init') {
|
|
90
99
|
const { dbName } = args;
|
|
91
100
|
|
|
92
|
-
// Reject if a different database was already opened in this worker instance.
|
|
93
|
-
// A SharedWorker owns exactly one OPFS file handle.
|
|
94
101
|
if (activeDbName !== null && activeDbName !== dbName) {
|
|
95
102
|
throw new Error(
|
|
96
103
|
`TalaDB worker already initialised for "${activeDbName}". ` +
|
|
97
|
-
`Cannot open "${dbName}" in the same
|
|
104
|
+
`Cannot open "${dbName}" in the same worker instance.`
|
|
98
105
|
);
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
// Deduplicate concurrent init calls for the same dbName.
|
|
102
108
|
if (!initPromises.has(dbName)) {
|
|
103
109
|
activeDbName = dbName;
|
|
104
110
|
initPromises.set(dbName, doInit(dbName));
|
|
@@ -171,6 +177,8 @@ async function dispatch(op, args) {
|
|
|
171
177
|
);
|
|
172
178
|
|
|
173
179
|
case 'close':
|
|
180
|
+
// Release the Web Lock and close the sync handle gracefully.
|
|
181
|
+
if (releaseLock) { releaseLock(); releaseLock = null; }
|
|
174
182
|
db = null;
|
|
175
183
|
return null;
|
|
176
184
|
|
|
@@ -180,41 +188,77 @@ async function dispatch(op, args) {
|
|
|
180
188
|
}
|
|
181
189
|
|
|
182
190
|
// ---------------------------------------------------------------------------
|
|
183
|
-
// Initialisation — load WASM, open OPFS file
|
|
191
|
+
// Initialisation — load WASM, acquire lock, open OPFS file
|
|
184
192
|
// ---------------------------------------------------------------------------
|
|
185
193
|
|
|
186
194
|
async function doInit(dbName) {
|
|
187
|
-
// Dynamic import of the WASM module (wasm-pack --target web output)
|
|
188
|
-
// The bundler (Vite/Webpack) will resolve this path correctly.
|
|
189
195
|
const wasm = await import('../pkg/taladb_web.js');
|
|
190
|
-
await wasm.default();
|
|
196
|
+
await wasm.default();
|
|
191
197
|
|
|
192
198
|
const { WorkerDB } = wasm;
|
|
193
199
|
|
|
194
200
|
const opfsAvailable = await checkOpfs();
|
|
195
201
|
if (!opfsAvailable) {
|
|
196
|
-
|
|
202
|
+
warn('OPFS unavailable — falling back to in-memory');
|
|
197
203
|
db = WorkerDB.openInMemory();
|
|
198
204
|
return;
|
|
199
205
|
}
|
|
200
206
|
|
|
201
|
-
// Get the OPFS root directory
|
|
202
207
|
const root = await navigator.storage.getDirectory();
|
|
203
|
-
|
|
204
|
-
// Open (or create) the database file
|
|
205
208
|
const fileName = `taladb_${dbName.replaceAll(/[/\\:]/g, '_')}.redb`;
|
|
206
209
|
const fileHandle = await root.getFileHandle(fileName, { create: true });
|
|
207
210
|
|
|
208
|
-
|
|
209
|
-
|
|
211
|
+
if (!('locks' in navigator)) {
|
|
212
|
+
// Web Locks not available — open directly (single-tab safe only).
|
|
213
|
+
warn('Web Locks unavailable — multi-tab write safety disabled');
|
|
214
|
+
const syncHandle = await fileHandle.createSyncAccessHandle();
|
|
215
|
+
db = WorkerDB.openWithOpfs(syncHandle);
|
|
216
|
+
log(`Opened "${fileName}" via OPFS`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
210
219
|
|
|
211
|
-
|
|
212
|
-
|
|
220
|
+
// Acquire an exclusive lock on the database file.
|
|
221
|
+
// If another tab's worker already holds it, this call blocks until that
|
|
222
|
+
// worker calls close() or the tab is terminated (lock auto-released).
|
|
223
|
+
const lockName = `taladb:${fileName}`;
|
|
224
|
+
await new Promise((resolve, reject) => {
|
|
225
|
+
navigator.locks.request(lockName, async () => {
|
|
226
|
+
try {
|
|
227
|
+
const syncHandle = await fileHandle.createSyncAccessHandle();
|
|
228
|
+
db = WorkerDB.openWithOpfs(syncHandle);
|
|
229
|
+
log(`Opened "${fileName}" via OPFS (Web Locks)`);
|
|
230
|
+
resolve(); // signal doInit complete — caller can proceed
|
|
231
|
+
|
|
232
|
+
// Hold the lock by keeping this async callback alive.
|
|
233
|
+
// Resolved by the 'close' op or when the worker is terminated.
|
|
234
|
+
await new Promise(res => { releaseLock = res; });
|
|
235
|
+
|
|
236
|
+
syncHandle.close();
|
|
237
|
+
db = null;
|
|
238
|
+
} catch (e) {
|
|
239
|
+
reject(e);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
});
|
|
213
243
|
}
|
|
214
244
|
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// OPFS capability probe
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
215
249
|
async function checkOpfs() {
|
|
216
250
|
try {
|
|
217
|
-
await navigator.storage.getDirectory();
|
|
251
|
+
const root = await navigator.storage.getDirectory();
|
|
252
|
+
// Probe createSyncAccessHandle — only available in Dedicated Workers.
|
|
253
|
+
// getDirectory() succeeding alone is not sufficient.
|
|
254
|
+
// Use a unique filename so concurrent workers don't collide on the same
|
|
255
|
+
// probe file (each createSyncAccessHandle is exclusive).
|
|
256
|
+
const probeName = `_taladb_probe_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
257
|
+
const probe = await root.getFileHandle(probeName, { create: true });
|
|
258
|
+
if (typeof probe.createSyncAccessHandle !== 'function') return false;
|
|
259
|
+
const handle = await probe.createSyncAccessHandle();
|
|
260
|
+
handle.close();
|
|
261
|
+
await root.removeEntry(probeName);
|
|
218
262
|
return true;
|
|
219
263
|
} catch {
|
|
220
264
|
return false;
|