@evolu/web 3.0.2 → 3.1.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/README.md +7 -1
- package/dist/src/Platform.d.ts +2 -0
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +6 -1
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +10 -4
- package/dist/src/Task.js +1 -1
- package/dist/src/Worker.d.ts +3 -3
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +11 -2
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/local-first/Db.worker.js +1 -1
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +2 -2
- package/dist/src/local-first/LocalAuth.js +6 -3
- package/dist/src/local-first/Shared.worker.js +2 -2
- package/package.json +6 -5
- package/src/Platform.test.ts +45 -0
- package/src/Platform.ts +8 -1
- package/src/Sqlite.test.ts +356 -0
- package/src/Sqlite.ts +16 -4
- package/src/Task.test.ts +51 -0
- package/src/Task.ts +1 -1
- package/src/Worker.test.ts +473 -0
- package/src/Worker.ts +15 -6
- package/src/index.ts +1 -1
- package/src/local-first/Db.worker.ts +1 -1
- package/src/local-first/Evolu.test.ts +50 -0
- package/src/local-first/Evolu.ts +2 -3
- package/src/local-first/LocalAuth.ts +6 -3
- package/src/local-first/Shared.worker.ts +2 -2
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertEqual,
|
|
3
|
+
assertEqualBytes,
|
|
4
|
+
assertTrue,
|
|
5
|
+
EncryptionKey,
|
|
6
|
+
Name,
|
|
7
|
+
sql,
|
|
8
|
+
testCreateRun,
|
|
9
|
+
} from "@evolu/common";
|
|
10
|
+
import { describe, it, mock } from "node:test";
|
|
11
|
+
|
|
12
|
+
const sqliteMock = (() => {
|
|
13
|
+
class PreparedStatement {
|
|
14
|
+
finalized = false;
|
|
15
|
+
resetCount = 0;
|
|
16
|
+
stepCount = 0;
|
|
17
|
+
readonly bound: Array<ReadonlyArray<unknown>> = [];
|
|
18
|
+
|
|
19
|
+
bind(parameters: ReadonlyArray<unknown>): void {
|
|
20
|
+
this.bound.push(parameters);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
step(): boolean {
|
|
24
|
+
this.stepCount += 1;
|
|
25
|
+
return this.stepCount === 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get(): Record<string, unknown> {
|
|
29
|
+
return { data: "prepared" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
reset(): void {
|
|
33
|
+
this.resetCount += 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
finalize(): void {
|
|
37
|
+
this.finalized = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class Database {
|
|
42
|
+
readonly execSql: Array<string> = [];
|
|
43
|
+
readonly filename: string;
|
|
44
|
+
|
|
45
|
+
constructor(filename: string) {
|
|
46
|
+
this.filename = filename;
|
|
47
|
+
state.createdDatabases.push(this);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
prepare(): PreparedStatement {
|
|
51
|
+
const statement = new PreparedStatement();
|
|
52
|
+
state.preparedStatements.push(statement);
|
|
53
|
+
return statement;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
exec(sql: string): ReadonlyArray<Record<string, unknown>> {
|
|
57
|
+
this.execSql.push(sql);
|
|
58
|
+
return [{ data: "row" }];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
changes(): number {
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
close(): void {
|
|
66
|
+
state.closedDatabases.push(this.filename);
|
|
67
|
+
state.events.push(`close:${this.filename}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const state = {
|
|
72
|
+
closedDatabases: [] as Array<string>,
|
|
73
|
+
createdDatabases: [] as Array<Database>,
|
|
74
|
+
deletedFilenames: [] as Array<string>,
|
|
75
|
+
events: [] as Array<string>,
|
|
76
|
+
pausedVfsNames: [] as Array<string>,
|
|
77
|
+
preparedStatements: [] as Array<PreparedStatement>,
|
|
78
|
+
unpausedVfsNames: [] as Array<string>,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
let poolPaused = false;
|
|
82
|
+
const pool = {
|
|
83
|
+
isPaused: mock.fn(() => poolPaused),
|
|
84
|
+
OpfsSAHPoolDb: Database,
|
|
85
|
+
pauseVfs: mock.fn(() => {
|
|
86
|
+
poolPaused = true;
|
|
87
|
+
state.pausedVfsNames.push(pool.vfsName);
|
|
88
|
+
state.events.push(`pause:${pool.vfsName}`);
|
|
89
|
+
return pool;
|
|
90
|
+
}),
|
|
91
|
+
unpauseVfs: mock.fn(() => {
|
|
92
|
+
poolPaused = false;
|
|
93
|
+
state.unpausedVfsNames.push(pool.vfsName);
|
|
94
|
+
state.events.push(`unpause:${pool.vfsName}`);
|
|
95
|
+
return Promise.resolve(pool);
|
|
96
|
+
}),
|
|
97
|
+
unlink: mock.fn((filename: string) => {
|
|
98
|
+
state.deletedFilenames.push(filename);
|
|
99
|
+
state.events.push(`unlink:${filename}`);
|
|
100
|
+
return true;
|
|
101
|
+
}),
|
|
102
|
+
vfsName: "mock-sahpool",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const sqlite3 = {
|
|
106
|
+
capi: {
|
|
107
|
+
sqlite3_js_db_export: mock.fn(() => new Uint8Array([1, 2, 3])),
|
|
108
|
+
sqlite3mc_vfs_create: mock.fn(),
|
|
109
|
+
},
|
|
110
|
+
installOpfsSAHPoolVfs: mock.fn(() => Promise.resolve(pool)),
|
|
111
|
+
oo1: { DB: Database },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
consoleWarn: mock.fn<typeof console.warn>(),
|
|
116
|
+
pool,
|
|
117
|
+
reset: () => {
|
|
118
|
+
state.closedDatabases.length = 0;
|
|
119
|
+
state.createdDatabases.length = 0;
|
|
120
|
+
state.deletedFilenames.length = 0;
|
|
121
|
+
state.events.length = 0;
|
|
122
|
+
state.pausedVfsNames.length = 0;
|
|
123
|
+
state.preparedStatements.length = 0;
|
|
124
|
+
state.unpausedVfsNames.length = 0;
|
|
125
|
+
poolPaused = false;
|
|
126
|
+
pool.isPaused.mock.resetCalls();
|
|
127
|
+
pool.pauseVfs.mock.resetCalls();
|
|
128
|
+
pool.unpauseVfs.mock.resetCalls();
|
|
129
|
+
pool.unlink.mock.resetCalls();
|
|
130
|
+
sqlite3.capi.sqlite3_js_db_export.mock.resetCalls();
|
|
131
|
+
sqlite3.capi.sqlite3mc_vfs_create.mock.resetCalls();
|
|
132
|
+
sqlite3.installOpfsSAHPoolVfs.mock.resetCalls();
|
|
133
|
+
},
|
|
134
|
+
sqlite3,
|
|
135
|
+
state,
|
|
136
|
+
};
|
|
137
|
+
})();
|
|
138
|
+
|
|
139
|
+
mock.module("@evolu/sqlite-wasm", {
|
|
140
|
+
// @ts-expect-error -- Node.js 24.20 replaces the deprecated defaultExport option with exports, which @types/node 24.13 does not declare yet.
|
|
141
|
+
exports: {
|
|
142
|
+
default: mock.fn(() => {
|
|
143
|
+
const config = Reflect.get(globalThis, "sqlite3ApiConfig") as
|
|
144
|
+
| {
|
|
145
|
+
readonly warn?: (arg: unknown) => void;
|
|
146
|
+
}
|
|
147
|
+
| undefined;
|
|
148
|
+
config?.warn?.("Ignoring inability to install OPFS sqlite3_vfs");
|
|
149
|
+
config?.warn?.("kept warning");
|
|
150
|
+
return Promise.resolve(sqliteMock.sqlite3);
|
|
151
|
+
}),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
mock.method(console, "warn", sqliteMock.consoleWarn);
|
|
156
|
+
const { createWasmSqliteDriver } = await import("./Sqlite.ts");
|
|
157
|
+
|
|
158
|
+
describe("createWasmSqliteDriver coverage helpers", () => {
|
|
159
|
+
it("filters sqlite init warnings", () => {
|
|
160
|
+
assertEqual(
|
|
161
|
+
sqliteMock.consoleWarn.mock.calls.map(({ arguments: args }) => args),
|
|
162
|
+
[["kept warning"]],
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("opens plain OPFS SAH-pool database", async () => {
|
|
167
|
+
sqliteMock.reset();
|
|
168
|
+
|
|
169
|
+
await using run = testCreateRun();
|
|
170
|
+
using _driver = await run.ok(
|
|
171
|
+
createWasmSqliteDriver(Name.orThrow("MockPlain")),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
assertEqual(
|
|
175
|
+
sqliteMock.sqlite3.installOpfsSAHPoolVfs.mock.calls.map(
|
|
176
|
+
({ arguments: args }) => args,
|
|
177
|
+
),
|
|
178
|
+
[[{ name: "MockPlain" }]],
|
|
179
|
+
);
|
|
180
|
+
assertEqual(
|
|
181
|
+
sqliteMock.state.createdDatabases[0]?.filename,
|
|
182
|
+
"file:evolu1.db",
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("opens memory database", async () => {
|
|
187
|
+
sqliteMock.reset();
|
|
188
|
+
|
|
189
|
+
await using run = testCreateRun();
|
|
190
|
+
using _driver = await run.ok(
|
|
191
|
+
createWasmSqliteDriver(Name.orThrow("MockMemory"), { mode: "memory" }),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
assertEqual(sqliteMock.sqlite3.installOpfsSAHPoolVfs.mock.callCount(), 0);
|
|
195
|
+
assertEqual(sqliteMock.state.createdDatabases[0]?.filename, ":memory:");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("executes non-prepared query and exports database", async () => {
|
|
199
|
+
sqliteMock.reset();
|
|
200
|
+
|
|
201
|
+
await using run = testCreateRun();
|
|
202
|
+
using driver = await run.ok(
|
|
203
|
+
createWasmSqliteDriver(Name.orThrow("MockPlain")),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const result = driver.exec(sql`select ${"row"};`);
|
|
207
|
+
const exported = driver.export();
|
|
208
|
+
|
|
209
|
+
assertEqual(result, { rows: [{ data: "row" }], changes: 1 });
|
|
210
|
+
assertEqualBytes(exported, [1, 2, 3]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("executes prepared query and finalizes statement on dispose", async () => {
|
|
214
|
+
sqliteMock.reset();
|
|
215
|
+
|
|
216
|
+
await using run = testCreateRun();
|
|
217
|
+
{
|
|
218
|
+
using driver = await run.ok(
|
|
219
|
+
createWasmSqliteDriver(Name.orThrow("MockPlain")),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const result = driver.exec({
|
|
223
|
+
...sql`select ${"prepared"};`,
|
|
224
|
+
options: { prepare: true },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
assertEqual(result, { rows: [{ data: "prepared" }], changes: 1 });
|
|
228
|
+
|
|
229
|
+
driver.exec({ ...sql`select 1;`, options: { prepare: true } });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
assertEqual(sqliteMock.state.preparedStatements[0]?.bound, [["prepared"]]);
|
|
233
|
+
assertEqual(sqliteMock.state.preparedStatements[0]?.resetCount, 1);
|
|
234
|
+
assertEqual(sqliteMock.state.preparedStatements[0]?.finalized, true);
|
|
235
|
+
assertEqual(sqliteMock.state.preparedStatements[1]?.bound, []);
|
|
236
|
+
assertEqual(sqliteMock.state.preparedStatements[1]?.finalized, true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("closes OPFS database on dispose", async () => {
|
|
240
|
+
sqliteMock.reset();
|
|
241
|
+
|
|
242
|
+
await using run = testCreateRun();
|
|
243
|
+
{
|
|
244
|
+
using _driver = await run.ok(
|
|
245
|
+
createWasmSqliteDriver(Name.orThrow("MockPlain")),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
assertEqual(sqliteMock.state.closedDatabases, ["file:evolu1.db"]);
|
|
250
|
+
assertEqual(sqliteMock.state.pausedVfsNames, ["mock-sahpool"]);
|
|
251
|
+
assertEqual(sqliteMock.pool.unlink.mock.callCount(), 0);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("unpauses OPFS SAH-pool database on reopen", async () => {
|
|
255
|
+
sqliteMock.reset();
|
|
256
|
+
|
|
257
|
+
await using run = testCreateRun();
|
|
258
|
+
{
|
|
259
|
+
using _driver = await run.ok(
|
|
260
|
+
createWasmSqliteDriver(Name.orThrow("MockPlain")),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
using _driver = await run.ok(
|
|
265
|
+
createWasmSqliteDriver(Name.orThrow("MockPlain")),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
assertEqual(sqliteMock.state.unpausedVfsNames, ["mock-sahpool"]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("deletes plain OPFS database file", async () => {
|
|
272
|
+
sqliteMock.reset();
|
|
273
|
+
|
|
274
|
+
await using run = testCreateRun();
|
|
275
|
+
using driver = await run.ok(
|
|
276
|
+
createWasmSqliteDriver(Name.orThrow("MockPlain")),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
driver.deleteDatabase();
|
|
280
|
+
|
|
281
|
+
assertEqual(sqliteMock.state.closedDatabases, ["file:evolu1.db"]);
|
|
282
|
+
assertEqual(sqliteMock.state.deletedFilenames, ["/evolu1.db"]);
|
|
283
|
+
assertEqual(sqliteMock.state.events, [
|
|
284
|
+
"close:file:evolu1.db",
|
|
285
|
+
"unlink:/evolu1.db",
|
|
286
|
+
"pause:mock-sahpool",
|
|
287
|
+
]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("deletes memory database", async () => {
|
|
291
|
+
sqliteMock.reset();
|
|
292
|
+
|
|
293
|
+
await using run = testCreateRun();
|
|
294
|
+
using driver = await run.ok(
|
|
295
|
+
createWasmSqliteDriver(Name.orThrow("MockMemory"), { mode: "memory" }),
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
driver.deleteDatabase();
|
|
299
|
+
|
|
300
|
+
assertEqual(sqliteMock.state.events, ["close::memory:"]);
|
|
301
|
+
assertEqual(sqliteMock.pool.unlink.mock.callCount(), 0);
|
|
302
|
+
assertEqual(sqliteMock.pool.pauseVfs.mock.callCount(), 0);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("deletes encrypted OPFS database file", async () => {
|
|
306
|
+
sqliteMock.reset();
|
|
307
|
+
|
|
308
|
+
await using run = testCreateRun();
|
|
309
|
+
using driver = await run.ok(
|
|
310
|
+
createWasmSqliteDriver(Name.orThrow("MockEncrypted"), {
|
|
311
|
+
mode: "encrypted",
|
|
312
|
+
encryptionKey: EncryptionKey.orThrow(new Uint8Array(32).fill(42)),
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
driver.deleteDatabase();
|
|
317
|
+
|
|
318
|
+
assertEqual(sqliteMock.state.closedDatabases, [
|
|
319
|
+
"file:evolu1.db?vfs=multipleciphers-opfs-sahpool",
|
|
320
|
+
]);
|
|
321
|
+
assertEqual(sqliteMock.state.deletedFilenames, ["/evolu1.db"]);
|
|
322
|
+
assertEqual(sqliteMock.state.events, [
|
|
323
|
+
"close:file:evolu1.db?vfs=multipleciphers-opfs-sahpool",
|
|
324
|
+
"unlink:/evolu1.db",
|
|
325
|
+
"pause:mock-sahpool",
|
|
326
|
+
]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("configures encrypted OPFS database", async () => {
|
|
330
|
+
sqliteMock.reset();
|
|
331
|
+
|
|
332
|
+
await using run = testCreateRun();
|
|
333
|
+
using _driver = await run.ok(
|
|
334
|
+
createWasmSqliteDriver(Name.orThrow("MockEncrypted"), {
|
|
335
|
+
mode: "encrypted",
|
|
336
|
+
encryptionKey: EncryptionKey.orThrow(new Uint8Array(32).fill(42)),
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
assertEqual(
|
|
341
|
+
sqliteMock.sqlite3.installOpfsSAHPoolVfs.mock.calls.map(
|
|
342
|
+
({ arguments: args }) => args,
|
|
343
|
+
),
|
|
344
|
+
[[{ directory: ".MockEncrypted" }]],
|
|
345
|
+
);
|
|
346
|
+
assertEqual(
|
|
347
|
+
sqliteMock.state.createdDatabases[0]?.filename,
|
|
348
|
+
"file:evolu1.db?vfs=multipleciphers-opfs-sahpool",
|
|
349
|
+
);
|
|
350
|
+
assertTrue(
|
|
351
|
+
sqliteMock.state.createdDatabases[0]?.execSql[0]?.includes(
|
|
352
|
+
"PRAGMA cipher = 'sqlcipher';",
|
|
353
|
+
),
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
});
|
package/src/Sqlite.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { CreateSqliteDriver, SqliteRow } from "@evolu/common";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
bytesToHex,
|
|
4
|
+
createPreparedStatementsCache,
|
|
5
|
+
exhaustiveCheck,
|
|
6
|
+
ok,
|
|
7
|
+
} from "@evolu/common";
|
|
3
8
|
import sqlite3InitModule, {
|
|
4
9
|
type Database,
|
|
5
10
|
type PreparedStatement,
|
|
@@ -16,7 +21,7 @@ globalThis.sqlite3ApiConfig = {
|
|
|
16
21
|
arg.startsWith("Ignoring inability to install OPFS sqlite3_vfs")
|
|
17
22
|
)
|
|
18
23
|
return;
|
|
19
|
-
//
|
|
24
|
+
// oxlint-disable-next-line eslint/no-console
|
|
20
25
|
console.warn(arg);
|
|
21
26
|
},
|
|
22
27
|
};
|
|
@@ -53,17 +58,19 @@ export const createWasmSqliteDriver: CreateSqliteDriver =
|
|
|
53
58
|
|
|
54
59
|
switch (options?.mode) {
|
|
55
60
|
case "memory":
|
|
61
|
+
// oxlint-disable-next-line react/rules-of-hooks -- useDatabase registers disposal and is not a React Hook.
|
|
56
62
|
db = useDatabase(new sqlite3.oo1.DB(":memory:"));
|
|
57
63
|
break;
|
|
58
64
|
|
|
59
65
|
case "encrypted": {
|
|
60
66
|
// MultipleCiphers encryption requires its VFS wrapper for OPFS SAH-pool.
|
|
61
67
|
// @ts-expect-error Missing types (update @evolu/sqlite-wasm types)
|
|
62
|
-
//
|
|
68
|
+
// oxlint-disable-next-line typescript/no-unsafe-call
|
|
63
69
|
sqlite3.capi.sqlite3mc_vfs_create("opfs", 1);
|
|
64
70
|
const pool = await createOpfsSAHPoolVfs({
|
|
65
71
|
directory: `.${name}`,
|
|
66
72
|
});
|
|
73
|
+
// oxlint-disable-next-line react/rules-of-hooks -- useDatabase registers disposal and is not a React Hook.
|
|
67
74
|
db = useDatabase(
|
|
68
75
|
new pool.OpfsSAHPoolDb(
|
|
69
76
|
// SQLite normalizes this URI filename to SAH-pool path "/evolu1.db".
|
|
@@ -77,10 +84,15 @@ export const createWasmSqliteDriver: CreateSqliteDriver =
|
|
|
77
84
|
break;
|
|
78
85
|
}
|
|
79
86
|
|
|
80
|
-
|
|
87
|
+
case undefined: {
|
|
81
88
|
const pool = await createOpfsSAHPoolVfs({ name });
|
|
89
|
+
// oxlint-disable-next-line react/rules-of-hooks -- useDatabase registers disposal and is not a React Hook.
|
|
82
90
|
db = useDatabase(new pool.OpfsSAHPoolDb(`file:${fileName}`));
|
|
91
|
+
break;
|
|
83
92
|
}
|
|
93
|
+
|
|
94
|
+
default:
|
|
95
|
+
exhaustiveCheck(options);
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
const cache = disposer.use(
|
package/src/Task.test.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertEqual,
|
|
3
|
+
assertNonNullable,
|
|
4
|
+
assertTrue,
|
|
5
|
+
testStubGlobal,
|
|
6
|
+
} from "@evolu/common";
|
|
7
|
+
import { describe, it, mock } from "node:test";
|
|
8
|
+
import { createRun } from "./Task.ts";
|
|
9
|
+
|
|
10
|
+
describe("createRun", () => {
|
|
11
|
+
it("createRun reports defects with global reportError", async () => {
|
|
12
|
+
const reportError = mock.fn<(error: unknown) => void>();
|
|
13
|
+
using _reportError = testStubGlobal("reportError", reportError);
|
|
14
|
+
await using run = createRun();
|
|
15
|
+
const defect = new Error("boom");
|
|
16
|
+
|
|
17
|
+
run.panic(defect);
|
|
18
|
+
|
|
19
|
+
assertEqual(reportError.mock.callCount(), 1);
|
|
20
|
+
const reported = reportError.mock.calls[0]?.arguments[0];
|
|
21
|
+
assertNonNullable(reported);
|
|
22
|
+
assertTrue(typeof reported === "object");
|
|
23
|
+
assertEqual(Reflect.get(reported, "reason"), {
|
|
24
|
+
type: "PanicAbortReason",
|
|
25
|
+
defect,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("createRun preserves a custom reportDefect", async () => {
|
|
30
|
+
const reportError = mock.fn();
|
|
31
|
+
const reportDefect = mock.fn();
|
|
32
|
+
using _reportError = testStubGlobal("reportError", reportError);
|
|
33
|
+
await using run = createRun({ reportDefect });
|
|
34
|
+
const defect = new Error("boom");
|
|
35
|
+
|
|
36
|
+
run.panic(defect);
|
|
37
|
+
|
|
38
|
+
assertEqual(reportDefect.mock.callCount(), 1);
|
|
39
|
+
assertEqual(reportError.mock.callCount(), 0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("merges custom deps", async () => {
|
|
43
|
+
interface CustomDep {
|
|
44
|
+
readonly customValue: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await using run = createRun<CustomDep>({ customValue: 42 });
|
|
48
|
+
|
|
49
|
+
assertEqual(run.deps.customValue, 42);
|
|
50
|
+
});
|
|
51
|
+
});
|
package/src/Task.ts
CHANGED
|
@@ -49,7 +49,7 @@ export function createRun<D extends object>(
|
|
|
49
49
|
deps?: RunCustomDeps<D>,
|
|
50
50
|
): DisposableRun | DisposableRun<D> {
|
|
51
51
|
const reportDefect = (reported: unknown): void => {
|
|
52
|
-
|
|
52
|
+
reportError(reported);
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
return deps === undefined
|