@evolu/web 3.0.0-next.0 → 3.0.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +138 -57
- package/dist/src/Task.d.ts +4 -13
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +3 -28
- package/dist/src/Worker.d.ts +25 -3
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +232 -12
- package/dist/src/local-first/Db.worker.js +2 -2
- package/dist/src/local-first/Evolu.d.ts +8 -2
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +40 -8
- package/dist/src/local-first/Shared.worker.js +84 -3
- package/package.json +16 -13
- package/src/Sqlite.ts +47 -18
- package/src/Task.ts +5 -40
- package/src/Worker.ts +178 -10
- package/src/local-first/Db.worker.ts +2 -2
- package/src/local-first/Evolu.ts +62 -3
- package/src/local-first/Shared.worker.ts +44 -4
|
@@ -4,13 +4,13 @@ installPolyfills();
|
|
|
4
4
|
import { createRandomBytes } from "@evolu/common";
|
|
5
5
|
import { startDbWorker } from "@evolu/common/local-first";
|
|
6
6
|
import { createWasmSqliteDriver } from "../Sqlite.js";
|
|
7
|
-
import {
|
|
7
|
+
import { createRun } from "../Task.js";
|
|
8
8
|
import { createWorkerDeps, createWorkerSelf } from "../Worker.js";
|
|
9
9
|
// TODO: Disposal.
|
|
10
10
|
const run = createRun({
|
|
11
11
|
...createWorkerDeps(),
|
|
12
12
|
createSqliteDriver: createWasmSqliteDriver,
|
|
13
|
-
|
|
13
|
+
lockManager: globalThis.navigator.locks,
|
|
14
14
|
randomBytes: createRandomBytes(),
|
|
15
15
|
});
|
|
16
16
|
run(startDbWorker(createWorkerSelf(self)));
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ConsoleDep } from "@evolu/common";
|
|
2
2
|
import type { EvoluDeps } from "@evolu/common/local-first";
|
|
3
|
+
export interface SharedWorkerUnsupported {
|
|
4
|
+
readonly type: "SharedWorkerUnsupported";
|
|
5
|
+
}
|
|
6
|
+
export interface SharedWorkerUnsupportedDep {
|
|
7
|
+
readonly onSharedWorkerUnsupported: () => void;
|
|
8
|
+
}
|
|
3
9
|
/** Creates Evolu dependencies for the web platform. */
|
|
4
|
-
export declare const createEvoluDeps: (deps?: Partial<ConsoleDep>) => EvoluDeps;
|
|
10
|
+
export declare const createEvoluDeps: (deps?: Partial<ConsoleDep> & Partial<SharedWorkerUnsupportedDep>) => EvoluDeps;
|
|
5
11
|
//# sourceMappingURL=Evolu.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Evolu.d.ts","sourceRoot":"","sources":["../../../src/local-first/Evolu.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"Evolu.d.ts","sourceRoot":"","sources":["../../../src/local-first/Evolu.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,KAAK,EAIV,SAAS,EAGV,MAAM,2BAA2B,CAAC;AAkBnC,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;CAC1C;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,yBAAyB,EAAE,MAAM,IAAI,CAAC;CAChD;AAED,uDAAuD;AACvD,eAAO,MAAM,eAAe,GAC1B,OAAM,OAAO,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,0BAA0B,CAAM,KACnE,SAkEF,CAAC"}
|
|
@@ -1,24 +1,56 @@
|
|
|
1
|
+
import { exhaustiveCheck } from "@evolu/common";
|
|
1
2
|
import { createEvoluDeps as createCommonEvoluDeps } from "@evolu/common/local-first";
|
|
2
3
|
import { reloadApp } from "../Platform.js";
|
|
3
|
-
import { createMessageChannel, createSharedWorker, createWorker, } from "../Worker.js";
|
|
4
|
-
// // TODO: Redesign.
|
|
5
|
-
// // eslint-disable-next-line evolu/require-pure-annotation
|
|
6
|
-
// export const localAuth = createLocalAuth({
|
|
7
|
-
// randomBytes: createRandomBytes(),
|
|
8
|
-
// secureStorage: createWebAuthnStore({ randomBytes: createRandomBytes() }),
|
|
9
|
-
// });
|
|
4
|
+
import { createBroadcastChannel, createMessageChannel, createSharedWorker, createWorker, installOneTabSharedWorkerPolyfill, } from "../Worker.js";
|
|
10
5
|
/** Creates Evolu dependencies for the web platform. */
|
|
11
6
|
export const createEvoluDeps = (deps = {}) => {
|
|
7
|
+
installOneTabSharedWorkerPolyfill();
|
|
12
8
|
const createDbWorker = () => createWorker(new Worker(new URL("Db.worker.js", import.meta.url), {
|
|
13
9
|
type: "module",
|
|
14
10
|
}));
|
|
15
|
-
const
|
|
11
|
+
const webSharedWorker = createSharedWorker(new SharedWorker(new URL("Shared.worker.js", import.meta.url), {
|
|
16
12
|
type: "module",
|
|
17
13
|
}));
|
|
14
|
+
let onSharedWorkerMessage = null;
|
|
15
|
+
const sharedWorker = {
|
|
16
|
+
...webSharedWorker,
|
|
17
|
+
port: {
|
|
18
|
+
...webSharedWorker.port,
|
|
19
|
+
get onMessage() {
|
|
20
|
+
return onSharedWorkerMessage;
|
|
21
|
+
},
|
|
22
|
+
set onMessage(fn) {
|
|
23
|
+
onSharedWorkerMessage = fn;
|
|
24
|
+
webSharedWorker.port.onMessage = fn
|
|
25
|
+
? (message) => {
|
|
26
|
+
switch (message.type) {
|
|
27
|
+
case "DbWorkerInit": {
|
|
28
|
+
fn(message);
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
case "SharedWorkerUnsupported": {
|
|
32
|
+
if (deps.onSharedWorkerUnsupported) {
|
|
33
|
+
deps.onSharedWorkerUnsupported();
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
globalThis.alert("This browser supports Evolu in one tab only. Close this tab and use the already open tab.");
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
default:
|
|
41
|
+
exhaustiveCheck(message);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
: null;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
18
48
|
return createCommonEvoluDeps({
|
|
19
49
|
...deps,
|
|
20
50
|
createDbWorker,
|
|
51
|
+
createBroadcastChannel,
|
|
21
52
|
createMessageChannel,
|
|
53
|
+
lockManager: globalThis.navigator.locks,
|
|
22
54
|
reloadApp,
|
|
23
55
|
sharedWorker,
|
|
24
56
|
});
|
|
@@ -1,13 +1,94 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
1
53
|
/// <reference lib="webworker" />
|
|
2
54
|
import { installPolyfills } from "@evolu/common/polyfills";
|
|
3
55
|
installPolyfills();
|
|
4
|
-
import { createWebSocket } from "@evolu/common";
|
|
56
|
+
import { createWebSocket, ok } from "@evolu/common";
|
|
5
57
|
import { initSharedWorker } from "@evolu/common/local-first";
|
|
6
58
|
import { createRun } from "../Task.js";
|
|
7
|
-
import { createSharedWorkerSelf, createWorkerDeps } from "../Worker.js";
|
|
59
|
+
import { createOneTabSharedWorkerSelfPolyfill, createSharedWorkerSelf, createWorkerDeps, } from "../Worker.js";
|
|
8
60
|
// No disposal (`await using`) is needed — a SharedWorker lives forever.
|
|
9
61
|
const run = createRun({
|
|
10
62
|
...createWorkerDeps(),
|
|
11
63
|
createWebSocket,
|
|
64
|
+
lockManager: globalThis.navigator.locks,
|
|
65
|
+
});
|
|
66
|
+
void run(async (run) => {
|
|
67
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
68
|
+
try {
|
|
69
|
+
if ("onconnect" in self) {
|
|
70
|
+
void run(initSharedWorker(createSharedWorkerSelf(self)));
|
|
71
|
+
return ok();
|
|
72
|
+
}
|
|
73
|
+
const workerSelf = __addDisposableResource(env_1, createOneTabSharedWorkerSelfPolyfill(self), false);
|
|
74
|
+
await globalThis.navigator.locks.request("evolu-one-tab-sharedworker-polyfill", { ifAvailable: true, mode: "exclusive" }, async (lock) => {
|
|
75
|
+
if (!lock) {
|
|
76
|
+
const message = {
|
|
77
|
+
type: "SharedWorkerUnsupported",
|
|
78
|
+
};
|
|
79
|
+
self.postMessage(message);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
void run(initSharedWorker(workerSelf));
|
|
83
|
+
await Promise.withResolvers().promise;
|
|
84
|
+
});
|
|
85
|
+
return ok();
|
|
86
|
+
}
|
|
87
|
+
catch (e_1) {
|
|
88
|
+
env_1.error = e_1;
|
|
89
|
+
env_1.hasError = true;
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
__disposeResources(env_1);
|
|
93
|
+
}
|
|
12
94
|
});
|
|
13
|
-
run(initSharedWorker(createSharedWorkerSelf(self)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evolu/web",
|
|
3
|
-
"version": "3.0.0-next.
|
|
3
|
+
"version": "3.0.0-next.1",
|
|
4
4
|
"description": "Evolu for web",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"evolu",
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
],
|
|
9
9
|
"author": "Daniel Steigerwald <daniel@steigerwald.cz>",
|
|
10
10
|
"license": "MIT",
|
|
11
|
-
"repository":
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/evoluhq/evolu.git"
|
|
14
|
+
},
|
|
12
15
|
"bugs": {
|
|
13
16
|
"url": "https://github.com/evoluhq/evolu/issues"
|
|
14
17
|
},
|
|
@@ -27,22 +30,18 @@
|
|
|
27
30
|
"src/**",
|
|
28
31
|
"README.md"
|
|
29
32
|
],
|
|
30
|
-
"scripts": {
|
|
31
|
-
"build": "tsc --build tsconfig.build.json",
|
|
32
|
-
"prepack": "npm run build",
|
|
33
|
-
"format": "prettier --write \"src/*.{ts,tsx,md}\""
|
|
34
|
-
},
|
|
35
33
|
"dependencies": {
|
|
36
34
|
"@evolu/sqlite-wasm": "2.2.4",
|
|
37
35
|
"idb-keyval": "^6.2.2"
|
|
38
36
|
},
|
|
39
37
|
"devDependencies": {
|
|
40
|
-
"@
|
|
41
|
-
"@evolu/typescript-config": "*",
|
|
38
|
+
"@types/node": "^24.10.9",
|
|
42
39
|
"@types/sharedworker": "^0.0.221",
|
|
43
40
|
"@types/web-locks-api": "^0.0.5",
|
|
44
|
-
"typescript": "^
|
|
45
|
-
"user-agent-data-types": "^0.4.2"
|
|
41
|
+
"typescript": "^6",
|
|
42
|
+
"user-agent-data-types": "^0.4.2",
|
|
43
|
+
"@evolu/common": "8.0.0-next.5",
|
|
44
|
+
"@evolu/typescript-config": "0.0.2"
|
|
46
45
|
},
|
|
47
46
|
"peerDependencies": {
|
|
48
47
|
"@evolu/common": "^8.0.0-next.0"
|
|
@@ -53,5 +52,9 @@
|
|
|
53
52
|
"engines": {
|
|
54
53
|
"node": ">=24.0.0"
|
|
55
54
|
},
|
|
56
|
-
"sideEffects": false
|
|
57
|
-
|
|
55
|
+
"sideEffects": false,
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc --build tsconfig.build.json",
|
|
58
|
+
"format": "prettier --write \"src/*.{ts,tsx,md}\""
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/Sqlite.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { bytesToHex, createPreparedStatementsCache, ok } from "@evolu/common";
|
|
|
3
3
|
import sqlite3InitModule, {
|
|
4
4
|
type Database,
|
|
5
5
|
type PreparedStatement,
|
|
6
|
+
type SAHPoolUtil,
|
|
6
7
|
} from "@evolu/sqlite-wasm";
|
|
7
8
|
|
|
8
9
|
// @ts-expect-error Missing types.
|
|
@@ -23,26 +24,51 @@ globalThis.sqlite3ApiConfig = {
|
|
|
23
24
|
// Init ASAP.
|
|
24
25
|
const sqlite3Promise = sqlite3InitModule();
|
|
25
26
|
|
|
27
|
+
const fileName = "evolu1.db";
|
|
28
|
+
|
|
26
29
|
export const createWasmSqliteDriver: CreateSqliteDriver =
|
|
27
30
|
(name, options) => async () => {
|
|
28
31
|
const sqlite3 = await sqlite3Promise;
|
|
29
|
-
// This is used to make OPFS default vfs for multipleciphers
|
|
30
|
-
// @ts-expect-error Missing types (update @evolu/sqlite-wasm types)
|
|
31
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
32
|
-
sqlite3.capi.sqlite3mc_vfs_create("opfs", 1);
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
using disposer = new DisposableStack();
|
|
34
|
+
const useDatabase = (database: Database): Database =>
|
|
35
|
+
disposer.adopt(database, (database) => {
|
|
36
|
+
database.close();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
let deleteDatabaseFile = false;
|
|
40
|
+
const createOpfsSAHPoolVfs = async (
|
|
41
|
+
options: Parameters<typeof sqlite3.installOpfsSAHPoolVfs>[0],
|
|
42
|
+
): Promise<SAHPoolUtil> => {
|
|
43
|
+
const pool = await sqlite3.installOpfsSAHPoolVfs(options);
|
|
44
|
+
if (pool.isPaused()) await pool.unpauseVfs();
|
|
45
|
+
disposer.defer(() => {
|
|
46
|
+
if (deleteDatabaseFile) pool.unlink(`/${fileName}`);
|
|
47
|
+
pool.pauseVfs();
|
|
48
|
+
});
|
|
49
|
+
return pool;
|
|
50
|
+
};
|
|
51
|
+
|
|
35
52
|
let db: Database;
|
|
53
|
+
|
|
36
54
|
switch (options?.mode) {
|
|
37
55
|
case "memory":
|
|
38
|
-
db = new sqlite3.oo1.DB(":memory:");
|
|
56
|
+
db = useDatabase(new sqlite3.oo1.DB(":memory:"));
|
|
39
57
|
break;
|
|
58
|
+
|
|
40
59
|
case "encrypted": {
|
|
41
|
-
|
|
60
|
+
// MultipleCiphers encryption requires its VFS wrapper for OPFS SAH-pool.
|
|
61
|
+
// @ts-expect-error Missing types (update @evolu/sqlite-wasm types)
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
63
|
+
sqlite3.capi.sqlite3mc_vfs_create("opfs", 1);
|
|
64
|
+
const pool = await createOpfsSAHPoolVfs({
|
|
42
65
|
directory: `.${name}`,
|
|
43
66
|
});
|
|
44
|
-
db =
|
|
45
|
-
|
|
67
|
+
db = useDatabase(
|
|
68
|
+
new pool.OpfsSAHPoolDb(
|
|
69
|
+
// SQLite normalizes this URI filename to SAH-pool path "/evolu1.db".
|
|
70
|
+
`file:${fileName}?vfs=multipleciphers-opfs-sahpool`,
|
|
71
|
+
),
|
|
46
72
|
);
|
|
47
73
|
db.exec(`
|
|
48
74
|
PRAGMA cipher = 'sqlcipher';
|
|
@@ -50,17 +76,14 @@ export const createWasmSqliteDriver: CreateSqliteDriver =
|
|
|
50
76
|
`);
|
|
51
77
|
break;
|
|
52
78
|
}
|
|
79
|
+
|
|
53
80
|
default: {
|
|
54
|
-
const pool = await
|
|
55
|
-
db = new pool.OpfsSAHPoolDb(
|
|
81
|
+
const pool = await createOpfsSAHPoolVfs({ name });
|
|
82
|
+
db = useDatabase(new pool.OpfsSAHPoolDb(`file:${fileName}`));
|
|
56
83
|
}
|
|
57
84
|
}
|
|
58
85
|
|
|
59
|
-
|
|
60
|
-
db.close();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const cache = stack.use(
|
|
86
|
+
const cache = disposer.use(
|
|
64
87
|
createPreparedStatementsCache<PreparedStatement>(
|
|
65
88
|
(sql) => db.prepare(sql),
|
|
66
89
|
(statement) => {
|
|
@@ -69,6 +92,8 @@ export const createWasmSqliteDriver: CreateSqliteDriver =
|
|
|
69
92
|
),
|
|
70
93
|
);
|
|
71
94
|
|
|
95
|
+
const disposables = disposer.move();
|
|
96
|
+
|
|
72
97
|
return ok({
|
|
73
98
|
exec: (query) => {
|
|
74
99
|
const prepared = cache.get(query);
|
|
@@ -101,9 +126,13 @@ export const createWasmSqliteDriver: CreateSqliteDriver =
|
|
|
101
126
|
|
|
102
127
|
export: () => sqlite3.capi.sqlite3_js_db_export(db),
|
|
103
128
|
|
|
129
|
+
deleteDatabase: () => {
|
|
130
|
+
deleteDatabaseFile = true;
|
|
131
|
+
disposables.dispose();
|
|
132
|
+
},
|
|
133
|
+
|
|
104
134
|
[Symbol.dispose]: () => {
|
|
105
|
-
|
|
106
|
-
stack.dispose();
|
|
135
|
+
disposables.dispose();
|
|
107
136
|
},
|
|
108
137
|
});
|
|
109
138
|
};
|
package/src/Task.ts
CHANGED
|
@@ -7,46 +7,11 @@
|
|
|
7
7
|
import {
|
|
8
8
|
createRun as createCommonRun,
|
|
9
9
|
createUnknownError,
|
|
10
|
-
ok,
|
|
11
10
|
type CreateRun,
|
|
12
|
-
type LeaderLock,
|
|
13
11
|
type Run,
|
|
14
|
-
type
|
|
12
|
+
type RunDefaultDeps,
|
|
15
13
|
} from "@evolu/common";
|
|
16
14
|
|
|
17
|
-
/**
|
|
18
|
-
* Creates a {@link LeaderLock} backed by the Web Locks API.
|
|
19
|
-
*
|
|
20
|
-
* Waiting for the browser lock is intentionally unabortable. If a caller starts
|
|
21
|
-
* waiting and its {@link Run} or fiber is later aborted, the underlying Web
|
|
22
|
-
* Locks request keeps waiting until the browser grants the lock. Only the
|
|
23
|
-
* returned lease releases it.
|
|
24
|
-
*/
|
|
25
|
-
export const createLeaderLock = (): LeaderLock => ({
|
|
26
|
-
lock: (name) => async () => {
|
|
27
|
-
const acquired = Promise.withResolvers<void>();
|
|
28
|
-
const release = Promise.withResolvers<void>();
|
|
29
|
-
|
|
30
|
-
void globalThis.navigator.locks.request(
|
|
31
|
-
`evolu-leaderlock-${name}`,
|
|
32
|
-
{ mode: "exclusive" },
|
|
33
|
-
async () => {
|
|
34
|
-
acquired.resolve();
|
|
35
|
-
await release.promise;
|
|
36
|
-
},
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
await acquired.promise;
|
|
40
|
-
|
|
41
|
-
return ok({
|
|
42
|
-
[Symbol.asyncDispose]: async () => {
|
|
43
|
-
release.resolve();
|
|
44
|
-
return Promise.resolve();
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
|
|
50
15
|
/**
|
|
51
16
|
* Creates {@link Run} for the browser with global error handling.
|
|
52
17
|
*
|
|
@@ -63,14 +28,14 @@ export const createLeaderLock = (): LeaderLock => ({
|
|
|
63
28
|
* });
|
|
64
29
|
*
|
|
65
30
|
* await using run = createRun({ console });
|
|
66
|
-
* await using
|
|
31
|
+
* await using disposer = new AsyncDisposableStack();
|
|
67
32
|
*
|
|
68
|
-
*
|
|
33
|
+
* disposer.use(await run.orThrow(startApp()));
|
|
69
34
|
* ```
|
|
70
35
|
*/
|
|
71
|
-
export const createRun: CreateRun<
|
|
36
|
+
export const createRun: CreateRun<RunDefaultDeps> = <D>(
|
|
72
37
|
deps?: D,
|
|
73
|
-
): Run<
|
|
38
|
+
): Run<RunDefaultDeps & D> => {
|
|
74
39
|
const run = createCommonRun(deps);
|
|
75
40
|
const console = run.deps.console.child("global");
|
|
76
41
|
|
package/src/Worker.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
BroadcastChannel,
|
|
3
|
+
CreateBroadcastChannel,
|
|
4
|
+
CreateBroadcastChannelDep,
|
|
5
|
+
CreateMessageChannelDep,
|
|
2
6
|
CreateMessagePort,
|
|
3
7
|
MessageChannel,
|
|
4
8
|
MessagePort,
|
|
@@ -10,14 +14,49 @@ import type {
|
|
|
10
14
|
WorkerDeps,
|
|
11
15
|
WorkerSelf,
|
|
12
16
|
} from "@evolu/common";
|
|
13
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
assert,
|
|
19
|
+
assertNotDisposed,
|
|
20
|
+
createConsole,
|
|
21
|
+
createConsoleStoreOutput,
|
|
22
|
+
} from "@evolu/common";
|
|
14
23
|
|
|
15
24
|
/** Creates a {@link Worker} from a Web Worker. */
|
|
16
25
|
export const createWorker = <Input, Output>(
|
|
17
26
|
nativeWorker: globalThis.Worker,
|
|
18
27
|
): Worker<Input, Output> => wrap(nativeWorker);
|
|
19
28
|
|
|
20
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Installs a one-tab SharedWorker polyfill backed by a Web Worker.
|
|
31
|
+
*
|
|
32
|
+
* Use it before constructing a SharedWorker in browsers without native
|
|
33
|
+
* SharedWorker support.
|
|
34
|
+
*/
|
|
35
|
+
export const installOneTabSharedWorkerPolyfill = (): void => {
|
|
36
|
+
if (typeof globalThis.SharedWorker === "function") return;
|
|
37
|
+
|
|
38
|
+
Object.defineProperty(globalThis, "SharedWorker", {
|
|
39
|
+
configurable: true,
|
|
40
|
+
writable: true,
|
|
41
|
+
value: class {
|
|
42
|
+
readonly port: globalThis.MessagePort;
|
|
43
|
+
|
|
44
|
+
constructor(scriptURL: string | URL, options?: WorkerOptions) {
|
|
45
|
+
this.port = new globalThis.Worker(
|
|
46
|
+
scriptURL,
|
|
47
|
+
options,
|
|
48
|
+
) as unknown as globalThis.MessagePort;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a {@link SharedWorker} from a Web SharedWorker.
|
|
56
|
+
*
|
|
57
|
+
* Call {@link installOneTabSharedWorkerPolyfill} first to support browsers
|
|
58
|
+
* without native SharedWorker support.
|
|
59
|
+
*/
|
|
21
60
|
export const createSharedWorker = <Input, Output = never>(
|
|
22
61
|
nativeSharedWorker: globalThis.SharedWorker,
|
|
23
62
|
): SharedWorker<Input, Output> => {
|
|
@@ -44,14 +83,16 @@ export const createMessageChannel = <Input, Output = never>(): MessageChannel<
|
|
|
44
83
|
Output
|
|
45
84
|
> => {
|
|
46
85
|
const channel = new globalThis.MessageChannel();
|
|
47
|
-
|
|
86
|
+
using disposer = new DisposableStack();
|
|
87
|
+
|
|
88
|
+
const port1 = disposer.use(wrap<Input, Output>(channel.port1));
|
|
89
|
+
const port2 = disposer.use(wrap<Output, Input>(channel.port2));
|
|
90
|
+
const disposables = disposer.move();
|
|
48
91
|
|
|
49
92
|
return {
|
|
50
|
-
port1
|
|
51
|
-
port2
|
|
52
|
-
[Symbol.dispose]: () =>
|
|
53
|
-
stack.dispose();
|
|
54
|
-
},
|
|
93
|
+
port1,
|
|
94
|
+
port2,
|
|
95
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
55
96
|
};
|
|
56
97
|
};
|
|
57
98
|
|
|
@@ -63,6 +104,45 @@ export const createMessageChannel = <Input, Output = never>(): MessageChannel<
|
|
|
63
104
|
export const createMessagePort: CreateMessagePort = (nativePort) =>
|
|
64
105
|
wrap(nativePort as unknown as globalThis.MessagePort);
|
|
65
106
|
|
|
107
|
+
/** Creates an Evolu {@link BroadcastChannel} from a Web BroadcastChannel. */
|
|
108
|
+
export const createBroadcastChannel: CreateBroadcastChannel = <
|
|
109
|
+
Input,
|
|
110
|
+
Output = Input,
|
|
111
|
+
>(
|
|
112
|
+
name: string,
|
|
113
|
+
): BroadcastChannel<Input, Output> => {
|
|
114
|
+
const nativeBroadcastChannel = new globalThis.BroadcastChannel(name);
|
|
115
|
+
using disposer = new DisposableStack();
|
|
116
|
+
|
|
117
|
+
disposer.defer(() => {
|
|
118
|
+
nativeBroadcastChannel.onmessage = null;
|
|
119
|
+
nativeBroadcastChannel.close();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const disposables = disposer.move();
|
|
123
|
+
let onMessageHandler: ((message: Output) => void) | null = null;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
postMessage: (message) => {
|
|
127
|
+
assertNotDisposed(disposables);
|
|
128
|
+
nativeBroadcastChannel.postMessage(message);
|
|
129
|
+
},
|
|
130
|
+
get onMessage() {
|
|
131
|
+
return disposables.disposed ? null : onMessageHandler;
|
|
132
|
+
},
|
|
133
|
+
set onMessage(fn) {
|
|
134
|
+
if (disposables.disposed) return;
|
|
135
|
+
onMessageHandler = fn;
|
|
136
|
+
nativeBroadcastChannel.onmessage = fn
|
|
137
|
+
? (event: MessageEvent<Output>) => {
|
|
138
|
+
fn(event.data);
|
|
139
|
+
}
|
|
140
|
+
: null;
|
|
141
|
+
},
|
|
142
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
66
146
|
/**
|
|
67
147
|
* Creates an Evolu {@link WorkerSelf} from a Web `DedicatedWorkerGlobalScope`
|
|
68
148
|
* (`self` inside a dedicated worker).
|
|
@@ -99,14 +179,102 @@ export const createSharedWorkerSelf = <Input, Output = never>(
|
|
|
99
179
|
return self;
|
|
100
180
|
};
|
|
101
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Creates a one-tab {@link SharedWorkerSelf} polyfill for browsers without
|
|
184
|
+
* native SharedWorker support.
|
|
185
|
+
*
|
|
186
|
+
* Delivers one synthetic `onConnect` port, queues startup messages until
|
|
187
|
+
* `onMessage` is set, and does not share state across tabs.
|
|
188
|
+
*/
|
|
189
|
+
export const createOneTabSharedWorkerSelfPolyfill = <Input, Output = never>(
|
|
190
|
+
nativeSelf: globalThis.DedicatedWorkerGlobalScope,
|
|
191
|
+
): SharedWorkerSelf<Input, Output> => {
|
|
192
|
+
using disposer = new DisposableStack();
|
|
193
|
+
|
|
194
|
+
let onConnectHandler: ((port: MessagePort<Output, Input>) => void) | null =
|
|
195
|
+
null;
|
|
196
|
+
let onMessageHandler: ((message: Input) => void) | null = null;
|
|
197
|
+
const messages: Array<Input> = [];
|
|
198
|
+
let connected = false;
|
|
199
|
+
|
|
200
|
+
disposer.defer(() => {
|
|
201
|
+
messages.length = 0;
|
|
202
|
+
onConnectHandler = null;
|
|
203
|
+
onMessageHandler = null;
|
|
204
|
+
nativeSelf.onmessage = null;
|
|
205
|
+
nativeSelf.close();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const disposables = disposer.move();
|
|
209
|
+
|
|
210
|
+
nativeSelf.onmessage = (event: MessageEvent<Input>) => {
|
|
211
|
+
if (disposables.disposed) return;
|
|
212
|
+
|
|
213
|
+
const handler = onMessageHandler;
|
|
214
|
+
if (handler) handler(event.data);
|
|
215
|
+
else messages.push(event.data);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const flushMessages = (): void => {
|
|
219
|
+
for (const message of messages.splice(0)) {
|
|
220
|
+
const handler = onMessageHandler;
|
|
221
|
+
if (!handler) return;
|
|
222
|
+
handler(message);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const port: MessagePort<Output, Input> = {
|
|
227
|
+
postMessage: (message, transfer) => {
|
|
228
|
+
if (disposables.disposed) return;
|
|
229
|
+
if (transfer == null) nativeSelf.postMessage(message);
|
|
230
|
+
else nativeSelf.postMessage(message, [...transfer]);
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
get onMessage() {
|
|
234
|
+
return disposables.disposed ? null : onMessageHandler;
|
|
235
|
+
},
|
|
236
|
+
set onMessage(fn) {
|
|
237
|
+
if (disposables.disposed) return;
|
|
238
|
+
onMessageHandler = fn;
|
|
239
|
+
if (fn) flushMessages();
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
native: nativeSelf as unknown as NativeMessagePort<Output, Input>,
|
|
243
|
+
|
|
244
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const connect = (): void => {
|
|
248
|
+
if (connected || !onConnectHandler) return;
|
|
249
|
+
connected = true;
|
|
250
|
+
onConnectHandler(port);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
get onConnect() {
|
|
255
|
+
return onConnectHandler;
|
|
256
|
+
},
|
|
257
|
+
set onConnect(fn) {
|
|
258
|
+
if (disposables.disposed) return;
|
|
259
|
+
onConnectHandler = fn;
|
|
260
|
+
connect();
|
|
261
|
+
},
|
|
262
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
|
|
102
266
|
/** Creates deps shared by web worker entry points. */
|
|
103
|
-
export const createWorkerDeps = (): WorkerDeps
|
|
267
|
+
export const createWorkerDeps = (): WorkerDeps &
|
|
268
|
+
CreateBroadcastChannelDep &
|
|
269
|
+
CreateMessageChannelDep => {
|
|
104
270
|
const consoleStoreOutput = createConsoleStoreOutput();
|
|
105
271
|
const console = createConsole({ output: consoleStoreOutput });
|
|
106
272
|
|
|
107
273
|
return {
|
|
108
274
|
console,
|
|
109
275
|
consoleStoreOutputEntry: consoleStoreOutput.entry,
|
|
276
|
+
createBroadcastChannel,
|
|
277
|
+
createMessageChannel,
|
|
110
278
|
createMessagePort,
|
|
111
279
|
};
|
|
112
280
|
};
|
|
@@ -144,7 +312,7 @@ const wrap = <Input, Output>(
|
|
|
144
312
|
|
|
145
313
|
[Symbol.dispose]: () => {
|
|
146
314
|
native.onmessage = null;
|
|
147
|
-
if (
|
|
315
|
+
if ("terminate" in native) native.terminate();
|
|
148
316
|
else native.close();
|
|
149
317
|
},
|
|
150
318
|
};
|