@evolu/common 8.15.0 → 8.16.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/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/Evolu.d.ts +4 -0
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Schema.d.ts +3 -0
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +9 -5
- 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 -320
- package/package.json +1 -1
- package/src/Console.test.ts +75 -247
- package/src/Console.ts +68 -110
- 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/Evolu.ts +4 -0
- package/src/local-first/Schema.test.ts +80 -1
- package/src/local-first/Schema.ts +15 -5
- package/src/local-first/Shared.test.ts +414 -39
- package/src/local-first/Shared.ts +386 -440
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/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
|
|
package/src/Task.ts
CHANGED
|
@@ -2136,20 +2136,21 @@ export type RunDefaultDeps = ConsoleDep &
|
|
|
2136
2136
|
/**
|
|
2137
2137
|
* Creates {@link RunDefaultDeps}.
|
|
2138
2138
|
*
|
|
2139
|
+
* The leak detector reports to the provided console, or to a default one.
|
|
2140
|
+
*
|
|
2139
2141
|
* @group Run
|
|
2140
2142
|
*/
|
|
2141
|
-
export const createRunDefaultDeps = (
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
};
|
|
2143
|
+
export const createRunDefaultDeps = ({
|
|
2144
|
+
console = createConsole(),
|
|
2145
|
+
}: Partial<ConsoleDep> = {}): RunDefaultDeps => ({
|
|
2146
|
+
console,
|
|
2147
|
+
leakDetector: isDev ? createLeakDetector({ console }) : noopLeakDetector,
|
|
2148
|
+
nativeFetch: globalThis.fetch.bind(globalThis),
|
|
2149
|
+
randomBytes: createRandomBytes(),
|
|
2150
|
+
random: createRandom(),
|
|
2151
|
+
reportDefect: reportDefectAfterMicrotask,
|
|
2152
|
+
time: createTime(),
|
|
2153
|
+
});
|
|
2153
2154
|
|
|
2154
2155
|
/**
|
|
2155
2156
|
* Factory type for creating root {@link DisposableRun} instances.
|
|
@@ -2198,7 +2199,7 @@ export const createRun: CreateRun = <D extends object>(
|
|
|
2198
2199
|
deps?: RunCustomDeps<D>,
|
|
2199
2200
|
): DisposableRun<D> =>
|
|
2200
2201
|
createRunInternal<D>({
|
|
2201
|
-
...createRunDefaultDeps(),
|
|
2202
|
+
...createRunDefaultDeps(deps),
|
|
2202
2203
|
...deps,
|
|
2203
2204
|
} as RunDefaultDeps & D);
|
|
2204
2205
|
|
|
@@ -2406,8 +2407,11 @@ export function testCreateRun<D extends object>(
|
|
|
2406
2407
|
export function testCreateRun<D extends object>(
|
|
2407
2408
|
deps?: TestRunDefaultDeps | RunCustomDeps<D>,
|
|
2408
2409
|
): DisposableRun<TestRunDefaultDeps & D> {
|
|
2410
|
+
// As in createRun, the default leak detector reports to a passed console.
|
|
2411
|
+
const { console }: Partial<ConsoleDep> = deps ?? {};
|
|
2409
2412
|
return createRunInternal<TestRunDefaultDeps & D>({
|
|
2410
2413
|
...testCreateDeps(),
|
|
2414
|
+
...(console && { leakDetector: testCreateLeakDetector({ console }) }),
|
|
2411
2415
|
...deps,
|
|
2412
2416
|
} as TestRunDefaultDeps & D);
|
|
2413
2417
|
}
|