@evolu/web 3.4.0 → 3.4.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.
@@ -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,kBAyLlC,CAAC"}
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,kBA4LlC,CAAC"}
@@ -81,9 +81,11 @@ export const createWasmSqliteDriver = (name, options) => async (run) => {
81
81
  // DbWorker that ended without closing it, because its tab closed,
82
82
  // crashed or navigated away, can hold pool files after the lock has
83
83
  // passed on: WebKit releases a terminated worker's locks and its files
84
- // separately, in no set order. sqlite-wasm cannot set up a pool with a
85
- // held file, and it then deletes the pool directory, which the held file
86
- // usually but not always prevents. Only a worker that has ended can hold
84
+ // separately, in no set order
85
+ // (https://bugs.webkit.org/show_bug.cgi?id=301520). sqlite-wasm cannot
86
+ // set up a pool with a held file, and it then deletes the pool
87
+ // directory, which the held file usually but not always prevents. Only
88
+ // a worker that has ended can hold
87
89
  // the files, and it can only release them, so the pool is set up once
88
90
  // every file opens.
89
91
  const canOpenPoolFiles = async () => {
@@ -101,7 +103,8 @@ export const createWasmSqliteDriver = (name, options) => async (run) => {
101
103
  const accessHandle = await tryAsync(() => handle.createSyncAccessHandle());
102
104
  if (!accessHandle.ok) {
103
105
  // WebKit rejects a held file with InvalidStateError, other
104
- // engines with NoModificationAllowedError, as the spec says.
106
+ // engines with NoModificationAllowedError, as the spec says
107
+ // (https://bugs.webkit.org/show_bug.cgi?id=326135).
105
108
  // WebKit also uses InvalidStateError for a closed or invalid
106
109
  // handle and a stopped context. A retry opens fresh handles from
107
110
  // a new listing, and a stopped context ends the loop with its
@@ -22,8 +22,9 @@ export interface SharedWorkerUnsupportedDep {
22
22
  *
23
23
  * When the page enters the browser's back-forward cache, as Safari does on
24
24
  * every navigation away, this tab ends its part as if it closed: it stops the
25
- * database workers it hosts, so another tab takes them over, and it reloads if
26
- * the user comes back to it.
25
+ * database workers it hosts, so another tab takes them over, the shared worker
26
+ * ends this tab's Evolu instances, and the tab reloads if the user comes back
27
+ * to it.
27
28
  *
28
29
  * Where the browser offers no persistent storage, as in Safari's Private
29
30
  * Browsing or a Firefox private window, the database is kept in memory, and
@@ -1 +1 @@
1
- {"version":3,"file":"Evolu.d.ts","sourceRoot":"","sources":["../../../src/local-first/Evolu.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,UAAU,EAEf,KAAK,YAAY,EAElB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAKV,SAAS,EACT,2BAA2B,EAI5B,MAAM,2BAA2B,CAAC;AAgBnC,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,eAAO,MAAM,eAAe,UACpB,OAAO,CAAC,UAAU,CAAC,GACvB,OAAO,CAAC,YAAY,CAAC,GACrB,OAAO,CAAC,0BAA0B,CAAC,GACnC,OAAO,CAAC,2BAA2B,CAAC,KACrC,SAsPF,CAAC"}
1
+ {"version":3,"file":"Evolu.d.ts","sourceRoot":"","sources":["../../../src/local-first/Evolu.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,UAAU,EAEf,KAAK,YAAY,EAElB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAKV,SAAS,EACT,2BAA2B,EAI5B,MAAM,2BAA2B,CAAC;AAgBnC,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,eAAe,UACpB,OAAO,CAAC,UAAU,CAAC,GACvB,OAAO,CAAC,YAAY,CAAC,GACrB,OAAO,CAAC,0BAA0B,CAAC,GACnC,OAAO,CAAC,2BAA2B,CAAC,KACrC,SAgSF,CAAC"}
@@ -70,8 +70,9 @@ import { createBroadcastChannel, createMessageChannel, createSharedWorker, creat
70
70
  *
71
71
  * When the page enters the browser's back-forward cache, as Safari does on
72
72
  * every navigation away, this tab ends its part as if it closed: it stops the
73
- * database workers it hosts, so another tab takes them over, and it reloads if
74
- * the user comes back to it.
73
+ * database workers it hosts, so another tab takes them over, the shared worker
74
+ * ends this tab's Evolu instances, and the tab reloads if the user comes back
75
+ * to it.
75
76
  *
76
77
  * Where the browser offers no persistent storage, as in Safari's Private
77
78
  * Browsing or a Firefox private window, the database is kept in memory, and
@@ -276,21 +277,45 @@ export const createEvoluDeps = (deps = {}) => {
276
277
  await navigator.storage.persist();
277
278
  });
278
279
  };
280
+ // Disposing the deps releases the locks this page holds through them, as
281
+ // closing the page would, and a lock granted afterwards is released at once.
282
+ // The SharedWorker learns that an Evolu instance ended when it gets the lock
283
+ // the instance holds. Chrome keeps the locks of a page in its back-forward
284
+ // cache, and a request made before the page entered the cache waits until
285
+ // Chrome drops the page (https://issues.chromium.org/issues/567630881), so
286
+ // the SharedWorker would keep syncing the owners of a cached tab's instances
287
+ // and rerunning their queries.
288
+ let areLocksReleased = false;
289
+ const locksReleased = Promise.withResolvers();
290
+ disposer.defer(() => {
291
+ areLocksReleased = true;
292
+ locksReleased.resolve();
293
+ });
294
+ function requestLock(name, ...args) {
295
+ const [options, callback] = args.length === 1 ? [{}, args[0]] : args;
296
+ return navigator.locks.request(name, options, (lock) => areLocksReleased
297
+ ? undefined
298
+ : Promise.race([callback(lock), locksReleased.promise]));
299
+ }
279
300
  const evoluDeps = disposer.use(createCommonEvoluDeps({
280
301
  requestPersistentStorage,
281
302
  ...deps,
282
303
  createDbWorker,
283
304
  createBroadcastChannel,
284
305
  createMessageChannel,
285
- lockManager: navigator.locks,
306
+ lockManager: {
307
+ query: () => navigator.locks.query(),
308
+ request: requestLock,
309
+ },
286
310
  reloadApp: reloadThisApp,
287
311
  sharedWorker,
288
312
  }));
289
- // A page entering the back-forward cache is frozen with its DbWorkers, which
290
- // keep their database locks, so the other tabs would stall. WebKit also
291
- // releases the page's own locks, so a restored page would work with state
292
- // the other tabs gave up on. The page therefore ends its part as if it
293
- // closed, and reloads when it is shown again.
313
+ // A page entering the back-forward cache is frozen with its DbWorkers, and
314
+ // WebKit keeps their database locks while the page is cached
315
+ // (https://bugs.webkit.org/show_bug.cgi?id=316904), so the other tabs would
316
+ // stall. WebKit also releases the page's own locks, so a restored page would
317
+ // work with state the other tabs gave up on. The page therefore ends its part
318
+ // as if it closed, and reloads when it is shown again.
294
319
  const reloadRestoredPage = (event) => {
295
320
  if (event.persisted)
296
321
  reloadThisApp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolu/web",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
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.14.0",
38
+ "@evolu/common": "8.18.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.14.0"
47
+ "@evolu/common": "^8.18.0"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"
package/src/Sqlite.ts CHANGED
@@ -53,9 +53,11 @@ export const createWasmSqliteDriver: CreateSqliteDriver =
53
53
  // DbWorker that ended without closing it, because its tab closed,
54
54
  // crashed or navigated away, can hold pool files after the lock has
55
55
  // passed on: WebKit releases a terminated worker's locks and its files
56
- // separately, in no set order. sqlite-wasm cannot set up a pool with a
57
- // held file, and it then deletes the pool directory, which the held file
58
- // usually but not always prevents. Only a worker that has ended can hold
56
+ // separately, in no set order
57
+ // (https://bugs.webkit.org/show_bug.cgi?id=301520). sqlite-wasm cannot
58
+ // set up a pool with a held file, and it then deletes the pool
59
+ // directory, which the held file usually but not always prevents. Only
60
+ // a worker that has ended can hold
59
61
  // the files, and it can only release them, so the pool is set up once
60
62
  // every file opens.
61
63
  const canOpenPoolFiles = async (): Promise<boolean> => {
@@ -76,7 +78,8 @@ export const createWasmSqliteDriver: CreateSqliteDriver =
76
78
  );
77
79
  if (!accessHandle.ok) {
78
80
  // WebKit rejects a held file with InvalidStateError, other
79
- // engines with NoModificationAllowedError, as the spec says.
81
+ // engines with NoModificationAllowedError, as the spec says
82
+ // (https://bugs.webkit.org/show_bug.cgi?id=326135).
80
83
  // WebKit also uses InvalidStateError for a closed or invalid
81
84
  // handle and a stopped context. A retry opens fresh handles from
82
85
  // a new listing, and a stopped context ends the loop with its
@@ -1,4 +1,5 @@
1
1
  import {
2
+ acquireLeaderLock,
2
3
  assertEqual,
3
4
  assertFalse,
4
5
  assertNonNullable,
@@ -7,6 +8,7 @@ import {
7
8
  createConsole,
8
9
  createIdFromString,
9
10
  PositiveInt,
11
+ testCreateRun,
10
12
  testName,
11
13
  testStubGlobal,
12
14
  type NativeMessagePort,
@@ -222,6 +224,39 @@ describe("createEvoluDeps", () => {
222
224
  assertSame(setup.reloadApp.mock.callCount(), 0);
223
225
  });
224
226
 
227
+ it("releases the locks of its Evolu instances when the page enters the cache", async () => {
228
+ using setup = setupWebEvoluDeps();
229
+ await using run = testCreateRun({
230
+ lockManager: setup.deps.lockManager,
231
+ });
232
+ await using _instanceLock = await run.ok(
233
+ acquireLeaderLock("cached-instance"),
234
+ );
235
+ assertFalse(await isLockAvailable("evolu-leaderlock-cached-instance"));
236
+
237
+ setup.dispatchPageTransition("pagehide", true);
238
+ await waitForMacrotask();
239
+
240
+ assertTrue(await isLockAvailable("evolu-leaderlock-cached-instance"));
241
+ });
242
+
243
+ it("releases at once a lock granted after the page entered the cache", async () => {
244
+ using setup = setupWebEvoluDeps();
245
+ const name = "evolu-granted-after-cache";
246
+ const held = Promise.withResolvers<void>();
247
+ const holding = navigator.locks.request(name, () => held.promise);
248
+ const callback = mock.fn<LockGrantedCallback<void>>();
249
+ const request = setup.deps.lockManager.request(name, callback);
250
+
251
+ setup.dispatchPageTransition("pagehide", true);
252
+ held.resolve();
253
+ await holding;
254
+ await request;
255
+
256
+ assertSame(callback.mock.callCount(), 0);
257
+ assertTrue(await isLockAvailable(name));
258
+ });
259
+
225
260
  it("reloads once when the page is restored from the cache", () => {
226
261
  using setup = setupWebEvoluDeps();
227
262
  setup.dispatchPageTransition("pageshow", false);
@@ -57,8 +57,9 @@ export interface SharedWorkerUnsupportedDep {
57
57
  *
58
58
  * When the page enters the browser's back-forward cache, as Safari does on
59
59
  * every navigation away, this tab ends its part as if it closed: it stops the
60
- * database workers it hosts, so another tab takes them over, and it reloads if
61
- * the user comes back to it.
60
+ * database workers it hosts, so another tab takes them over, the shared worker
61
+ * ends this tab's Evolu instances, and the tab reloads if the user comes back
62
+ * to it.
62
63
  *
63
64
  * Where the browser offers no persistent storage, as in Safari's Private
64
65
  * Browsing or a Firefox private window, the database is kept in memory, and
@@ -293,6 +294,44 @@ export const createEvoluDeps = (
293
294
  });
294
295
  };
295
296
 
297
+ // Disposing the deps releases the locks this page holds through them, as
298
+ // closing the page would, and a lock granted afterwards is released at once.
299
+ // The SharedWorker learns that an Evolu instance ended when it gets the lock
300
+ // the instance holds. Chrome keeps the locks of a page in its back-forward
301
+ // cache, and a request made before the page entered the cache waits until
302
+ // Chrome drops the page (https://issues.chromium.org/issues/567630881), so
303
+ // the SharedWorker would keep syncing the owners of a cached tab's instances
304
+ // and rerunning their queries.
305
+ let areLocksReleased = false;
306
+ const locksReleased = Promise.withResolvers<void>();
307
+ disposer.defer(() => {
308
+ areLocksReleased = true;
309
+ locksReleased.resolve();
310
+ });
311
+
312
+ function requestLock<T>(
313
+ name: string,
314
+ callback: LockGrantedCallback<T>,
315
+ ): Promise<Awaited<T>>;
316
+ function requestLock<T>(
317
+ name: string,
318
+ options: LockOptions,
319
+ callback: LockGrantedCallback<T>,
320
+ ): Promise<Awaited<T>>;
321
+ function requestLock(
322
+ name: string,
323
+ ...args:
324
+ | [LockGrantedCallback<unknown>]
325
+ | [LockOptions, LockGrantedCallback<unknown>]
326
+ ): Promise<unknown> {
327
+ const [options, callback] = args.length === 1 ? [{}, args[0]] : args;
328
+ return navigator.locks.request(name, options, (lock) =>
329
+ areLocksReleased
330
+ ? undefined
331
+ : Promise.race([callback(lock), locksReleased.promise]),
332
+ );
333
+ }
334
+
296
335
  const evoluDeps = disposer.use(
297
336
  createCommonEvoluDeps({
298
337
  requestPersistentStorage,
@@ -300,17 +339,21 @@ export const createEvoluDeps = (
300
339
  createDbWorker,
301
340
  createBroadcastChannel,
302
341
  createMessageChannel,
303
- lockManager: navigator.locks,
342
+ lockManager: {
343
+ query: () => navigator.locks.query(),
344
+ request: requestLock,
345
+ },
304
346
  reloadApp: reloadThisApp,
305
347
  sharedWorker,
306
348
  }),
307
349
  );
308
350
 
309
- // A page entering the back-forward cache is frozen with its DbWorkers, which
310
- // keep their database locks, so the other tabs would stall. WebKit also
311
- // releases the page's own locks, so a restored page would work with state
312
- // the other tabs gave up on. The page therefore ends its part as if it
313
- // closed, and reloads when it is shown again.
351
+ // A page entering the back-forward cache is frozen with its DbWorkers, and
352
+ // WebKit keeps their database locks while the page is cached
353
+ // (https://bugs.webkit.org/show_bug.cgi?id=316904), so the other tabs would
354
+ // stall. WebKit also releases the page's own locks, so a restored page would
355
+ // work with state the other tabs gave up on. The page therefore ends its part
356
+ // as if it closed, and reloads when it is shown again.
314
357
  const reloadRestoredPage = (event: PageTransitionEvent): void => {
315
358
  if (event.persisted) reloadThisApp();
316
359
  };