@evolu/common 8.15.1 → 8.17.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/Console.d.ts +24 -46
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +34 -29
- package/dist/src/Crypto.d.ts +21 -3
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +18 -2
- package/dist/src/Error.d.ts +10 -6
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +52 -20
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +16 -12
- package/dist/src/Task.d.ts +3 -1
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +15 -13
- package/dist/src/Worker.d.ts +3 -0
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +6 -1
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +7 -7
- package/dist/src/local-first/Owner.d.ts +20 -8
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +23 -10
- package/dist/src/local-first/Schema.d.ts +1 -1
- package/dist/src/local-first/Shared.d.ts +55 -39
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +264 -318
- package/package.json +1 -1
- package/src/Console.test.ts +75 -247
- package/src/Console.ts +68 -110
- package/src/Crypto.ts +23 -3
- package/src/Error.test.ts +85 -0
- package/src/Error.ts +48 -22
- package/src/Sqlite.ts +19 -11
- package/src/Task.test.ts +46 -0
- package/src/Task.ts +17 -13
- package/src/Worker.ts +6 -1
- package/src/local-first/Db.ts +12 -8
- package/src/local-first/Owner.test.ts +52 -1
- package/src/local-first/Owner.ts +26 -7
- package/src/local-first/Shared.test.ts +414 -39
- package/src/local-first/Shared.ts +386 -438
package/src/Console.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { objectFrom } from "./Object.ts";
|
|
8
7
|
import type { ReadonlyStore } from "./Store.ts";
|
|
9
8
|
import { createStore } from "./Store.ts";
|
|
10
9
|
import type { Task } from "./Task.ts";
|
|
@@ -54,9 +53,7 @@ import {
|
|
|
54
53
|
* createConsole,
|
|
55
54
|
* createConsoleArrayOutput,
|
|
56
55
|
* Data,
|
|
57
|
-
* type Console,
|
|
58
56
|
* type ConsoleEntry,
|
|
59
|
-
* type ConsoleLevel,
|
|
60
57
|
* } from "@evolu/common";
|
|
61
58
|
*
|
|
62
59
|
* const entries: Array<ConsoleEntry> = [];
|
|
@@ -75,16 +72,11 @@ import {
|
|
|
75
72
|
* assertType(Data, args);
|
|
76
73
|
* assertEqual(args, ["Creating instance", { config: { port: 4000 } }]);
|
|
77
74
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* )
|
|
82
|
-
*
|
|
83
|
-
* for (const child of console.children) setLevelRecursive(child, level);
|
|
84
|
-
* };
|
|
85
|
-
* setLevelRecursive(root, "warn");
|
|
86
|
-
* assertEqual(root.getLevel(), "warn");
|
|
87
|
-
* assertEqual(relay.getLevel(), "warn");
|
|
75
|
+
* // Children without their own level follow their parent.
|
|
76
|
+
* const db = root.child("db");
|
|
77
|
+
* root.setLevel("warn");
|
|
78
|
+
* assertEqual(db.getLevel(), "warn");
|
|
79
|
+
* assertEqual(relay.getLevel(), "silent");
|
|
88
80
|
* ```
|
|
89
81
|
*
|
|
90
82
|
* Console intentionally does not use {@link Task}. Logging must be as fast as
|
|
@@ -97,17 +89,12 @@ import {
|
|
|
97
89
|
* @see {@link createConsole}
|
|
98
90
|
*/
|
|
99
91
|
export interface Console {
|
|
100
|
-
/** Name of this console. Empty for root. */
|
|
101
|
-
readonly name: string;
|
|
102
|
-
|
|
103
|
-
/** Child consoles created via {@link Console.child}. */
|
|
104
|
-
readonly children: ReadonlySet<Console>;
|
|
105
|
-
|
|
106
92
|
/**
|
|
107
93
|
* Returns the effective log level.
|
|
108
94
|
*
|
|
109
95
|
* If this console has its own level set via {@link Console.setLevel}, returns
|
|
110
|
-
* that. Otherwise returns
|
|
96
|
+
* that. Otherwise returns its parent's current level, or
|
|
97
|
+
* {@link ConsoleConfig.level} for a root console.
|
|
111
98
|
*/
|
|
112
99
|
readonly getLevel: () => ConsoleLevel;
|
|
113
100
|
|
|
@@ -125,12 +112,12 @@ export interface Console {
|
|
|
125
112
|
/**
|
|
126
113
|
* Creates a child console with the given name added to the path.
|
|
127
114
|
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
115
|
+
* A child without its own level follows its parent's level, including later
|
|
116
|
+
* changes.
|
|
130
117
|
*/
|
|
131
118
|
readonly child: (name: string) => Console;
|
|
132
119
|
|
|
133
|
-
/**
|
|
120
|
+
/** Most detailed execution flow, such as every SQL query. */
|
|
134
121
|
readonly trace: (...args: ReadonlyArray<unknown>) => void;
|
|
135
122
|
|
|
136
123
|
/** Development diagnostics. */
|
|
@@ -148,31 +135,10 @@ export interface Console {
|
|
|
148
135
|
/** Failures requiring immediate attention. */
|
|
149
136
|
readonly error: (...args: ReadonlyArray<unknown>) => void;
|
|
150
137
|
|
|
151
|
-
/** Displays an object with expandable properties. Level: debug. */
|
|
152
|
-
readonly dir: (item: unknown) => void;
|
|
153
|
-
|
|
154
|
-
/** Displays tabular data. Level: debug. */
|
|
155
|
-
readonly table: (data: unknown) => void;
|
|
156
|
-
|
|
157
|
-
/** Starts a timer with the given label. Level: debug. */
|
|
158
|
-
readonly time: (label: string) => void;
|
|
159
|
-
|
|
160
|
-
/** Logs elapsed time for a timer. Level: debug. */
|
|
161
|
-
readonly timeLog: (label: string, ...args: ReadonlyArray<unknown>) => void;
|
|
162
|
-
|
|
163
|
-
/** Ends a timer and logs elapsed time. Level: debug. */
|
|
164
|
-
readonly timeEnd: (label: string) => void;
|
|
165
|
-
|
|
166
|
-
/** Increments and logs a counter. Level: debug. */
|
|
167
|
-
readonly count: (label?: string) => void;
|
|
168
|
-
|
|
169
|
-
/** Resets a counter. Level: debug. */
|
|
170
|
-
readonly countReset: (label?: string) => void;
|
|
171
|
-
|
|
172
138
|
/**
|
|
173
|
-
* Writes a pre-built {@link ConsoleEntry}
|
|
174
|
-
* level
|
|
175
|
-
*
|
|
139
|
+
* Writes a pre-built {@link ConsoleEntry} to the output if this console's
|
|
140
|
+
* level allows its method. Used to replay entries from another context, such
|
|
141
|
+
* as a worker, that filtered them by its own level.
|
|
176
142
|
*/
|
|
177
143
|
readonly write: (entry: ConsoleEntry) => void;
|
|
178
144
|
}
|
|
@@ -192,8 +158,8 @@ export interface ConsoleDep {
|
|
|
192
158
|
* Setting a level enables all logs at that level and above (ordered by
|
|
193
159
|
* severity):
|
|
194
160
|
*
|
|
195
|
-
* - `"trace"` —
|
|
196
|
-
* - `"debug"` — Development diagnostics
|
|
161
|
+
* - `"trace"` — Most detailed execution flow, such as every SQL query
|
|
162
|
+
* - `"debug"` — Development diagnostics
|
|
197
163
|
* - `"log"` — General-purpose messages
|
|
198
164
|
* - `"info"` — Operational milestones (startup, shutdown)
|
|
199
165
|
* - `"warn"` — Recoverable issues that may need attention
|
|
@@ -233,19 +199,7 @@ export interface ConsoleEntry {
|
|
|
233
199
|
* @group Core
|
|
234
200
|
*/
|
|
235
201
|
export type ConsoleMethod =
|
|
236
|
-
| "
|
|
237
|
-
| "debug"
|
|
238
|
-
| "log"
|
|
239
|
-
| "info"
|
|
240
|
-
| "warn"
|
|
241
|
-
| "error"
|
|
242
|
-
| "dir"
|
|
243
|
-
| "table"
|
|
244
|
-
| "time"
|
|
245
|
-
| "timeLog"
|
|
246
|
-
| "timeEnd"
|
|
247
|
-
| "count"
|
|
248
|
-
| "countReset";
|
|
202
|
+
"trace" | "debug" | "log" | "info" | "warn" | "error";
|
|
249
203
|
|
|
250
204
|
/**
|
|
251
205
|
* Output destination for {@link Console}.
|
|
@@ -278,9 +232,6 @@ export type ConsoleFormatter = (entry: ConsoleEntry) => ReadonlyArray<unknown>;
|
|
|
278
232
|
* @group Core
|
|
279
233
|
*/
|
|
280
234
|
export interface ConsoleConfig {
|
|
281
|
-
/** Name of this console. Defaults to empty string. */
|
|
282
|
-
readonly name?: string;
|
|
283
|
-
|
|
284
235
|
/** Initial log level. Defaults to `"log"`. */
|
|
285
236
|
readonly level?: ConsoleLevel;
|
|
286
237
|
|
|
@@ -320,7 +271,8 @@ export interface ConsoleFormatterConfig {
|
|
|
320
271
|
readonly timestampFormat?: ConsoleEntryTimestampFormat;
|
|
321
272
|
|
|
322
273
|
/**
|
|
323
|
-
* Start time for relative timestamps. Defaults to first entry
|
|
274
|
+
* Start time for relative timestamps. Defaults to the time the first entry is
|
|
275
|
+
* formatted.
|
|
324
276
|
*
|
|
325
277
|
* Pass a {@link Millis} value to use a custom start time, useful when multiple
|
|
326
278
|
* consoles should share the same relative timeline.
|
|
@@ -427,65 +379,66 @@ const levelOrder: Record<ConsoleLevel, number> = {
|
|
|
427
379
|
* @group Core
|
|
428
380
|
*/
|
|
429
381
|
export const createConsole = ({
|
|
430
|
-
name = "",
|
|
431
382
|
level = "log",
|
|
432
383
|
output = createNativeConsoleOutput(),
|
|
433
384
|
path = [],
|
|
434
385
|
formatter,
|
|
435
|
-
}: ConsoleConfig = {}): Console =>
|
|
436
|
-
|
|
386
|
+
}: ConsoleConfig = {}): Console =>
|
|
387
|
+
createConsoleWithInheritedLevel({
|
|
388
|
+
getInheritedLevel: () => level,
|
|
389
|
+
output,
|
|
390
|
+
path,
|
|
391
|
+
formatter,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const createConsoleWithInheritedLevel = ({
|
|
395
|
+
getInheritedLevel,
|
|
396
|
+
output,
|
|
397
|
+
path,
|
|
398
|
+
formatter,
|
|
399
|
+
}: {
|
|
400
|
+
getInheritedLevel: () => ConsoleLevel;
|
|
401
|
+
output: ConsoleOutput;
|
|
402
|
+
path: ReadonlyArray<string>;
|
|
403
|
+
formatter: ConsoleFormatter | undefined;
|
|
404
|
+
}): Console => {
|
|
437
405
|
let ownLevel: ConsoleLevel | null = null;
|
|
438
406
|
|
|
439
|
-
const getLevel = (): ConsoleLevel => ownLevel ??
|
|
407
|
+
const getLevel = (): ConsoleLevel => ownLevel ?? getInheritedLevel();
|
|
408
|
+
|
|
409
|
+
const isEnabled = (method: ConsoleMethod): boolean =>
|
|
410
|
+
levelOrder[method] >= levelOrder[getLevel()];
|
|
440
411
|
|
|
441
412
|
const createMethod =
|
|
442
|
-
(
|
|
443
|
-
method: ConsoleMethod,
|
|
444
|
-
methodLevel: ConsoleLevel,
|
|
445
|
-
formatter?: ConsoleFormatter,
|
|
446
|
-
) =>
|
|
413
|
+
(method: ConsoleMethod) =>
|
|
447
414
|
(...args: ReadonlyArray<unknown>): void => {
|
|
448
|
-
if (
|
|
449
|
-
output.write({ method, path, args }, formatter);
|
|
415
|
+
if (isEnabled(method)) output.write({ method, path, args }, formatter);
|
|
450
416
|
};
|
|
451
417
|
|
|
452
|
-
const levelMethod = (method: ConsoleLevel & ConsoleMethod) =>
|
|
453
|
-
createMethod(method, method, formatter);
|
|
454
|
-
|
|
455
|
-
const debugMethod = (method: ConsoleMethod) => createMethod(method, "debug");
|
|
456
|
-
|
|
457
418
|
return {
|
|
458
|
-
name,
|
|
459
|
-
children: childrenSet,
|
|
460
419
|
getLevel,
|
|
461
420
|
setLevel: (level) => {
|
|
462
421
|
ownLevel = level;
|
|
463
422
|
},
|
|
464
423
|
hasOwnLevel: () => ownLevel !== null,
|
|
465
424
|
|
|
466
|
-
child: (name) =>
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
level,
|
|
425
|
+
child: (name) =>
|
|
426
|
+
createConsoleWithInheritedLevel({
|
|
427
|
+
getInheritedLevel: getLevel,
|
|
470
428
|
output,
|
|
471
429
|
path: [...path, name],
|
|
472
|
-
|
|
473
|
-
})
|
|
474
|
-
childrenSet.add(childConsole);
|
|
475
|
-
return childConsole;
|
|
476
|
-
},
|
|
430
|
+
formatter,
|
|
431
|
+
}),
|
|
477
432
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
),
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
debugMethod,
|
|
485
|
-
),
|
|
433
|
+
trace: createMethod("trace"),
|
|
434
|
+
debug: createMethod("debug"),
|
|
435
|
+
log: createMethod("log"),
|
|
436
|
+
info: createMethod("info"),
|
|
437
|
+
warn: createMethod("warn"),
|
|
438
|
+
error: createMethod("error"),
|
|
486
439
|
|
|
487
440
|
write: (entry) => {
|
|
488
|
-
output.write(entry, formatter);
|
|
441
|
+
if (isEnabled(entry.method)) output.write(entry, formatter);
|
|
489
442
|
},
|
|
490
443
|
};
|
|
491
444
|
};
|
|
@@ -497,6 +450,9 @@ export const createConsole = ({
|
|
|
497
450
|
* Use {@link createConsoleFormatter} with {@link ConsoleConfig.formatter} for
|
|
498
451
|
* timestamps and path prefixes.
|
|
499
452
|
*
|
|
453
|
+
* Trace entries are written with native `console.debug`, because native
|
|
454
|
+
* `console.trace` prints a stack trace with every call.
|
|
455
|
+
*
|
|
500
456
|
* ### Example
|
|
501
457
|
*
|
|
502
458
|
* ```ts
|
|
@@ -517,11 +473,9 @@ export const createNativeConsoleOutput = (): ConsoleOutput => ({
|
|
|
517
473
|
write: (entry, formatter) => {
|
|
518
474
|
const args = formatter ? formatter(entry) : entry.args;
|
|
519
475
|
// oxlint-disable-next-line evolu/no-unnecessary-global-this -- Write to the global object console even if a realm lexical binding shadows it.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
>;
|
|
524
|
-
nativeConsole[entry.method](...args);
|
|
476
|
+
globalThis.console[entry.method === "trace" ? "debug" : entry.method](
|
|
477
|
+
...args,
|
|
478
|
+
);
|
|
525
479
|
},
|
|
526
480
|
});
|
|
527
481
|
|
|
@@ -592,7 +546,11 @@ export const createConsoleFormatter =
|
|
|
592
546
|
timestamp = "";
|
|
593
547
|
break;
|
|
594
548
|
case "relative":
|
|
595
|
-
|
|
549
|
+
// A clock set back, or a startTime ahead of now, must not print a
|
|
550
|
+
// negative duration.
|
|
551
|
+
timestamp = `+${formatMillisAsDuration(
|
|
552
|
+
Math.max(0, now - startTime) as Millis,
|
|
553
|
+
)}`;
|
|
596
554
|
break;
|
|
597
555
|
case "absolute":
|
|
598
556
|
timestamp = formatMillisAsClockTime(now);
|
|
@@ -709,8 +667,8 @@ export const createMultiOutput = (
|
|
|
709
667
|
/**
|
|
710
668
|
* Creates a {@link TestConsole} that captures all output for testing.
|
|
711
669
|
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
670
|
+
* Captures entries in memory and defaults to the `"trace"` level, so tests see
|
|
671
|
+
* every entry.
|
|
714
672
|
*
|
|
715
673
|
* ### Example
|
|
716
674
|
*
|
package/src/Crypto.ts
CHANGED
|
@@ -102,6 +102,14 @@ type Entropy = typeof Entropy.Output;
|
|
|
102
102
|
export const Entropy16 = /*#__PURE__*/ length(16)(Entropy);
|
|
103
103
|
export type Entropy16 = typeof Entropy16.Output;
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Cryptographic entropy of exactly 20 bytes.
|
|
107
|
+
*
|
|
108
|
+
* @group Random bytes
|
|
109
|
+
*/
|
|
110
|
+
export const Entropy20 = /*#__PURE__*/ length(20)(Entropy);
|
|
111
|
+
export type Entropy20 = typeof Entropy20.Output;
|
|
112
|
+
|
|
105
113
|
/**
|
|
106
114
|
* Cryptographic entropy of exactly 24 bytes.
|
|
107
115
|
*
|
|
@@ -110,6 +118,14 @@ export type Entropy16 = typeof Entropy16.Output;
|
|
|
110
118
|
export const Entropy24 = /*#__PURE__*/ length(24)(Entropy);
|
|
111
119
|
export type Entropy24 = typeof Entropy24.Output;
|
|
112
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Cryptographic entropy of exactly 28 bytes.
|
|
123
|
+
*
|
|
124
|
+
* @group Random bytes
|
|
125
|
+
*/
|
|
126
|
+
export const Entropy28 = /*#__PURE__*/ length(28)(Entropy);
|
|
127
|
+
export type Entropy28 = typeof Entropy28.Output;
|
|
128
|
+
|
|
113
129
|
/**
|
|
114
130
|
* Cryptographic entropy of exactly 32 bytes.
|
|
115
131
|
*
|
|
@@ -150,14 +166,18 @@ export const testCreateRandomBytes = (deps: RandomLibDep): RandomBytes =>
|
|
|
150
166
|
}) as RandomBytes;
|
|
151
167
|
|
|
152
168
|
/**
|
|
153
|
-
*
|
|
169
|
+
* Derives a 32-byte key from a seed and a path using
|
|
170
|
+
* [SLIP-21](https://github.com/satoshilabs/slips/blob/master/slip-0021.md).
|
|
154
171
|
*
|
|
155
|
-
*
|
|
172
|
+
* Derived keys are no stronger than the seed. A 16-byte seed gives 128 bits of
|
|
173
|
+
* security, or about 64 against a quantum computer, whatever the output length.
|
|
174
|
+
* Use a 32-byte random seed for keys that must stay safe against a quantum
|
|
175
|
+
* computer.
|
|
156
176
|
*
|
|
157
177
|
* @group Key derivation
|
|
158
178
|
*/
|
|
159
179
|
export const createSlip21 = (
|
|
160
|
-
seed: Entropy16 | Entropy32 | Entropy64,
|
|
180
|
+
seed: Entropy16 | Entropy20 | Entropy24 | Entropy28 | Entropy32 | Entropy64,
|
|
161
181
|
path: ReadonlyArray<string | number>,
|
|
162
182
|
): Entropy32 => {
|
|
163
183
|
let currentNode = hmac(
|
package/src/Error.test.ts
CHANGED
|
@@ -75,6 +75,91 @@ describe("createUnknownError", () => {
|
|
|
75
75
|
assertFalse("nonClonable" in result.error);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
it("describes error properties that cannot be cloned", () => {
|
|
79
|
+
const error = new Error("Test error", {
|
|
80
|
+
cause: { attempt: 2, retry: () => undefined },
|
|
81
|
+
});
|
|
82
|
+
const result = createUnknownError(error);
|
|
83
|
+
|
|
84
|
+
assertType(Object, result.error);
|
|
85
|
+
assertEqual(result.error.cause, '{"attempt":2}');
|
|
86
|
+
assertEqual(structuredClone(result), result);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("converts errors in an array that cannot be cloned", () => {
|
|
90
|
+
const error = new AggregateError(
|
|
91
|
+
[
|
|
92
|
+
new Error("first", { cause: { retry: () => undefined } }),
|
|
93
|
+
new Error("second"),
|
|
94
|
+
],
|
|
95
|
+
"both",
|
|
96
|
+
);
|
|
97
|
+
const result = createUnknownError(error);
|
|
98
|
+
|
|
99
|
+
assertType(Object, result.error);
|
|
100
|
+
const errors = result.error.errors as ReadonlyArray<{
|
|
101
|
+
readonly message: string;
|
|
102
|
+
}>;
|
|
103
|
+
assertEqual(
|
|
104
|
+
errors.map(({ message }) => message),
|
|
105
|
+
["first", "second"],
|
|
106
|
+
);
|
|
107
|
+
assertEqual(structuredClone(result), result);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("keeps reference cycles through errors and arrays", () => {
|
|
111
|
+
const error = new Error("Test error") as Error & {
|
|
112
|
+
originalError?: unknown;
|
|
113
|
+
items?: ReadonlyArray<unknown>;
|
|
114
|
+
};
|
|
115
|
+
const items: Array<unknown> = [() => undefined];
|
|
116
|
+
items.push(items);
|
|
117
|
+
error.cause = error;
|
|
118
|
+
error.originalError = error;
|
|
119
|
+
error.items = items;
|
|
120
|
+
|
|
121
|
+
const result = structuredClone(createUnknownError(error));
|
|
122
|
+
|
|
123
|
+
assertType(Object, result.error);
|
|
124
|
+
assertSame(result.error.cause, result.error);
|
|
125
|
+
assertSame(result.error.originalError, result.error);
|
|
126
|
+
const convertedItems = result.error.items as ReadonlyArray<unknown>;
|
|
127
|
+
assertSame(convertedItems[0], "() => undefined");
|
|
128
|
+
assertSame(convertedItems[1], convertedItems);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("converts the errors of an AggregateError that can be cloned", () => {
|
|
132
|
+
const inner = globalThis.Object.assign(new Error("connect failed"), {
|
|
133
|
+
code: "ECONNREFUSED",
|
|
134
|
+
});
|
|
135
|
+
const result = structuredClone(
|
|
136
|
+
createUnknownError(new AggregateError([inner], "all failed")),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
assertType(Object, result.error);
|
|
140
|
+
assertEqual(result.error.errors, [
|
|
141
|
+
{
|
|
142
|
+
stack: inner.stack,
|
|
143
|
+
message: "connect failed",
|
|
144
|
+
code: "ECONNREFUSED",
|
|
145
|
+
name: "Error",
|
|
146
|
+
},
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("includes the inherited name and message of a DOMException", () => {
|
|
151
|
+
const result = createUnknownError(
|
|
152
|
+
new Error("write failed", {
|
|
153
|
+
cause: new DOMException("Quota exceeded", "QuotaExceededError"),
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
assertType(Object, result.error);
|
|
158
|
+
assertType(Object, result.error.cause);
|
|
159
|
+
assertEqual(result.error.cause.name, "QuotaExceededError");
|
|
160
|
+
assertEqual(result.error.cause.message, "Quota exceeded");
|
|
161
|
+
});
|
|
162
|
+
|
|
78
163
|
it("handles structured cloneable objects", () => {
|
|
79
164
|
const error = { key: "value" };
|
|
80
165
|
const result = createUnknownError(error);
|
package/src/Error.ts
CHANGED
|
@@ -34,31 +34,57 @@ export interface UnknownError extends InferType<typeof UnknownError> {}
|
|
|
34
34
|
* Creates an {@link UnknownError} from an unknown error.
|
|
35
35
|
*
|
|
36
36
|
* An `Error` becomes a plain object of its own properties, such as `message`
|
|
37
|
-
* and `stack
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
37
|
+
* and `stack`, plus its `name`, `message`, and `stack` when they are inherited,
|
|
38
|
+
* as in a `DOMException`. A property that is an `Error`, such as `cause`, is
|
|
39
|
+
* converted the same way, an array, such as the `errors` of an
|
|
40
|
+
* `AggregateError`, is converted element by element, a function is dropped, and
|
|
41
|
+
* any other property is kept when it can be structured-cloned, or described as
|
|
42
|
+
* a string when it cannot. Reference cycles through errors and arrays stay
|
|
43
|
+
* cycles in the result. An `Error`'s properties are not enumerable, so JSON and
|
|
44
|
+
* comparisons by content miss them, and a structured clone creates a new
|
|
45
|
+
* `Error`. Any other value is structured-cloned, or described by `String` when
|
|
46
|
+
* it cannot be, or as `"[Unserializable Object]"` when even that throws.
|
|
43
47
|
*/
|
|
44
48
|
export const createUnknownError = (error: unknown): UnknownError => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
// An UnknownError is posted between workers, so its values must clone.
|
|
50
|
+
// Each error and array is converted once and registered before its values,
|
|
51
|
+
// so a reference cycle becomes a cycle in the result instead of endless
|
|
52
|
+
// recursion, and structured cloning preserves it.
|
|
53
|
+
const converted = new Map<object, unknown>();
|
|
54
|
+
|
|
55
|
+
const convertValue = (value: unknown): unknown => {
|
|
56
|
+
if (value instanceof Error) return convertError(value);
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
const known = converted.get(value);
|
|
59
|
+
if (known !== undefined) return known;
|
|
60
|
+
const result: Array<unknown> = [];
|
|
61
|
+
converted.set(value, result);
|
|
62
|
+
for (const element of value) result.push(convertValue(element));
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
structuredClone(value);
|
|
67
|
+
return value;
|
|
68
|
+
} catch {
|
|
69
|
+
return safelyStringifyUnknownValue(value);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const convertError = (err: Error): unknown => {
|
|
74
|
+
const known = converted.get(err);
|
|
75
|
+
if (known !== undefined) return known;
|
|
76
|
+
const result: Record<string, unknown> = {};
|
|
77
|
+
converted.set(err, result);
|
|
78
|
+
for (const key of Object.getOwnPropertyNames(err)) {
|
|
49
79
|
const value = (err as never)[key] as unknown;
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// Firefox defines `stack` as a getter on Error.prototype, not as an own
|
|
59
|
-
// property, so getOwnPropertyNames misses it. Explicitly include it.
|
|
60
|
-
if (err.stack !== undefined && !("stack" in result)) {
|
|
61
|
-
result.stack = err.stack;
|
|
80
|
+
if (typeof value !== "function") result[key] = convertValue(value);
|
|
81
|
+
}
|
|
82
|
+
// A DOMException keeps `name` and `message` on its prototype, and Firefox
|
|
83
|
+
// defines `stack` as a getter on Error.prototype, so getOwnPropertyNames
|
|
84
|
+
// misses them. Explicitly include them.
|
|
85
|
+
for (const key of ["name", "message", "stack"] as const) {
|
|
86
|
+
const value = err[key];
|
|
87
|
+
if (value !== undefined && !(key in result)) result[key] = value;
|
|
62
88
|
}
|
|
63
89
|
return result;
|
|
64
90
|
};
|
package/src/Sqlite.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { createMutableRecord, objectToEntries } from "./Object.ts";
|
|
|
14
14
|
import type { Result } from "./Result.ts";
|
|
15
15
|
import { ok } from "./Result.ts";
|
|
16
16
|
import { type Task, testCreateRun, type TestRunDep } from "./Task.ts";
|
|
17
|
+
import { performanceDurationBetween } from "./Time.ts";
|
|
17
18
|
import {
|
|
18
19
|
array,
|
|
19
20
|
type ArrayType,
|
|
@@ -330,7 +331,7 @@ export const createSqlite =
|
|
|
330
331
|
options?: SqliteDriverOptions,
|
|
331
332
|
): Task<Sqlite, never, CreateSqliteDriverDep> =>
|
|
332
333
|
async (run) => {
|
|
333
|
-
const { createSqliteDriver } = run.deps;
|
|
334
|
+
const { createSqliteDriver, time } = run.deps;
|
|
334
335
|
const console = run.deps.console.child("sql");
|
|
335
336
|
await using disposer = new AsyncDisposableStack();
|
|
336
337
|
|
|
@@ -343,16 +344,23 @@ export const createSqlite =
|
|
|
343
344
|
disposable<Sqlite>(
|
|
344
345
|
{
|
|
345
346
|
exec: <R extends SqliteRow = SqliteRow>(query: SqliteQuery) => {
|
|
346
|
-
console.
|
|
347
|
+
console.trace({ query });
|
|
347
348
|
|
|
348
|
-
const
|
|
349
|
+
const start =
|
|
349
350
|
query.options?.logQueryExecutionTime === true
|
|
350
|
-
?
|
|
351
|
+
? time.performance.now()
|
|
351
352
|
: null;
|
|
352
|
-
|
|
353
|
-
if (label !== null) console.time(label);
|
|
354
353
|
const result = driver.exec(query);
|
|
355
|
-
if (
|
|
354
|
+
if (start !== null) {
|
|
355
|
+
const duration = performanceDurationBetween(
|
|
356
|
+
start,
|
|
357
|
+
time.performance.now(),
|
|
358
|
+
);
|
|
359
|
+
console.log("[logQueryExecutionTime]", {
|
|
360
|
+
query,
|
|
361
|
+
duration: `${duration.toFixed(3)}ms`,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
356
364
|
|
|
357
365
|
if (query.options?.logExplainQueryPlan) {
|
|
358
366
|
const result = driver.exec({
|
|
@@ -367,12 +375,12 @@ export const createSqlite =
|
|
|
367
375
|
);
|
|
368
376
|
}
|
|
369
377
|
|
|
370
|
-
console.
|
|
378
|
+
console.trace({ result });
|
|
371
379
|
return result as SqliteExecResult<R>;
|
|
372
380
|
},
|
|
373
381
|
|
|
374
382
|
transaction: ((callback: () => Result<unknown, unknown> | void) => {
|
|
375
|
-
console.
|
|
383
|
+
console.trace("begin");
|
|
376
384
|
driver.exec(sql`begin;`);
|
|
377
385
|
|
|
378
386
|
// After errors such as SQLITE_FULL or SQLITE_IOERR, SQLite may roll
|
|
@@ -386,14 +394,14 @@ export const createSqlite =
|
|
|
386
394
|
let shouldRollback = true;
|
|
387
395
|
rollback.defer(() => {
|
|
388
396
|
if (!shouldRollback) return;
|
|
389
|
-
console.
|
|
397
|
+
console.trace("rollback");
|
|
390
398
|
driver.exec(sql`rollback;`);
|
|
391
399
|
});
|
|
392
400
|
|
|
393
401
|
const result = callback();
|
|
394
402
|
if (result != null && !result.ok) return result;
|
|
395
403
|
|
|
396
|
-
console.
|
|
404
|
+
console.trace("commit");
|
|
397
405
|
driver.exec(sql`commit;`);
|
|
398
406
|
shouldRollback = false;
|
|
399
407
|
|
package/src/Task.test.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./Assert.ts";
|
|
20
20
|
|
|
21
21
|
import { emptyArray, type NonEmptyReadonlyArray } from "./Array.ts";
|
|
22
|
+
import { testCreateConsole } from "./Console.ts";
|
|
22
23
|
import { emptyRecord } from "./Object.ts";
|
|
23
24
|
import type { Int1To100OrPositiveInt } from "./Number.ts";
|
|
24
25
|
import { none, some, type Option } from "./Option.ts";
|
|
@@ -1640,6 +1641,51 @@ describe("Run", () => {
|
|
|
1640
1641
|
assertSame(secondRun.deps.console.getLevel(), secondRunLevel);
|
|
1641
1642
|
});
|
|
1642
1643
|
|
|
1644
|
+
it("reports leaks to the console passed to createRun", async () => {
|
|
1645
|
+
const console = testCreateConsole();
|
|
1646
|
+
let report: ((leak: unknown) => void) | undefined;
|
|
1647
|
+
// Captures the callback that garbage collection would call.
|
|
1648
|
+
using _registry = testStubGlobal(
|
|
1649
|
+
"FinalizationRegistry",
|
|
1650
|
+
function (callback: (leak: unknown) => void) {
|
|
1651
|
+
report = callback;
|
|
1652
|
+
return { register: () => undefined, unregister: () => undefined };
|
|
1653
|
+
},
|
|
1654
|
+
);
|
|
1655
|
+
await using _run = createRun({ console });
|
|
1656
|
+
|
|
1657
|
+
assertNotUndefined(report);
|
|
1658
|
+
report({ name: "Lease", isLeaked: () => true, stack: "stack" });
|
|
1659
|
+
|
|
1660
|
+
assertEqual(console.getEntriesSnapshot(), [
|
|
1661
|
+
{
|
|
1662
|
+
method: "warn",
|
|
1663
|
+
path: [],
|
|
1664
|
+
args: [
|
|
1665
|
+
"Lease was garbage-collected without cleanup. Tracked at:",
|
|
1666
|
+
"stack",
|
|
1667
|
+
],
|
|
1668
|
+
},
|
|
1669
|
+
]);
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
it("reports leaks to the console passed to testCreateRun", async () => {
|
|
1673
|
+
const console = testCreateConsole();
|
|
1674
|
+
await using run = testCreateRun({ console });
|
|
1675
|
+
|
|
1676
|
+
run.deps.leakDetector.track(
|
|
1677
|
+
{},
|
|
1678
|
+
{ name: "Lease", isLeaked: () => true },
|
|
1679
|
+
{},
|
|
1680
|
+
);
|
|
1681
|
+
|
|
1682
|
+
assertEqual(run.deps.leakDetector.collect(), 1);
|
|
1683
|
+
assertEqual(
|
|
1684
|
+
console.getEntriesSnapshot().map(({ args }) => args[0]),
|
|
1685
|
+
["Lease was garbage-collected without cleanup. Tracked at:"],
|
|
1686
|
+
);
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1643
1689
|
it("lets custom deps override defaults in createRun", async () => {
|
|
1644
1690
|
await using run = createRun({ random });
|
|
1645
1691
|
|