@evolu/web 3.1.1 → 3.2.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/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +4 -1
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +53 -2
- package/dist/src/Task.d.ts +6 -3
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +2 -2
- package/dist/src/local-first/Evolu.d.ts +47 -3
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +336 -50
- package/dist/src/local-first/Shared.worker.js +3 -1
- package/package.json +4 -5
- package/src/Platform.test.ts +29 -1
- package/src/Platform.ts +2 -1
- package/src/Sqlite.test.ts +176 -0
- package/src/Sqlite.ts +68 -1
- package/src/Task.test.ts +3 -14
- package/src/Task.ts +8 -4
- package/src/local-first/Evolu.test.ts +600 -2
- package/src/local-first/Evolu.ts +321 -40
- package/src/local-first/Shared.worker.ts +4 -1
- package/dist/src/local-first/LocalAuth.d.ts +0 -3
- package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
- package/dist/src/local-first/LocalAuth.js +0 -185
- package/src/local-first/LocalAuth.ts +0 -320
package/src/Platform.test.ts
CHANGED
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
PositiveInt,
|
|
7
7
|
testStubGlobal,
|
|
8
8
|
} from "@evolu/common";
|
|
9
|
-
import { test } from "node:test";
|
|
9
|
+
import { mock, test } from "node:test";
|
|
10
10
|
import { availableParallelism, isApplePlatform } from "./index.ts";
|
|
11
|
+
import { reloadApp } from "./Platform.ts";
|
|
11
12
|
|
|
12
13
|
for (const platform of ["macOS", "MacIntel", "iPhone", "iPad", "iPod"]) {
|
|
13
14
|
test(`isApplePlatform recognizes ${platform}`, () => {
|
|
@@ -43,3 +44,30 @@ test("availableParallelism returns the validated browser value", () => {
|
|
|
43
44
|
assertType<typeof parallelism, PositiveInt>();
|
|
44
45
|
assertEqual(parallelism, 128);
|
|
45
46
|
});
|
|
47
|
+
|
|
48
|
+
test("reloadApp reloads the current page", () => {
|
|
49
|
+
const reload = mock.fn<() => void>();
|
|
50
|
+
const replace = mock.fn<(url: string) => void>();
|
|
51
|
+
using _document = testStubGlobal("document", {});
|
|
52
|
+
using _location = testStubGlobal("location", { reload, replace });
|
|
53
|
+
|
|
54
|
+
reloadApp();
|
|
55
|
+
|
|
56
|
+
assertEqual(reload.mock.callCount(), 1);
|
|
57
|
+
assertEqual(replace.mock.callCount(), 0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("reloadApp loads a given URL instead", () => {
|
|
61
|
+
const reload = mock.fn<() => void>();
|
|
62
|
+
const replace = mock.fn<(url: string) => void>();
|
|
63
|
+
using _document = testStubGlobal("document", {});
|
|
64
|
+
using _location = testStubGlobal("location", { reload, replace });
|
|
65
|
+
|
|
66
|
+
reloadApp("/signed-out");
|
|
67
|
+
|
|
68
|
+
assertEqual(
|
|
69
|
+
replace.mock.calls.map((call) => call.arguments),
|
|
70
|
+
[["/signed-out"]],
|
|
71
|
+
);
|
|
72
|
+
assertEqual(reload.mock.callCount(), 0);
|
|
73
|
+
});
|
package/src/Platform.ts
CHANGED
package/src/Sqlite.test.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AbortError,
|
|
3
|
+
assert,
|
|
2
4
|
assertEqual,
|
|
3
5
|
assertEqualBytes,
|
|
6
|
+
assertSame,
|
|
4
7
|
assertTrue,
|
|
8
|
+
assertType,
|
|
5
9
|
EncryptionKey,
|
|
6
10
|
Name,
|
|
7
11
|
sql,
|
|
@@ -9,6 +13,90 @@ import {
|
|
|
9
13
|
} from "@evolu/common";
|
|
10
14
|
import { describe, it, mock } from "node:test";
|
|
11
15
|
|
|
16
|
+
// OPFS with an optional pool directory. Each pool file lists the error names
|
|
17
|
+
// its next openings reject with, in order.
|
|
18
|
+
const opfsMock = (() => {
|
|
19
|
+
const state = {
|
|
20
|
+
closedAccessHandleCount: 0,
|
|
21
|
+
directoryNames: [] as Array<string>,
|
|
22
|
+
getDirectoryCount: 0,
|
|
23
|
+
hasOpaqueDirectory: true,
|
|
24
|
+
openedAccessHandleCount: 0,
|
|
25
|
+
poolFiles: null as Array<Array<string>> | null,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const opaqueDirectory = {
|
|
29
|
+
// for await also iterates a sync iterable.
|
|
30
|
+
values: function* () {
|
|
31
|
+
yield { kind: "directory" };
|
|
32
|
+
for (const errorNames of state.poolFiles ?? [])
|
|
33
|
+
yield {
|
|
34
|
+
kind: "file",
|
|
35
|
+
createSyncAccessHandle: () => {
|
|
36
|
+
const errorName = errorNames.shift();
|
|
37
|
+
if (errorName !== undefined)
|
|
38
|
+
return Promise.reject(new DOMException("Held", errorName));
|
|
39
|
+
state.openedAccessHandleCount += 1;
|
|
40
|
+
return Promise.resolve({
|
|
41
|
+
close: () => {
|
|
42
|
+
state.closedAccessHandleCount += 1;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const poolDirectory = {
|
|
51
|
+
getDirectoryHandle: (name: string) => {
|
|
52
|
+
state.directoryNames.push(name);
|
|
53
|
+
return state.hasOpaqueDirectory
|
|
54
|
+
? Promise.resolve(opaqueDirectory)
|
|
55
|
+
: Promise.reject(new DOMException("Missing", "NotFoundError"));
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const root = {
|
|
60
|
+
getDirectoryHandle: (name: string) => {
|
|
61
|
+
state.directoryNames.push(name);
|
|
62
|
+
return state.poolFiles
|
|
63
|
+
? Promise.resolve(poolDirectory)
|
|
64
|
+
: Promise.reject(new DOMException("Missing", "NotFoundError"));
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
Object.defineProperty(navigator, "storage", {
|
|
69
|
+
configurable: true,
|
|
70
|
+
value: {
|
|
71
|
+
getDirectory: () => {
|
|
72
|
+
state.getDirectoryCount += 1;
|
|
73
|
+
return Promise.resolve(root);
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
reset: (
|
|
80
|
+
poolFiles: Array<Array<string>> | null = null,
|
|
81
|
+
{ hasOpaqueDirectory = true } = {},
|
|
82
|
+
) => {
|
|
83
|
+
state.closedAccessHandleCount = 0;
|
|
84
|
+
state.directoryNames.length = 0;
|
|
85
|
+
state.getDirectoryCount = 0;
|
|
86
|
+
state.hasOpaqueDirectory = hasOpaqueDirectory;
|
|
87
|
+
state.openedAccessHandleCount = 0;
|
|
88
|
+
state.poolFiles = poolFiles;
|
|
89
|
+
},
|
|
90
|
+
state,
|
|
91
|
+
};
|
|
92
|
+
})();
|
|
93
|
+
|
|
94
|
+
// Lets resolved mocks settle before test time advances.
|
|
95
|
+
const flushMicrotasks = (): Promise<void> =>
|
|
96
|
+
new Promise((resolve) => {
|
|
97
|
+
setImmediate(resolve);
|
|
98
|
+
});
|
|
99
|
+
|
|
12
100
|
const sqliteMock = (() => {
|
|
13
101
|
class PreparedStatement {
|
|
14
102
|
finalized = false;
|
|
@@ -115,6 +203,7 @@ const sqliteMock = (() => {
|
|
|
115
203
|
consoleWarn: mock.fn<typeof console.warn>(),
|
|
116
204
|
pool,
|
|
117
205
|
reset: () => {
|
|
206
|
+
opfsMock.reset();
|
|
118
207
|
state.closedDatabases.length = 0;
|
|
119
208
|
state.createdDatabases.length = 0;
|
|
120
209
|
state.deletedFilenames.length = 0;
|
|
@@ -193,6 +282,7 @@ describe("createWasmSqliteDriver coverage helpers", () => {
|
|
|
193
282
|
|
|
194
283
|
assertEqual(sqliteMock.sqlite3.installOpfsSAHPoolVfs.mock.callCount(), 0);
|
|
195
284
|
assertEqual(sqliteMock.state.createdDatabases[0]?.filename, ":memory:");
|
|
285
|
+
assertEqual(opfsMock.state.getDirectoryCount, 0);
|
|
196
286
|
});
|
|
197
287
|
|
|
198
288
|
it("executes non-prepared query and exports database", async () => {
|
|
@@ -354,3 +444,89 @@ describe("createWasmSqliteDriver coverage helpers", () => {
|
|
|
354
444
|
);
|
|
355
445
|
});
|
|
356
446
|
});
|
|
447
|
+
|
|
448
|
+
describe("createWasmSqliteDriver held pool files", () => {
|
|
449
|
+
for (const errorName of ["InvalidStateError", "NoModificationAllowedError"])
|
|
450
|
+
it(`sets up the pool once a file held with ${errorName} is released`, async () => {
|
|
451
|
+
sqliteMock.reset();
|
|
452
|
+
opfsMock.reset([[], [errorName, errorName]]);
|
|
453
|
+
|
|
454
|
+
await using run = testCreateRun();
|
|
455
|
+
const driver = run.ok(createWasmSqliteDriver(Name.orThrow("MockPlain")));
|
|
456
|
+
await flushMicrotasks();
|
|
457
|
+
|
|
458
|
+
assertEqual(opfsMock.state.directoryNames, [".MockPlain", ".opaque"]);
|
|
459
|
+
run.deps.time.advance("49ms");
|
|
460
|
+
await flushMicrotasks();
|
|
461
|
+
assertEqual(sqliteMock.sqlite3.installOpfsSAHPoolVfs.mock.callCount(), 0);
|
|
462
|
+
|
|
463
|
+
// The retry delay doubles after each held pass.
|
|
464
|
+
run.deps.time.advance("1ms");
|
|
465
|
+
await flushMicrotasks();
|
|
466
|
+
run.deps.time.advance("99ms");
|
|
467
|
+
await flushMicrotasks();
|
|
468
|
+
assertEqual(sqliteMock.sqlite3.installOpfsSAHPoolVfs.mock.callCount(), 0);
|
|
469
|
+
|
|
470
|
+
run.deps.time.advance("1ms");
|
|
471
|
+
using _driver = await driver;
|
|
472
|
+
|
|
473
|
+
// The free file opens on each of the three passes, the held one once.
|
|
474
|
+
assertEqual(sqliteMock.sqlite3.installOpfsSAHPoolVfs.mock.callCount(), 1);
|
|
475
|
+
assertEqual(opfsMock.state.openedAccessHandleCount, 4);
|
|
476
|
+
assertEqual(opfsMock.state.closedAccessHandleCount, 4);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("sets up the pool at once when its directory has no files yet", async () => {
|
|
480
|
+
sqliteMock.reset();
|
|
481
|
+
opfsMock.reset([], { hasOpaqueDirectory: false });
|
|
482
|
+
|
|
483
|
+
await using run = testCreateRun();
|
|
484
|
+
using _driver = await run.ok(
|
|
485
|
+
createWasmSqliteDriver(Name.orThrow("MockPlain")),
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
assertEqual(opfsMock.state.directoryNames, [".MockPlain", ".opaque"]);
|
|
489
|
+
assertEqual(opfsMock.state.openedAccessHandleCount, 0);
|
|
490
|
+
assertEqual(sqliteMock.sqlite3.installOpfsSAHPoolVfs.mock.callCount(), 1);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("warns once when pool files stay held for five seconds", async () => {
|
|
494
|
+
sqliteMock.reset();
|
|
495
|
+
opfsMock.reset([Array.from({ length: 12 }, () => "InvalidStateError")]);
|
|
496
|
+
|
|
497
|
+
await using run = testCreateRun();
|
|
498
|
+
const driver = run.ok(createWasmSqliteDriver(Name.orThrow("MockPlain")));
|
|
499
|
+
for (let second = 0; second < 12; second += 1) {
|
|
500
|
+
await flushMicrotasks();
|
|
501
|
+
run.deps.time.advance("1s");
|
|
502
|
+
}
|
|
503
|
+
using _driver = await driver;
|
|
504
|
+
|
|
505
|
+
const warnings = run.deps.console
|
|
506
|
+
.getEntriesSnapshot()
|
|
507
|
+
.filter((entry) => entry.method === "warn");
|
|
508
|
+
assertEqual(
|
|
509
|
+
warnings.map((entry) => entry.args),
|
|
510
|
+
[
|
|
511
|
+
[
|
|
512
|
+
"Waiting for an ended DbWorker to release the files of database MockPlain.",
|
|
513
|
+
],
|
|
514
|
+
],
|
|
515
|
+
);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("fails before setting up the pool when a file cannot be opened", async () => {
|
|
519
|
+
sqliteMock.reset();
|
|
520
|
+
opfsMock.reset([["UnknownError"]]);
|
|
521
|
+
|
|
522
|
+
await using run = testCreateRun();
|
|
523
|
+
const result = await run.abortable(
|
|
524
|
+
createWasmSqliteDriver(Name.orThrow("MockPlain")),
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
assert(!result.ok, "Expected the driver to fail.");
|
|
528
|
+
assertType(AbortError, result.error);
|
|
529
|
+
assertSame(result.error.reason.type, "PanicAbortReason");
|
|
530
|
+
assertEqual(sqliteMock.sqlite3.installOpfsSAHPoolVfs.mock.callCount(), 0);
|
|
531
|
+
});
|
|
532
|
+
});
|
package/src/Sqlite.ts
CHANGED
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
createPreparedStatementsCache,
|
|
5
5
|
exhaustiveCheck,
|
|
6
6
|
ok,
|
|
7
|
+
performanceDurationBetween,
|
|
8
|
+
PositiveMillis,
|
|
9
|
+
sleep,
|
|
10
|
+
tryAsync,
|
|
7
11
|
} from "@evolu/common";
|
|
8
12
|
import sqlite3InitModule, {
|
|
9
13
|
type Database,
|
|
@@ -32,7 +36,7 @@ const sqlite3Promise = sqlite3InitModule();
|
|
|
32
36
|
const fileName = "evolu1.db";
|
|
33
37
|
|
|
34
38
|
export const createWasmSqliteDriver: CreateSqliteDriver =
|
|
35
|
-
(name, options) => async () => {
|
|
39
|
+
(name, options) => async (run) => {
|
|
36
40
|
const sqlite3 = await sqlite3Promise;
|
|
37
41
|
|
|
38
42
|
using disposer = new DisposableStack();
|
|
@@ -45,6 +49,69 @@ export const createWasmSqliteDriver: CreateSqliteDriver =
|
|
|
45
49
|
const createOpfsSAHPoolVfs = async (
|
|
46
50
|
options: Parameters<typeof sqlite3.installOpfsSAHPoolVfs>[0],
|
|
47
51
|
): Promise<SAHPoolUtil> => {
|
|
52
|
+
// Evolu opens a database only while it holds the database lock, but a
|
|
53
|
+
// DbWorker that ended without closing it, because its tab closed,
|
|
54
|
+
// crashed or navigated away, can hold pool files after the lock has
|
|
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
|
|
59
|
+
// the files, and it can only release them, so the pool is set up once
|
|
60
|
+
// every file opens.
|
|
61
|
+
const canOpenPoolFiles = async (): Promise<boolean> => {
|
|
62
|
+
const root = await navigator.storage.getDirectory();
|
|
63
|
+
// sqlite-wasm keeps the pool in `.${name}/.opaque` in both OPFS modes.
|
|
64
|
+
const poolDirectory = await tryAsync(() =>
|
|
65
|
+
root.getDirectoryHandle(`.${name}`),
|
|
66
|
+
);
|
|
67
|
+
if (!poolDirectory.ok) return true;
|
|
68
|
+
const opaqueDirectory = await tryAsync(() =>
|
|
69
|
+
poolDirectory.value.getDirectoryHandle(".opaque"),
|
|
70
|
+
);
|
|
71
|
+
if (!opaqueDirectory.ok) return true;
|
|
72
|
+
for await (const handle of opaqueDirectory.value.values()) {
|
|
73
|
+
if (handle.kind !== "file") continue;
|
|
74
|
+
const accessHandle = await tryAsync(() =>
|
|
75
|
+
handle.createSyncAccessHandle(),
|
|
76
|
+
);
|
|
77
|
+
if (!accessHandle.ok) {
|
|
78
|
+
// WebKit rejects a held file with InvalidStateError, other
|
|
79
|
+
// engines with NoModificationAllowedError, as the spec says.
|
|
80
|
+
// WebKit also uses InvalidStateError for a closed or invalid
|
|
81
|
+
// handle and a stopped context. A retry opens fresh handles from
|
|
82
|
+
// a new listing, and a stopped context ends the loop with its
|
|
83
|
+
// worker, so retrying those is harmless.
|
|
84
|
+
if (
|
|
85
|
+
accessHandle.error instanceof DOMException &&
|
|
86
|
+
(accessHandle.error.name === "InvalidStateError" ||
|
|
87
|
+
accessHandle.error.name === "NoModificationAllowedError")
|
|
88
|
+
)
|
|
89
|
+
return false;
|
|
90
|
+
throw accessHandle.error;
|
|
91
|
+
}
|
|
92
|
+
accessHandle.value.close();
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const waitStart = run.deps.time.performance.now();
|
|
98
|
+
let retryDelay = 50;
|
|
99
|
+
let isWaitReported = false;
|
|
100
|
+
while (!(await canOpenPoolFiles())) {
|
|
101
|
+
const waited = performanceDurationBetween(
|
|
102
|
+
waitStart,
|
|
103
|
+
run.deps.time.performance.now(),
|
|
104
|
+
);
|
|
105
|
+
if (!isWaitReported && waited >= 5000) {
|
|
106
|
+
isWaitReported = true;
|
|
107
|
+
run.deps.console.warn(
|
|
108
|
+
`Waiting for an ended DbWorker to release the files of database ${name}.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
await run.ok(sleep(PositiveMillis.orThrow(retryDelay)));
|
|
112
|
+
retryDelay = Math.min(retryDelay * 2, 1000);
|
|
113
|
+
}
|
|
114
|
+
|
|
48
115
|
const pool = await sqlite3.installOpfsSAHPoolVfs(options);
|
|
49
116
|
if (pool.isPaused()) await pool.unpauseVfs();
|
|
50
117
|
disposer.defer(() => {
|
package/src/Task.test.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
assertEqual,
|
|
3
|
-
assertNonNullable,
|
|
4
|
-
assertTrue,
|
|
5
|
-
testStubGlobal,
|
|
6
|
-
} from "@evolu/common";
|
|
1
|
+
import { assertEqual, assertSame, testStubGlobal } from "@evolu/common";
|
|
7
2
|
import { describe, it, mock } from "node:test";
|
|
8
3
|
import { createRun } from "./Task.ts";
|
|
9
4
|
|
|
10
5
|
describe("createRun", () => {
|
|
11
|
-
it("createRun reports
|
|
6
|
+
it("createRun reports a panic's defect with global reportError", async () => {
|
|
12
7
|
const reportError = mock.fn<(error: unknown) => void>();
|
|
13
8
|
using _reportError = testStubGlobal("reportError", reportError);
|
|
14
9
|
await using run = createRun();
|
|
@@ -17,13 +12,7 @@ describe("createRun", () => {
|
|
|
17
12
|
run.panic(defect);
|
|
18
13
|
|
|
19
14
|
assertEqual(reportError.mock.callCount(), 1);
|
|
20
|
-
|
|
21
|
-
assertNonNullable(reported);
|
|
22
|
-
assertTrue(typeof reported === "object");
|
|
23
|
-
assertEqual(Reflect.get(reported, "reason"), {
|
|
24
|
-
type: "PanicAbortReason",
|
|
25
|
-
defect,
|
|
26
|
-
});
|
|
15
|
+
assertSame(reportError.mock.calls[0]?.arguments[0], defect);
|
|
27
16
|
});
|
|
28
17
|
|
|
29
18
|
it("createRun preserves a custom reportDefect", async () => {
|
package/src/Task.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
createRun as createCommonRun,
|
|
9
|
+
defectToError,
|
|
9
10
|
type DisposableRun,
|
|
10
11
|
type Run,
|
|
11
12
|
type RunCustomDeps,
|
|
@@ -14,13 +15,16 @@ import {
|
|
|
14
15
|
/**
|
|
15
16
|
* Creates a root {@link Run} for the browser.
|
|
16
17
|
*
|
|
17
|
-
* Defects are reported with the browser's global `reportError
|
|
18
|
-
*
|
|
18
|
+
* Defects are reported with the browser's global `reportError` as an `Error`
|
|
19
|
+
* from {@link defectToError}, because browsers show any other value only as
|
|
20
|
+
* unhelpful text. A custom `reportDefect` dependency overrides the browser
|
|
21
|
+
* default.
|
|
19
22
|
*
|
|
20
23
|
* ### Example
|
|
21
24
|
*
|
|
22
25
|
* ```ts
|
|
23
26
|
* import {
|
|
27
|
+
* assertEqual,
|
|
24
28
|
* createConsole,
|
|
25
29
|
* createConsoleFormatter,
|
|
26
30
|
* ok,
|
|
@@ -36,7 +40,7 @@ import {
|
|
|
36
40
|
* await using run = createRun({ console });
|
|
37
41
|
* const appPromise = run.ok(() => ok("started"));
|
|
38
42
|
*
|
|
39
|
-
*
|
|
43
|
+
* assertEqual(await appPromise, "started");
|
|
40
44
|
* ```
|
|
41
45
|
*/
|
|
42
46
|
export function createRun(): DisposableRun;
|
|
@@ -49,7 +53,7 @@ export function createRun<D extends object>(
|
|
|
49
53
|
deps?: RunCustomDeps<D>,
|
|
50
54
|
): DisposableRun | DisposableRun<D> {
|
|
51
55
|
const reportDefect = (reported: unknown): void => {
|
|
52
|
-
reportError(reported);
|
|
56
|
+
reportError(defectToError(reported));
|
|
53
57
|
};
|
|
54
58
|
|
|
55
59
|
return deps === undefined
|