@evolu/web 3.2.0 → 3.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/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +17 -10
- package/dist/src/Worker.d.ts +38 -1
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +61 -2
- package/dist/src/local-first/Shared.worker.js +12 -4
- package/package.json +3 -3
- package/src/Sqlite.test.ts +5 -4
- package/src/Sqlite.ts +17 -11
- package/src/Worker.test.ts +59 -0
- package/src/Worker.ts +66 -1
- package/src/local-first/Evolu.test.ts +1 -0
- package/src/local-first/Shared.worker.ts +29 -5
package/dist/src/Sqlite.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sqlite.d.ts","sourceRoot":"","sources":["../../src/Sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAa,MAAM,eAAe,CAAC;AAqCnE,eAAO,MAAM,sBAAsB,EAAE,
|
|
1
|
+
{"version":3,"file":"Sqlite.d.ts","sourceRoot":"","sources":["../../src/Sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAa,MAAM,eAAe,CAAC;AAqCnE,eAAO,MAAM,sBAAsB,EAAE,kBAyLlC,CAAC"}
|
package/dist/src/Sqlite.js
CHANGED
|
@@ -179,17 +179,24 @@ export const createWasmSqliteDriver = (name, options) => async (run) => {
|
|
|
179
179
|
exec: (query) => {
|
|
180
180
|
const prepared = cache.get(query);
|
|
181
181
|
if (prepared) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
try {
|
|
183
|
+
if (query.parameters.length > 0)
|
|
184
|
+
prepared.bind(query.parameters);
|
|
185
|
+
const rows = [];
|
|
186
|
+
while (prepared.step()) {
|
|
187
|
+
rows.push(prepared.get({}));
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
rows: rows,
|
|
191
|
+
changes: db.changes(),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
// SQLite refuses to bind a statement whose step failed until it is
|
|
196
|
+
// reset. That reset returns the step's error again, which
|
|
197
|
+
// PreparedStatement.reset would throw over the original one.
|
|
198
|
+
sqlite3.capi.sqlite3_reset(prepared);
|
|
187
199
|
}
|
|
188
|
-
prepared.reset();
|
|
189
|
-
return {
|
|
190
|
-
rows: rows,
|
|
191
|
-
changes: db.changes(),
|
|
192
|
-
};
|
|
193
200
|
}
|
|
194
201
|
const rows = db.exec(query.sql, {
|
|
195
202
|
returnValue: "resultRows",
|
package/dist/src/Worker.d.ts
CHANGED
|
@@ -44,7 +44,9 @@ export declare const createWorkerSelf: <Input, Output = never>(nativeSelf: Dedic
|
|
|
44
44
|
* Creates an Evolu {@link SharedWorkerSelf} from a Web `SharedWorkerGlobalScope`
|
|
45
45
|
* (`self` inside a shared worker).
|
|
46
46
|
*
|
|
47
|
-
* Disposing closes the shared worker scope for all connected clients.
|
|
47
|
+
* Disposing closes the shared worker scope for all connected clients. Browsers
|
|
48
|
+
* never pass a shared worker's uncaught errors to its pages; see
|
|
49
|
+
* {@link addUncaughtErrorListener}.
|
|
48
50
|
*/
|
|
49
51
|
export declare const createSharedWorkerSelf: <Input, Output = never>(nativeSelf: SharedWorkerGlobalScope) => SharedWorkerSelf<Input, Output>;
|
|
50
52
|
/**
|
|
@@ -55,6 +57,41 @@ export declare const createSharedWorkerSelf: <Input, Output = never>(nativeSelf:
|
|
|
55
57
|
* `onMessage` is set, and does not share state across tabs.
|
|
56
58
|
*/
|
|
57
59
|
export declare const createOneTabSharedWorkerSelfPolyfill: <Input, Output = never>(nativeSelf: DedicatedWorkerGlobalScope) => SharedWorkerSelf<Input, Output>;
|
|
60
|
+
/**
|
|
61
|
+
* Passes each uncaught error and unhandled rejection of a worker global scope
|
|
62
|
+
* to `listener` as an `Error` from {@link defectToError}.
|
|
63
|
+
*
|
|
64
|
+
* Browsers never pass a shared worker's uncaught errors to a page's error
|
|
65
|
+
* handlers, and no worker passes its unhandled rejections to its page. A shared
|
|
66
|
+
* worker calls this with its global scope, `self`, to send them to its pages,
|
|
67
|
+
* for example over a BroadcastChannel. Defects of a Run from {@link createRun}
|
|
68
|
+
* arrive too, because it reports them with `reportError`, which dispatches
|
|
69
|
+
* "error" on the global scope.
|
|
70
|
+
*
|
|
71
|
+
* The events stay uncanceled, so the browser still logs them, and a dedicated
|
|
72
|
+
* worker still passes its uncaught errors to its page. The listeners stay until
|
|
73
|
+
* disposal, so they also receive errors after a Run's panic.
|
|
74
|
+
*
|
|
75
|
+
* ### Example
|
|
76
|
+
*
|
|
77
|
+
* ```ts
|
|
78
|
+
* import { assertEqual } from "@evolu/common";
|
|
79
|
+
* import { addUncaughtErrorListener } from "@evolu/web";
|
|
80
|
+
*
|
|
81
|
+
* const messages: Array<string> = [];
|
|
82
|
+
* // A worker passes its global scope, `self`.
|
|
83
|
+
* const scope = new EventTarget();
|
|
84
|
+
* using _listener = addUncaughtErrorListener(scope, (error) => {
|
|
85
|
+
* messages.push(error.message);
|
|
86
|
+
* });
|
|
87
|
+
*
|
|
88
|
+
* scope.dispatchEvent(
|
|
89
|
+
* Object.assign(new Event("error"), { error: new Error("unexpected") }),
|
|
90
|
+
* );
|
|
91
|
+
* assertEqual(messages, ["unexpected"]);
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export declare const addUncaughtErrorListener: (scope: EventTarget, listener: (error: Error) => void) => Disposable;
|
|
58
95
|
/** Creates deps shared by web worker entry points. */
|
|
59
96
|
export declare const createWorkerDeps: () => WorkerDeps & CreateBroadcastChannelDep & CreateMessageChannelDep;
|
|
60
97
|
//# sourceMappingURL=Worker.d.ts.map
|
package/dist/src/Worker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Worker.d.ts","sourceRoot":"","sources":["../../src/Worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,sBAAsB,EACtB,yBAAyB,EACzB,uBAAuB,EACvB,iBAAiB,EACjB,cAAc,EAGd,YAAY,EACZ,gBAAgB,EAEhB,MAAM,EACN,UAAU,EACV,UAAU,EACX,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"Worker.d.ts","sourceRoot":"","sources":["../../src/Worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,sBAAsB,EACtB,yBAAyB,EACzB,uBAAuB,EACvB,iBAAiB,EACjB,cAAc,EAGd,YAAY,EACZ,gBAAgB,EAEhB,MAAM,EACN,UAAU,EACV,UAAU,EACX,MAAM,eAAe,CAAC;AAUvB,kDAAkD;AAClD,eAAO,MAAM,YAAY,GAAI,KAAK,EAAE,MAAM,gBAC1B,UAAU,CAAC,MAAM,KAC9B,MAAM,CAAC,KAAK,EAAE,MAAM,CAAuB,CAAC;AAE/C;;;;;GAKG;AACH,eAAO,MAAM,iCAAiC,QAAO,IAiBpD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,KAAK,EAAE,MAAM,GAAG,KAAK,sBAClC,UAAU,CAAC,YAAY,KAC1C,YAAY,CAAC,KAAK,EAAE,MAAM,CAM5B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,EAAE,MAAM,GAAG,KAAK,OAAK,cAAc,CAC3E,KAAK,EACL,MAAM,CAcP,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,iBACuB,CAAC;AAExD,6EAA6E;AAC7E,eAAO,MAAM,sBAAsB,EAAE,sBAwCpC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,KAAK,EAAE,MAAM,GAAG,KAAK,cACxC,0BAA0B,KACrC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAoC,CAAC;AAEhE;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,GAAI,KAAK,EAAE,MAAM,GAAG,KAAK,cAC9C,uBAAuB,KAClC,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAoBhC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oCAAoC,GAAI,KAAK,EAAE,MAAM,GAAG,KAAK,cAC5D,0BAA0B,KACrC,gBAAgB,CAAC,KAAK,EAAE,MAAM,CA2EhC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,wBAAwB,UAC5B,WAAW,YACR,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,KAC/B,UAsBF,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,gBAAgB,QAAO,UAAU,GAC5C,yBAAyB,GACzB,uBAWD,CAAC"}
|
package/dist/src/Worker.js
CHANGED
|
@@ -50,7 +50,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
50
50
|
var e = new Error(message);
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
|
-
import { assertNonNullable, createConsole, createConsoleStoreOutput, disposable, } from "@evolu/common";
|
|
53
|
+
import { assertNonNullable, createConsole, createConsoleStoreOutput, defectToError, disposable, } from "@evolu/common";
|
|
54
54
|
/** Creates a {@link Worker} from a Web Worker. */
|
|
55
55
|
export const createWorker = (nativeWorker) => wrap(nativeWorker);
|
|
56
56
|
/**
|
|
@@ -176,7 +176,9 @@ export const createWorkerSelf = (nativeSelf) => wrap(nativeSelf);
|
|
|
176
176
|
* Creates an Evolu {@link SharedWorkerSelf} from a Web `SharedWorkerGlobalScope`
|
|
177
177
|
* (`self` inside a shared worker).
|
|
178
178
|
*
|
|
179
|
-
* Disposing closes the shared worker scope for all connected clients.
|
|
179
|
+
* Disposing closes the shared worker scope for all connected clients. Browsers
|
|
180
|
+
* never pass a shared worker's uncaught errors to its pages; see
|
|
181
|
+
* {@link addUncaughtErrorListener}.
|
|
180
182
|
*/
|
|
181
183
|
export const createSharedWorkerSelf = (nativeSelf) => {
|
|
182
184
|
const self = {
|
|
@@ -285,6 +287,63 @@ export const createOneTabSharedWorkerSelfPolyfill = (nativeSelf) => {
|
|
|
285
287
|
__disposeResources(env_3);
|
|
286
288
|
}
|
|
287
289
|
};
|
|
290
|
+
/**
|
|
291
|
+
* Passes each uncaught error and unhandled rejection of a worker global scope
|
|
292
|
+
* to `listener` as an `Error` from {@link defectToError}.
|
|
293
|
+
*
|
|
294
|
+
* Browsers never pass a shared worker's uncaught errors to a page's error
|
|
295
|
+
* handlers, and no worker passes its unhandled rejections to its page. A shared
|
|
296
|
+
* worker calls this with its global scope, `self`, to send them to its pages,
|
|
297
|
+
* for example over a BroadcastChannel. Defects of a Run from {@link createRun}
|
|
298
|
+
* arrive too, because it reports them with `reportError`, which dispatches
|
|
299
|
+
* "error" on the global scope.
|
|
300
|
+
*
|
|
301
|
+
* The events stay uncanceled, so the browser still logs them, and a dedicated
|
|
302
|
+
* worker still passes its uncaught errors to its page. The listeners stay until
|
|
303
|
+
* disposal, so they also receive errors after a Run's panic.
|
|
304
|
+
*
|
|
305
|
+
* ### Example
|
|
306
|
+
*
|
|
307
|
+
* ```ts
|
|
308
|
+
* import { assertEqual } from "@evolu/common";
|
|
309
|
+
* import { addUncaughtErrorListener } from "@evolu/web";
|
|
310
|
+
*
|
|
311
|
+
* const messages: Array<string> = [];
|
|
312
|
+
* // A worker passes its global scope, `self`.
|
|
313
|
+
* const scope = new EventTarget();
|
|
314
|
+
* using _listener = addUncaughtErrorListener(scope, (error) => {
|
|
315
|
+
* messages.push(error.message);
|
|
316
|
+
* });
|
|
317
|
+
*
|
|
318
|
+
* scope.dispatchEvent(
|
|
319
|
+
* Object.assign(new Event("error"), { error: new Error("unexpected") }),
|
|
320
|
+
* );
|
|
321
|
+
* assertEqual(messages, ["unexpected"]);
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
export const addUncaughtErrorListener = (scope, listener) => {
|
|
325
|
+
const env_4 = { stack: [], error: void 0, hasError: false };
|
|
326
|
+
try {
|
|
327
|
+
const disposer = __addDisposableResource(env_4, new DisposableStack(), false);
|
|
328
|
+
const { signal } = disposer.adopt(new AbortController(), (controller) => {
|
|
329
|
+
controller.abort();
|
|
330
|
+
});
|
|
331
|
+
scope.addEventListener("error", (event) => {
|
|
332
|
+
listener(defectToError(event.error));
|
|
333
|
+
}, { signal });
|
|
334
|
+
scope.addEventListener("unhandledrejection", (event) => {
|
|
335
|
+
listener(defectToError(event.reason));
|
|
336
|
+
}, { signal });
|
|
337
|
+
return disposable({}, disposer);
|
|
338
|
+
}
|
|
339
|
+
catch (e_4) {
|
|
340
|
+
env_4.error = e_4;
|
|
341
|
+
env_4.hasError = true;
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
__disposeResources(env_4);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
288
347
|
/** Creates deps shared by web worker entry points. */
|
|
289
348
|
export const createWorkerDeps = () => {
|
|
290
349
|
const consoleStoreOutput = createConsoleStoreOutput();
|
|
@@ -53,10 +53,16 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
53
53
|
/// <reference lib="webworker" />
|
|
54
54
|
import { installPolyfills } from "@evolu/common/polyfills";
|
|
55
55
|
installPolyfills();
|
|
56
|
-
import { createWebSocket, ok, tryAsync, waitForAbort } from "@evolu/common";
|
|
57
|
-
import { initSharedWorker } from "@evolu/common/local-first";
|
|
56
|
+
import { createUnknownError, createWebSocket, ok, tryAsync, waitForAbort, } from "@evolu/common";
|
|
57
|
+
import { consoleEntryOrErrorBroadcastChannelName, initSharedWorker, } from "@evolu/common/local-first";
|
|
58
58
|
import { createRun } from "../Task.js";
|
|
59
|
-
import { createOneTabSharedWorkerSelfPolyfill, createSharedWorkerSelf, createWorkerDeps, } from "../Worker.js";
|
|
59
|
+
import { addUncaughtErrorListener, createBroadcastChannel, createOneTabSharedWorkerSelfPolyfill, createSharedWorkerSelf, createWorkerDeps, } from "../Worker.js";
|
|
60
|
+
const errors = createBroadcastChannel(consoleEntryOrErrorBroadcastChannelName);
|
|
61
|
+
// Each tab logs an uncaught error, including a Run defect, and sets it as its
|
|
62
|
+
// evoluError.
|
|
63
|
+
addUncaughtErrorListener(self, (error) => {
|
|
64
|
+
errors.postMessage({ type: "Error", error: createUnknownError(error) });
|
|
65
|
+
});
|
|
60
66
|
const run = createRun({
|
|
61
67
|
...createWorkerDeps(),
|
|
62
68
|
createWebSocket,
|
|
@@ -70,7 +76,9 @@ void run(async (run) => {
|
|
|
70
76
|
if ("onconnect" in self) {
|
|
71
77
|
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
72
78
|
try {
|
|
73
|
-
|
|
79
|
+
// A panic closes the worker, so tabs that connect later start another.
|
|
80
|
+
const workerSelf = __addDisposableResource(env_2, createSharedWorkerSelf(self), false);
|
|
81
|
+
const _ = __addDisposableResource(env_2, await run.ok(initSharedWorker(workerSelf)), true);
|
|
74
82
|
return await run(waitForAbort);
|
|
75
83
|
}
|
|
76
84
|
catch (e_1) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evolu/web",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Evolu for web",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"evolu",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@evolu/sqlite-wasm": "2.2.4"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@evolu/common": "8.
|
|
38
|
+
"@evolu/common": "8.13.0",
|
|
39
39
|
"@evolu/typescript-config": "0.1.1",
|
|
40
40
|
"@types/node": "^24.10.9",
|
|
41
41
|
"@types/sharedworker": "^0.0.229",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"user-agent-data-types": "^0.4.2"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@evolu/common": "^8.
|
|
47
|
+
"@evolu/common": "^8.13.0"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"access": "public"
|
package/src/Sqlite.test.ts
CHANGED
|
@@ -117,10 +117,6 @@ const sqliteMock = (() => {
|
|
|
117
117
|
return { data: "prepared" };
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
reset(): void {
|
|
121
|
-
this.resetCount += 1;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
120
|
finalize(): void {
|
|
125
121
|
this.finalized = true;
|
|
126
122
|
}
|
|
@@ -193,6 +189,10 @@ const sqliteMock = (() => {
|
|
|
193
189
|
const sqlite3 = {
|
|
194
190
|
capi: {
|
|
195
191
|
sqlite3_js_db_export: mock.fn(() => new Uint8Array([1, 2, 3])),
|
|
192
|
+
sqlite3_reset: mock.fn((statement: PreparedStatement) => {
|
|
193
|
+
statement.resetCount += 1;
|
|
194
|
+
return 0;
|
|
195
|
+
}),
|
|
196
196
|
sqlite3mc_vfs_create: mock.fn(),
|
|
197
197
|
},
|
|
198
198
|
installOpfsSAHPoolVfs: mock.fn(() => Promise.resolve(pool)),
|
|
@@ -217,6 +217,7 @@ const sqliteMock = (() => {
|
|
|
217
217
|
pool.unpauseVfs.mock.resetCalls();
|
|
218
218
|
pool.unlink.mock.resetCalls();
|
|
219
219
|
sqlite3.capi.sqlite3_js_db_export.mock.resetCalls();
|
|
220
|
+
sqlite3.capi.sqlite3_reset.mock.resetCalls();
|
|
220
221
|
sqlite3.capi.sqlite3mc_vfs_create.mock.resetCalls();
|
|
221
222
|
sqlite3.installOpfsSAHPoolVfs.mock.resetCalls();
|
|
222
223
|
},
|
package/src/Sqlite.ts
CHANGED
|
@@ -178,18 +178,24 @@ export const createWasmSqliteDriver: CreateSqliteDriver =
|
|
|
178
178
|
const prepared = cache.get(query);
|
|
179
179
|
|
|
180
180
|
if (prepared) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
try {
|
|
182
|
+
if (query.parameters.length > 0) prepared.bind(query.parameters);
|
|
183
|
+
|
|
184
|
+
const rows = [];
|
|
185
|
+
while (prepared.step()) {
|
|
186
|
+
rows.push(prepared.get({}));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
rows: rows as ReadonlyArray<SqliteRow>,
|
|
191
|
+
changes: db.changes(),
|
|
192
|
+
};
|
|
193
|
+
} finally {
|
|
194
|
+
// SQLite refuses to bind a statement whose step failed until it is
|
|
195
|
+
// reset. That reset returns the step's error again, which
|
|
196
|
+
// PreparedStatement.reset would throw over the original one.
|
|
197
|
+
sqlite3.capi.sqlite3_reset(prepared);
|
|
186
198
|
}
|
|
187
|
-
prepared.reset();
|
|
188
|
-
|
|
189
|
-
return {
|
|
190
|
-
rows: rows as ReadonlyArray<SqliteRow>,
|
|
191
|
-
changes: db.changes(),
|
|
192
|
-
};
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
const rows = db.exec(query.sql, {
|
package/src/Worker.test.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
assertEqual,
|
|
3
|
+
assertFalse,
|
|
4
|
+
assertLength,
|
|
3
5
|
assertNonNullable,
|
|
4
6
|
assertSame,
|
|
5
7
|
assertThrowsInstanceOf,
|
|
@@ -9,6 +11,7 @@ import {
|
|
|
9
11
|
} from "@evolu/common";
|
|
10
12
|
import { describe, it, mock, test } from "node:test";
|
|
11
13
|
import {
|
|
14
|
+
addUncaughtErrorListener,
|
|
12
15
|
createBroadcastChannel,
|
|
13
16
|
createOneTabSharedWorkerSelfPolyfill,
|
|
14
17
|
createMessageChannel,
|
|
@@ -450,6 +453,62 @@ test("createSharedWorkerSelf asserts when a connection arrives before onConnect
|
|
|
450
453
|
);
|
|
451
454
|
});
|
|
452
455
|
|
|
456
|
+
describe("addUncaughtErrorListener", () => {
|
|
457
|
+
const dispatch = (
|
|
458
|
+
scope: EventTarget,
|
|
459
|
+
type: "error" | "unhandledrejection",
|
|
460
|
+
fields: object,
|
|
461
|
+
): Event => {
|
|
462
|
+
const event = Object.assign(new Event(type, { cancelable: true }), fields);
|
|
463
|
+
scope.dispatchEvent(event);
|
|
464
|
+
return event;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
it("passes uncaught errors and unhandled rejections without canceling them", () => {
|
|
468
|
+
const scope = new EventTarget();
|
|
469
|
+
const listener = mock.fn<(error: Error) => void>();
|
|
470
|
+
using _listener = addUncaughtErrorListener(scope, listener);
|
|
471
|
+
const error = new Error("uncaught");
|
|
472
|
+
const reason = new Error("rejected");
|
|
473
|
+
|
|
474
|
+
const errorEvent = dispatch(scope, "error", { error });
|
|
475
|
+
const rejectionEvent = dispatch(scope, "unhandledrejection", { reason });
|
|
476
|
+
|
|
477
|
+
const errors = listener.mock.calls.map((call) => call.arguments[0]);
|
|
478
|
+
assertLength(errors, 2);
|
|
479
|
+
assertSame(errors[0], error);
|
|
480
|
+
assertSame(errors[1], reason);
|
|
481
|
+
assertFalse(errorEvent.defaultPrevented);
|
|
482
|
+
assertFalse(rejectionEvent.defaultPrevented);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("passes a thrown non-Error as an Error", () => {
|
|
486
|
+
const scope = new EventTarget();
|
|
487
|
+
const listener = mock.fn<(error: Error) => void>();
|
|
488
|
+
using _listener = addUncaughtErrorListener(scope, listener);
|
|
489
|
+
|
|
490
|
+
dispatch(scope, "error", { error: null });
|
|
491
|
+
dispatch(scope, "unhandledrejection", { reason: "rejected" });
|
|
492
|
+
|
|
493
|
+
assertEqual(
|
|
494
|
+
listener.mock.calls.map((call) => call.arguments[0].message),
|
|
495
|
+
["Defect: null", 'Defect: "rejected"'],
|
|
496
|
+
);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("stops passing errors after disposal", () => {
|
|
500
|
+
const scope = new EventTarget();
|
|
501
|
+
const listener = mock.fn<(error: Error) => void>();
|
|
502
|
+
const uncaughtErrorListener = addUncaughtErrorListener(scope, listener);
|
|
503
|
+
|
|
504
|
+
uncaughtErrorListener[Symbol.dispose]();
|
|
505
|
+
dispatch(scope, "error", { error: new Error("uncaught") });
|
|
506
|
+
dispatch(scope, "unhandledrejection", { reason: new Error("rejected") });
|
|
507
|
+
|
|
508
|
+
assertSame(listener.mock.callCount(), 0);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
453
512
|
test("createWorkerDeps stores console output entries and exposes createMessagePort", () => {
|
|
454
513
|
const deps = createWorkerDeps();
|
|
455
514
|
|
package/src/Worker.ts
CHANGED
|
@@ -18,8 +18,10 @@ import {
|
|
|
18
18
|
assertNonNullable,
|
|
19
19
|
createConsole,
|
|
20
20
|
createConsoleStoreOutput,
|
|
21
|
+
defectToError,
|
|
21
22
|
disposable,
|
|
22
23
|
} from "@evolu/common";
|
|
24
|
+
import type { createRun } from "./Task.ts";
|
|
23
25
|
|
|
24
26
|
/** Creates a {@link Worker} from a Web Worker. */
|
|
25
27
|
export const createWorker = <Input, Output>(
|
|
@@ -159,7 +161,9 @@ export const createWorkerSelf = <Input, Output = never>(
|
|
|
159
161
|
* Creates an Evolu {@link SharedWorkerSelf} from a Web `SharedWorkerGlobalScope`
|
|
160
162
|
* (`self` inside a shared worker).
|
|
161
163
|
*
|
|
162
|
-
* Disposing closes the shared worker scope for all connected clients.
|
|
164
|
+
* Disposing closes the shared worker scope for all connected clients. Browsers
|
|
165
|
+
* never pass a shared worker's uncaught errors to its pages; see
|
|
166
|
+
* {@link addUncaughtErrorListener}.
|
|
163
167
|
*/
|
|
164
168
|
export const createSharedWorkerSelf = <Input, Output = never>(
|
|
165
169
|
nativeSelf: SharedWorkerGlobalScope,
|
|
@@ -271,6 +275,67 @@ export const createOneTabSharedWorkerSelfPolyfill = <Input, Output = never>(
|
|
|
271
275
|
};
|
|
272
276
|
};
|
|
273
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Passes each uncaught error and unhandled rejection of a worker global scope
|
|
280
|
+
* to `listener` as an `Error` from {@link defectToError}.
|
|
281
|
+
*
|
|
282
|
+
* Browsers never pass a shared worker's uncaught errors to a page's error
|
|
283
|
+
* handlers, and no worker passes its unhandled rejections to its page. A shared
|
|
284
|
+
* worker calls this with its global scope, `self`, to send them to its pages,
|
|
285
|
+
* for example over a BroadcastChannel. Defects of a Run from {@link createRun}
|
|
286
|
+
* arrive too, because it reports them with `reportError`, which dispatches
|
|
287
|
+
* "error" on the global scope.
|
|
288
|
+
*
|
|
289
|
+
* The events stay uncanceled, so the browser still logs them, and a dedicated
|
|
290
|
+
* worker still passes its uncaught errors to its page. The listeners stay until
|
|
291
|
+
* disposal, so they also receive errors after a Run's panic.
|
|
292
|
+
*
|
|
293
|
+
* ### Example
|
|
294
|
+
*
|
|
295
|
+
* ```ts
|
|
296
|
+
* import { assertEqual } from "@evolu/common";
|
|
297
|
+
* import { addUncaughtErrorListener } from "@evolu/web";
|
|
298
|
+
*
|
|
299
|
+
* const messages: Array<string> = [];
|
|
300
|
+
* // A worker passes its global scope, `self`.
|
|
301
|
+
* const scope = new EventTarget();
|
|
302
|
+
* using _listener = addUncaughtErrorListener(scope, (error) => {
|
|
303
|
+
* messages.push(error.message);
|
|
304
|
+
* });
|
|
305
|
+
*
|
|
306
|
+
* scope.dispatchEvent(
|
|
307
|
+
* Object.assign(new Event("error"), { error: new Error("unexpected") }),
|
|
308
|
+
* );
|
|
309
|
+
* assertEqual(messages, ["unexpected"]);
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
export const addUncaughtErrorListener = (
|
|
313
|
+
scope: EventTarget,
|
|
314
|
+
listener: (error: Error) => void,
|
|
315
|
+
): Disposable => {
|
|
316
|
+
using disposer = new DisposableStack();
|
|
317
|
+
const { signal } = disposer.adopt(new AbortController(), (controller) => {
|
|
318
|
+
controller.abort();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
scope.addEventListener(
|
|
322
|
+
"error",
|
|
323
|
+
(event) => {
|
|
324
|
+
listener(defectToError((event as ErrorEvent).error));
|
|
325
|
+
},
|
|
326
|
+
{ signal },
|
|
327
|
+
);
|
|
328
|
+
scope.addEventListener(
|
|
329
|
+
"unhandledrejection",
|
|
330
|
+
(event) => {
|
|
331
|
+
listener(defectToError((event as PromiseRejectionEvent).reason));
|
|
332
|
+
},
|
|
333
|
+
{ signal },
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
return disposable({}, disposer);
|
|
337
|
+
};
|
|
338
|
+
|
|
274
339
|
/** Creates deps shared by web worker entry points. */
|
|
275
340
|
export const createWorkerDeps = (): WorkerDeps &
|
|
276
341
|
CreateBroadcastChannelDep &
|
|
@@ -572,6 +572,7 @@ const setupWebEvoluDeps = ({
|
|
|
572
572
|
sqliteSchema: { tables: {}, indexes: [] },
|
|
573
573
|
encryptionKey: testAppOwner.encryptionKey,
|
|
574
574
|
memoryOnly: false,
|
|
575
|
+
sharedWorkerId: createIdFromString<"SharedWorker">("worker"),
|
|
575
576
|
port: createClosableNativePort() as unknown as NativeMessagePort<
|
|
576
577
|
DbWorkerOutput,
|
|
577
578
|
DbWorkerInput
|
|
@@ -4,20 +4,41 @@ declare const self: DedicatedWorkerGlobalScope | SharedWorkerGlobalScope;
|
|
|
4
4
|
import { installPolyfills } from "@evolu/common/polyfills";
|
|
5
5
|
installPolyfills();
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
createUnknownError,
|
|
9
|
+
createWebSocket,
|
|
10
|
+
ok,
|
|
11
|
+
tryAsync,
|
|
12
|
+
waitForAbort,
|
|
13
|
+
} from "@evolu/common";
|
|
8
14
|
import type {
|
|
15
|
+
ConsoleEntryOrError,
|
|
9
16
|
SharedWorkerInput,
|
|
10
17
|
SharedWorkerOutput,
|
|
11
18
|
} from "@evolu/common/local-first";
|
|
12
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
consoleEntryOrErrorBroadcastChannelName,
|
|
21
|
+
initSharedWorker,
|
|
22
|
+
} from "@evolu/common/local-first";
|
|
13
23
|
import type { SharedWorkerUnsupported } from "./Evolu.ts";
|
|
14
24
|
import { createRun } from "../Task.ts";
|
|
15
25
|
import {
|
|
26
|
+
addUncaughtErrorListener,
|
|
27
|
+
createBroadcastChannel,
|
|
16
28
|
createOneTabSharedWorkerSelfPolyfill,
|
|
17
29
|
createSharedWorkerSelf,
|
|
18
30
|
createWorkerDeps,
|
|
19
31
|
} from "../Worker.ts";
|
|
20
32
|
|
|
33
|
+
const errors = createBroadcastChannel<ConsoleEntryOrError>(
|
|
34
|
+
consoleEntryOrErrorBroadcastChannelName,
|
|
35
|
+
);
|
|
36
|
+
// Each tab logs an uncaught error, including a Run defect, and sets it as its
|
|
37
|
+
// evoluError.
|
|
38
|
+
addUncaughtErrorListener(self, (error) => {
|
|
39
|
+
errors.postMessage({ type: "Error", error: createUnknownError(error) });
|
|
40
|
+
});
|
|
41
|
+
|
|
21
42
|
const run = createRun({
|
|
22
43
|
...createWorkerDeps(),
|
|
23
44
|
createWebSocket,
|
|
@@ -29,9 +50,12 @@ const run = createRun({
|
|
|
29
50
|
|
|
30
51
|
void run(async (run) => {
|
|
31
52
|
if ("onconnect" in self) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
// A panic closes the worker, so tabs that connect later start another.
|
|
54
|
+
using workerSelf = createSharedWorkerSelf<
|
|
55
|
+
SharedWorkerInput,
|
|
56
|
+
SharedWorkerOutput
|
|
57
|
+
>(self);
|
|
58
|
+
await using _ = await run.ok(initSharedWorker(workerSelf));
|
|
35
59
|
return await run(waitForAbort);
|
|
36
60
|
}
|
|
37
61
|
|