@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.
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +7 -4
- package/dist/src/local-first/Evolu.d.ts +3 -2
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +33 -8
- package/package.json +3 -3
- package/src/Sqlite.ts +7 -4
- package/src/local-first/Evolu.test.ts +35 -0
- package/src/local-first/Evolu.ts +51 -8
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,kBA4LlC,CAAC"}
|
package/dist/src/Sqlite.js
CHANGED
|
@@ -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
|
|
85
|
-
//
|
|
86
|
-
//
|
|
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,
|
|
26
|
-
* the user comes back
|
|
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
|
|
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,
|
|
74
|
-
* the user comes back
|
|
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:
|
|
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,
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
57
|
-
//
|
|
58
|
-
//
|
|
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);
|
package/src/local-first/Evolu.ts
CHANGED
|
@@ -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,
|
|
61
|
-
* the user comes back
|
|
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:
|
|
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,
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
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
|
};
|