@evolu/web 2.4.0 → 3.0.0-next.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.
Files changed (58) hide show
  1. package/README.md +3 -1
  2. package/dist/src/Platform.d.ts +3 -0
  3. package/dist/src/Platform.d.ts.map +1 -0
  4. package/dist/src/{local-first/Platform.js → Platform.js} +1 -1
  5. package/dist/src/Sqlite.d.ts +3 -0
  6. package/dist/src/Sqlite.d.ts.map +1 -0
  7. package/dist/src/{WasmSqliteDriver.js → Sqlite.js} +35 -39
  8. package/dist/src/Task.d.ts +38 -0
  9. package/dist/src/Task.d.ts.map +1 -0
  10. package/dist/src/Task.js +63 -0
  11. package/dist/src/Worker.d.ts +38 -0
  12. package/dist/src/Worker.d.ts.map +1 -0
  13. package/dist/src/Worker.js +108 -0
  14. package/dist/src/index.d.ts +3 -3
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +3 -3
  17. package/dist/src/local-first/Db.worker.js +13 -10
  18. package/dist/src/local-first/Evolu.d.ts +5 -0
  19. package/dist/src/local-first/Evolu.d.ts.map +1 -0
  20. package/dist/src/local-first/Evolu.js +25 -0
  21. package/dist/src/local-first/LocalAuth.d.ts +2 -3
  22. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  23. package/dist/src/local-first/LocalAuth.js +60 -71
  24. package/dist/src/local-first/Shared.worker.d.ts +2 -0
  25. package/dist/src/local-first/Shared.worker.d.ts.map +1 -0
  26. package/dist/src/local-first/Shared.worker.js +13 -0
  27. package/dist/src/local-first/index.d.ts +1 -3
  28. package/dist/src/local-first/index.d.ts.map +1 -1
  29. package/dist/src/local-first/index.js +1 -20
  30. package/package.json +17 -20
  31. package/src/Platform.ts +9 -0
  32. package/src/Sqlite.ts +109 -0
  33. package/src/Task.ts +94 -0
  34. package/src/Worker.ts +151 -0
  35. package/src/index.ts +3 -3
  36. package/src/local-first/Db.worker.ts +16 -16
  37. package/src/local-first/Evolu.ts +45 -0
  38. package/src/local-first/LocalAuth.ts +93 -105
  39. package/src/local-first/Shared.worker.ts +18 -0
  40. package/src/local-first/index.ts +1 -41
  41. package/LICENSE +0 -21
  42. package/dist/src/SharedWebWorker.d.ts +0 -30
  43. package/dist/src/SharedWebWorker.d.ts.map +0 -1
  44. package/dist/src/SharedWebWorker.js +0 -104
  45. package/dist/src/WasmSqliteDriver.d.ts +0 -3
  46. package/dist/src/WasmSqliteDriver.d.ts.map +0 -1
  47. package/dist/src/WebWorker.d.ts +0 -17
  48. package/dist/src/WebWorker.d.ts.map +0 -1
  49. package/dist/src/WebWorker.js +0 -42
  50. package/dist/src/local-first/Platform.d.ts +0 -3
  51. package/dist/src/local-first/Platform.d.ts.map +0 -1
  52. package/dist/test/SharedWebWorker.test.d.ts +0 -2
  53. package/dist/test/SharedWebWorker.test.d.ts.map +0 -1
  54. package/dist/test/SharedWebWorker.test.js +0 -248
  55. package/src/SharedWebWorker.ts +0 -149
  56. package/src/WasmSqliteDriver.ts +0 -118
  57. package/src/WebWorker.ts +0 -54
  58. package/src/local-first/Platform.ts +0 -9
@@ -1,6 +1,5 @@
1
- import { set, get, del, keys, clear, createStore } from "idb-keyval";
2
- import { createSlip21, utf8ToBytes, bytesToUtf8, base64UrlToUint8Array, uint8ArrayToBase64Url, EncryptionKey, Base64Url, } from "@evolu/common";
3
- /** @experimental */
1
+ import { Base64Url, base64UrlToUint8Array, bytesToUtf8, createSlip21, decryptWithXChaCha20Poly1305, EncryptionKey, encryptWithXChaCha20Poly1305, Entropy24, uint8ArrayToBase64Url, utf8ToBytes, XChaCha20Poly1305Ciphertext, } from "@evolu/common";
2
+ import { clear, createStore, del, get, keys, set } from "idb-keyval";
4
3
  export const createWebAuthnStore = (deps) => ({
5
4
  setItem: async (key, value, options) => {
6
5
  if (options?.accessControl === "none") {
@@ -38,7 +37,7 @@ export const createWebAuthnStore = (deps) => ({
38
37
  const credential = await getCredential(deps)(data.credentialId, options?.relyingPartyID, options?.webAuthnUserVerification);
39
38
  const credentialSeed = extractSeedFromCredential(credential);
40
39
  const encryptionKey = deriveEncryptionKey(credentialSeed);
41
- const authResultVal = decryptAuthResult(deps)(data, encryptionKey);
40
+ const authResultVal = decryptAuthResult(data, encryptionKey);
42
41
  if (!authResultVal) {
43
42
  return null;
44
43
  }
@@ -81,18 +80,14 @@ export const createWebAuthnStore = (deps) => ({
81
80
  * Create default metadata for backwards compatibility with items that don't
82
81
  * have stored metadata.
83
82
  */
84
- const createMetadata = (isSecure = true) => {
85
- return {
86
- backend: "keychain",
87
- accessControl: isSecure ? "biometryCurrentSet" : "none",
88
- securityLevel: isSecure ? "biometry" : "software",
89
- timestamp: Date.now(),
90
- };
91
- };
83
+ const createMetadata = (isSecure = true) => ({
84
+ backend: "keychain",
85
+ accessControl: isSecure ? "biometryCurrentSet" : "none",
86
+ securityLevel: isSecure ? "biometry" : "software",
87
+ timestamp: Date.now(),
88
+ });
92
89
  /** Get storage key for owner ID. (supports namespaces via prefix) */
93
- const getStore = (prefix = "default") => {
94
- return createStore(prefix, "evolu-auth");
95
- };
90
+ const getStore = (prefix = "default") => createStore(prefix, "evolu-auth");
96
91
  const createCredential = (deps) => async (username, seed, relyingPartyID, relyingPartyName, userVerification, authenticatorAttachment) => {
97
92
  const options = createCredentialCreationOptions(deps)(username, seed, relyingPartyID, relyingPartyName, userVerification, authenticatorAttachment);
98
93
  const credential = (await navigator.credentials.create(options));
@@ -116,78 +111,72 @@ const extractSeedFromCredential = (credential) => {
116
111
  }
117
112
  return new Uint8Array(response.userHandle);
118
113
  };
119
- const createCredentialCreationOptions = (deps) => (username, seed, relyingPartyID, relyingPartyName, userVerification, authenticatorAttachment) => {
120
- return {
121
- publicKey: {
122
- challenge: generateSeed(deps)(),
123
- rp: {
124
- id: relyingPartyID ?? document.location.hostname,
125
- name: relyingPartyName ?? "Evolu",
126
- },
127
- user: {
128
- id: seed,
129
- name: username,
130
- displayName: username,
131
- },
132
- pubKeyCredParams: [
133
- { type: "public-key", alg: -8 }, // Ed25519
134
- { type: "public-key", alg: -7 }, // ES256
135
- { type: "public-key", alg: -257 }, // RS256
136
- ],
137
- attestation: "none",
138
- authenticatorSelection: {
139
- // - "platform": Uses the platform's built-in authenticator.
140
- // - "cross-platform": Uses a device specific authenticator (yubikey, fido2, etc.)
141
- authenticatorAttachment: authenticatorAttachment ?? "platform",
142
- // - "discouraged": Only User Presence is needed.
143
- // - "preferred": User Verification is preferred but not required. Falls back to User Presence.
144
- // - "required": User Verification MUST occur (biometrics/PIN). Clients may silently downgrade to User Presence only.
145
- userVerification: userVerification ?? "required",
146
- // - "discouraged": Server-side credential is preferable, but will accept client-side discoverable credential.
147
- // - "preferred": Relying Party strongly prefers client-side discoverable credential but will accept server-side credential.
148
- // - "required": Client-side discoverable credential MUST be created, error if it can't be created.
149
- residentKey: "required",
150
- // Included for backwards compatibility. Deprecated in favor of residentKey (true = "required")
151
- requireResidentKey: true,
152
- },
114
+ const createCredentialCreationOptions = (deps) => (username, seed, relyingPartyID, relyingPartyName, userVerification, authenticatorAttachment) => ({
115
+ publicKey: {
116
+ challenge: generateSeed(deps)(),
117
+ rp: {
118
+ id: relyingPartyID ?? document.location.hostname,
119
+ name: relyingPartyName ?? "Evolu",
153
120
  },
154
- };
155
- };
156
- const createCredentialRequestOptions = (deps) => (credentialId, relyingPartyID, userVerification) => {
157
- return {
158
- publicKey: {
159
- challenge: generateSeed(deps)(),
160
- rpId: relyingPartyID ?? document.location.hostname,
161
- userVerification: userVerification ?? "preferred",
162
- allowCredentials: [
163
- {
164
- type: "public-key",
165
- id: base64UrlToUint8Array(Base64Url.orThrow(credentialId)),
166
- },
167
- ],
121
+ user: {
122
+ id: seed,
123
+ name: username,
124
+ displayName: username,
168
125
  },
169
- };
170
- };
126
+ pubKeyCredParams: [
127
+ { type: "public-key", alg: -8 }, // Ed25519
128
+ { type: "public-key", alg: -7 }, // ES256
129
+ { type: "public-key", alg: -257 }, // RS256
130
+ ],
131
+ attestation: "none",
132
+ authenticatorSelection: {
133
+ // - "platform": Uses the platform's built-in authenticator.
134
+ // - "cross-platform": Uses a device specific authenticator (yubikey, fido2, etc.)
135
+ authenticatorAttachment: authenticatorAttachment ?? "platform",
136
+ // - "discouraged": Only User Presence is needed.
137
+ // - "preferred": User Verification is preferred but not required. Falls back to User Presence.
138
+ // - "required": User Verification MUST occur (biometrics/PIN). Clients may silently downgrade to User Presence only.
139
+ userVerification: userVerification ?? "required",
140
+ // - "discouraged": Server-side credential is preferable, but will accept client-side discoverable credential.
141
+ // - "preferred": Relying Party strongly prefers client-side discoverable credential but will accept server-side credential.
142
+ // - "required": Client-side discoverable credential MUST be created, error if it can't be created.
143
+ residentKey: "required",
144
+ // Included for backwards compatibility. Deprecated in favor of residentKey (true = "required")
145
+ requireResidentKey: true,
146
+ },
147
+ },
148
+ });
149
+ const createCredentialRequestOptions = (deps) => (credentialId, relyingPartyID, userVerification) => ({
150
+ publicKey: {
151
+ challenge: generateSeed(deps)(),
152
+ rpId: relyingPartyID ?? document.location.hostname,
153
+ userVerification: userVerification ?? "preferred",
154
+ allowCredentials: [
155
+ {
156
+ type: "public-key",
157
+ id: base64UrlToUint8Array(Base64Url.orThrow(credentialId)),
158
+ },
159
+ ],
160
+ },
161
+ });
171
162
  const deriveEncryptionKey = (seed) => {
172
163
  const seed32 = seed.length === 32 ? seed : seed.slice(0, 32);
173
164
  return EncryptionKey.orThrow(createSlip21(seed32, ["evolu", "auth"]));
174
165
  };
175
166
  const encryptAuthResult = (deps) => (authResult, encryptionKey) => {
176
167
  const plaintext = utf8ToBytes(JSON.stringify(authResult));
177
- const { nonce, ciphertext } = deps.symmetricCrypto.encrypt(plaintext, encryptionKey);
168
+ const [ciphertext, nonce] = encryptWithXChaCha20Poly1305(deps)(plaintext, encryptionKey);
178
169
  return {
179
170
  nonce: uint8ArrayToBase64Url(nonce),
180
171
  ciphertext: uint8ArrayToBase64Url(ciphertext),
181
172
  };
182
173
  };
183
- const decryptAuthResult = (deps) => (encryptedData, encryptionKey) => {
174
+ const decryptAuthResult = (encryptedData, encryptionKey) => {
184
175
  const nonce = base64UrlToUint8Array(encryptedData.nonce);
185
176
  const ciphertext = base64UrlToUint8Array(encryptedData.ciphertext);
186
- const result = deps.symmetricCrypto.decrypt(ciphertext, encryptionKey, nonce);
177
+ const result = decryptWithXChaCha20Poly1305(XChaCha20Poly1305Ciphertext.orThrow(ciphertext), Entropy24.orThrow(nonce), encryptionKey);
187
178
  if (!result.ok)
188
179
  return null;
189
180
  return bytesToUtf8(result.value);
190
181
  };
191
- const generateSeed = (deps) => () => {
192
- return deps.randomBytes.create(32);
193
- };
182
+ const generateSeed = (deps) => () => deps.randomBytes.create(32);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=Shared.worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Shared.worker.d.ts","sourceRoot":"","sources":["../../../src/local-first/Shared.worker.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ /// <reference lib="webworker" />
2
+ import { installPolyfills } from "@evolu/common/polyfills";
3
+ installPolyfills();
4
+ import { createWebSocket } from "@evolu/common";
5
+ import { initSharedWorker } from "@evolu/common/local-first";
6
+ import { createRun } from "../Task.js";
7
+ import { createSharedWorkerSelf, createWorkerDeps } from "../Worker.js";
8
+ // No disposal (`await using`) is needed — a SharedWorker lives forever.
9
+ const run = createRun({
10
+ ...createWorkerDeps(),
11
+ createWebSocket,
12
+ });
13
+ run(initSharedWorker(createSharedWorkerSelf(self)));
@@ -1,4 +1,2 @@
1
- import { EvoluDeps } from "@evolu/common/local-first";
2
- export declare const localAuth: import("@evolu/common").LocalAuth;
3
- export declare const evoluWebDeps: EvoluDeps;
1
+ export * from "./Evolu.js";
4
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/local-first/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAIL,SAAS,EACV,MAAM,2BAA2B,CAAC;AAiBnC,eAAO,MAAM,SAAS,mCAGpB,CAAC;AAEH,eAAO,MAAM,YAAY,EAAE,SAM1B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/local-first/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
@@ -1,20 +1 @@
1
- import { createConsole, createLocalAuth, createRandomBytes, createSymmetricCrypto, createTime, } from "@evolu/common";
2
- import { createSharedWebWorker } from "../SharedWebWorker.js";
3
- import { createWebAuthnStore } from "./LocalAuth.js";
4
- import { reloadApp } from "./Platform.js";
5
- const randomBytes = createRandomBytes();
6
- const symmetricCrypto = createSymmetricCrypto({ randomBytes });
7
- const createDbWorker = (name) => createSharedWebWorker(name, () => new Worker(new URL("Db.worker.js", import.meta.url), {
8
- type: "module",
9
- }));
10
- export const localAuth = createLocalAuth({
11
- randomBytes,
12
- secureStorage: createWebAuthnStore({ randomBytes, symmetricCrypto }),
13
- });
14
- export const evoluWebDeps = {
15
- console: createConsole(),
16
- createDbWorker,
17
- randomBytes: createRandomBytes(),
18
- reloadApp,
19
- time: createTime(),
20
- };
1
+ export * from "./Evolu.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolu/web",
3
- "version": "2.4.0",
3
+ "version": "3.0.0-next.0",
4
4
  "description": "Evolu for web",
5
5
  "keywords": [
6
6
  "evolu",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "homepage": "https://evolu.dev",
16
16
  "type": "module",
17
- "types": "./dist/index.d.ts",
17
+ "types": "./dist/src/index.d.ts",
18
18
  "exports": {
19
19
  ".": {
20
20
  "types": "./dist/src/index.d.ts",
@@ -23,38 +23,35 @@
23
23
  }
24
24
  },
25
25
  "files": [
26
- "dist/**",
26
+ "dist/src/**",
27
27
  "src/**",
28
28
  "README.md"
29
29
  ],
30
+ "scripts": {
31
+ "build": "tsc --build tsconfig.build.json",
32
+ "prepack": "npm run build",
33
+ "format": "prettier --write \"src/*.{ts,tsx,md}\""
34
+ },
30
35
  "dependencies": {
31
36
  "@evolu/sqlite-wasm": "2.2.4",
32
37
  "idb-keyval": "^6.2.2"
33
38
  },
34
39
  "devDependencies": {
40
+ "@evolu/common": "8.0.0-next.0",
41
+ "@evolu/typescript-config": "*",
42
+ "@types/sharedworker": "^0.0.221",
35
43
  "@types/web-locks-api": "^0.0.5",
36
- "typescript": "^5.9.2",
37
- "user-agent-data-types": "^0.4.2",
38
- "vitest": "^4.0.4",
39
- "@evolu/common": "7.4.0",
40
- "@evolu/tsconfig": "0.0.2"
44
+ "typescript": "^5.9.3",
45
+ "user-agent-data-types": "^0.4.2"
41
46
  },
42
47
  "peerDependencies": {
43
- "@evolu/common": "^7.4.0"
48
+ "@evolu/common": "^8.0.0-next.0"
44
49
  },
45
50
  "publishConfig": {
46
51
  "access": "public"
47
52
  },
48
53
  "engines": {
49
- "node": ">=22.0.0"
54
+ "node": ">=24.0.0"
50
55
  },
51
- "sideEffects": [],
52
- "scripts": {
53
- "dev": "tsc --watch",
54
- "build": "rimraf dist && tsc",
55
- "test": "vitest run",
56
- "test:watch": "vitest",
57
- "clean": "rimraf .turbo node_modules dist",
58
- "format": "prettier --write \"src/*.{ts,tsx,md}\""
59
- }
60
- }
56
+ "sideEffects": false
57
+ }
@@ -0,0 +1,9 @@
1
+ import type { ReloadApp } from "@evolu/common";
2
+
3
+ export const reloadApp: ReloadApp = (url) => {
4
+ if (typeof document === "undefined") {
5
+ return;
6
+ }
7
+
8
+ location.replace(url ?? "/");
9
+ };
package/src/Sqlite.ts ADDED
@@ -0,0 +1,109 @@
1
+ import type { CreateSqliteDriver, SqliteRow } from "@evolu/common";
2
+ import { bytesToHex, createPreparedStatementsCache, ok } from "@evolu/common";
3
+ import sqlite3InitModule, {
4
+ type Database,
5
+ type PreparedStatement,
6
+ } from "@evolu/sqlite-wasm";
7
+
8
+ // @ts-expect-error Missing types.
9
+ globalThis.sqlite3ApiConfig = {
10
+ warn: (arg: unknown) => {
11
+ // Ignore irrelevant warning.
12
+ // https://github.com/sqlite/sqlite-wasm/issues/62
13
+ if (
14
+ typeof arg === "string" &&
15
+ arg.startsWith("Ignoring inability to install OPFS sqlite3_vfs")
16
+ )
17
+ return;
18
+ // eslint-disable-next-line no-console
19
+ console.warn(arg);
20
+ },
21
+ };
22
+
23
+ // Init ASAP.
24
+ const sqlite3Promise = sqlite3InitModule();
25
+
26
+ export const createWasmSqliteDriver: CreateSqliteDriver =
27
+ (name, options) => async () => {
28
+ 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
+
34
+ const stack = new globalThis.DisposableStack();
35
+ let db: Database;
36
+ switch (options?.mode) {
37
+ case "memory":
38
+ db = new sqlite3.oo1.DB(":memory:");
39
+ break;
40
+ case "encrypted": {
41
+ const pool = await sqlite3.installOpfsSAHPoolVfs({
42
+ directory: `.${name}`,
43
+ });
44
+ db = new pool.OpfsSAHPoolDb(
45
+ "file:evolu1.db?vfs=multipleciphers-opfs-sahpool",
46
+ );
47
+ db.exec(`
48
+ PRAGMA cipher = 'sqlcipher';
49
+ PRAGMA key = "x'${bytesToHex(options.encryptionKey)}'";
50
+ `);
51
+ break;
52
+ }
53
+ default: {
54
+ const pool = await sqlite3.installOpfsSAHPoolVfs({ name });
55
+ db = new pool.OpfsSAHPoolDb("file:evolu1.db");
56
+ }
57
+ }
58
+
59
+ db = stack.adopt(db, (db) => {
60
+ db.close();
61
+ });
62
+
63
+ const cache = stack.use(
64
+ createPreparedStatementsCache<PreparedStatement>(
65
+ (sql) => db.prepare(sql),
66
+ (statement) => {
67
+ statement.finalize();
68
+ },
69
+ ),
70
+ );
71
+
72
+ return ok({
73
+ exec: (query) => {
74
+ const prepared = cache.get(query);
75
+
76
+ if (prepared) {
77
+ if (query.parameters.length > 0) prepared.bind(query.parameters);
78
+
79
+ const rows = [];
80
+ while (prepared.step()) {
81
+ rows.push(prepared.get({}));
82
+ }
83
+ prepared.reset();
84
+
85
+ return {
86
+ rows: rows as ReadonlyArray<SqliteRow>,
87
+ changes: db.changes(),
88
+ };
89
+ }
90
+
91
+ const rows = db.exec(query.sql, {
92
+ returnValue: "resultRows",
93
+ rowMode: "object",
94
+ bind: query.parameters,
95
+ }) as ReadonlyArray<SqliteRow>;
96
+
97
+ const changes = db.changes();
98
+
99
+ return { rows, changes };
100
+ },
101
+
102
+ export: () => sqlite3.capi.sqlite3_js_db_export(db),
103
+
104
+ [Symbol.dispose]: () => {
105
+ // poolUtil.unlink?
106
+ stack.dispose();
107
+ },
108
+ });
109
+ };
package/src/Task.ts ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Web platform-specific Task utilities.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import {
8
+ createRun as createCommonRun,
9
+ createUnknownError,
10
+ ok,
11
+ type CreateRun,
12
+ type LeaderLock,
13
+ type Run,
14
+ type RunDeps,
15
+ } from "@evolu/common";
16
+
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
+ /**
51
+ * Creates {@link Run} for the browser with global error handling.
52
+ *
53
+ * Registers `error` and `unhandledrejection` handlers that log errors to the
54
+ * console. Handlers are removed when the Run is disposed.
55
+ *
56
+ * ### Example
57
+ *
58
+ * ```ts
59
+ * const console = createConsole({
60
+ * formatter: createConsoleFormatter()({
61
+ * timestampFormat: "relative",
62
+ * }),
63
+ * });
64
+ *
65
+ * await using run = createRun({ console });
66
+ * await using stack = new AsyncDisposableStack();
67
+ *
68
+ * stack.use(await run.orThrow(startApp()));
69
+ * ```
70
+ */
71
+ export const createRun: CreateRun<RunDeps> = <D>(
72
+ deps?: D,
73
+ ): Run<RunDeps & D> => {
74
+ const run = createCommonRun(deps);
75
+ const console = run.deps.console.child("global");
76
+
77
+ globalThis.addEventListener(
78
+ "error",
79
+ (event) => {
80
+ console.error("error", createUnknownError(event.error));
81
+ },
82
+ { signal: run.signal },
83
+ );
84
+
85
+ globalThis.addEventListener(
86
+ "unhandledrejection",
87
+ (event) => {
88
+ console.error("unhandledrejection", createUnknownError(event.reason));
89
+ },
90
+ { signal: run.signal },
91
+ );
92
+
93
+ return run;
94
+ };
package/src/Worker.ts ADDED
@@ -0,0 +1,151 @@
1
+ import type {
2
+ CreateMessagePort,
3
+ MessageChannel,
4
+ MessagePort,
5
+ NativeMessagePort,
6
+ SharedWorker,
7
+ SharedWorkerSelf,
8
+ Transferable,
9
+ Worker,
10
+ WorkerDeps,
11
+ WorkerSelf,
12
+ } from "@evolu/common";
13
+ import { assert, createConsole, createConsoleStoreOutput } from "@evolu/common";
14
+
15
+ /** Creates a {@link Worker} from a Web Worker. */
16
+ export const createWorker = <Input, Output>(
17
+ nativeWorker: globalThis.Worker,
18
+ ): Worker<Input, Output> => wrap(nativeWorker);
19
+
20
+ /** Creates an Evolu {@link SharedWorker} from a Web SharedWorker. */
21
+ export const createSharedWorker = <Input, Output = never>(
22
+ nativeSharedWorker: globalThis.SharedWorker,
23
+ ): SharedWorker<Input, Output> => {
24
+ const port = wrap<Input, Output>(nativeSharedWorker.port);
25
+ return {
26
+ port,
27
+ [Symbol.dispose]: port[Symbol.dispose],
28
+ };
29
+ };
30
+
31
+ /**
32
+ * Creates a {@link MessageChannel} from a Web MessageChannel.
33
+ *
34
+ * ### Example
35
+ *
36
+ * ```ts
37
+ * const channel = createMessageChannel<Request, Response>();
38
+ * channel.port1.onMessage = (response) => console.log(response);
39
+ * channel.port1.postMessage({ type: "ping" });
40
+ * ```
41
+ */
42
+ export const createMessageChannel = <Input, Output = never>(): MessageChannel<
43
+ Input,
44
+ Output
45
+ > => {
46
+ const channel = new globalThis.MessageChannel();
47
+ const stack = new DisposableStack();
48
+
49
+ return {
50
+ port1: stack.use(wrap<Input, Output>(channel.port1)),
51
+ port2: stack.use(wrap<Output, Input>(channel.port2)),
52
+ [Symbol.dispose]: () => {
53
+ stack.dispose();
54
+ },
55
+ };
56
+ };
57
+
58
+ /**
59
+ * Creates an Evolu {@link MessagePort} from a Web MessagePort.
60
+ *
61
+ * Use this for ports received via postMessage transfer.
62
+ */
63
+ export const createMessagePort: CreateMessagePort = (nativePort) =>
64
+ wrap(nativePort as unknown as globalThis.MessagePort);
65
+
66
+ /**
67
+ * Creates an Evolu {@link WorkerSelf} from a Web `DedicatedWorkerGlobalScope`
68
+ * (`self` inside a dedicated worker).
69
+ */
70
+ export const createWorkerSelf = <Input, Output = never>(
71
+ nativeSelf: globalThis.DedicatedWorkerGlobalScope,
72
+ ): WorkerSelf<Input, Output> => wrap<Output, Input>(nativeSelf);
73
+
74
+ /**
75
+ * Creates an Evolu {@link SharedWorkerSelf} from a Web `SharedWorkerGlobalScope`
76
+ * (`self` inside a shared worker).
77
+ *
78
+ * Disposing closes the shared worker scope for all connected clients.
79
+ */
80
+ export const createSharedWorkerSelf = <Input, Output = never>(
81
+ nativeSelf: globalThis.SharedWorkerGlobalScope,
82
+ ): SharedWorkerSelf<Input, Output> => {
83
+ const self: SharedWorkerSelf<Input, Output> = {
84
+ onConnect: null,
85
+ [Symbol.dispose]: () => {
86
+ nativeSelf.onconnect = null;
87
+ nativeSelf.close();
88
+ },
89
+ };
90
+
91
+ nativeSelf.onconnect = (e) => {
92
+ assert(
93
+ self.onConnect != null,
94
+ "onConnect must be set before receiving connections",
95
+ );
96
+ self.onConnect(wrap<Output, Input>(e.ports[0]));
97
+ };
98
+
99
+ return self;
100
+ };
101
+
102
+ /** Creates deps shared by web worker entry points. */
103
+ export const createWorkerDeps = (): WorkerDeps => {
104
+ const consoleStoreOutput = createConsoleStoreOutput();
105
+ const console = createConsole({ output: consoleStoreOutput });
106
+
107
+ return {
108
+ console,
109
+ consoleStoreOutputEntry: consoleStoreOutput.entry,
110
+ createMessagePort,
111
+ };
112
+ };
113
+
114
+ const wrap = <Input, Output>(
115
+ native:
116
+ | DedicatedWorkerGlobalScope
117
+ | globalThis.MessagePort
118
+ | globalThis.Worker,
119
+ ): MessagePort<Input, Output> => {
120
+ let onMessageHandler: ((message: Output) => void) | null = null;
121
+
122
+ return {
123
+ postMessage: (message: Input, transfer?: ReadonlyArray<Transferable>) => {
124
+ if (transfer == null) native.postMessage(message);
125
+ else native.postMessage(message, [...transfer]);
126
+ },
127
+
128
+ get onMessage() {
129
+ return onMessageHandler;
130
+ },
131
+ set onMessage(fn) {
132
+ onMessageHandler = fn;
133
+ if (fn) {
134
+ // Messages are queued until onMessage is assigned.
135
+ native.onmessage = (event: MessageEvent<Output>) => {
136
+ fn(event.data);
137
+ };
138
+ } else {
139
+ native.onmessage = null;
140
+ }
141
+ },
142
+
143
+ native: native as unknown as NativeMessagePort<Input, Output>,
144
+
145
+ [Symbol.dispose]: () => {
146
+ native.onmessage = null;
147
+ if (native instanceof globalThis.Worker) native.terminate();
148
+ else native.close();
149
+ },
150
+ };
151
+ };
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export * from "./local-first/index.js";
2
- export * from "./SharedWebWorker.js";
3
- export * from "./WasmSqliteDriver.js";
4
- export * from "./WebWorker.js";
2
+ export * from "./Sqlite.js";
3
+ export * from "./Task.js";
4
+ export * from "./Worker.js";