@cfasim-ui/shared 0.10.0 → 0.11.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/package.json +3 -2
- package/src/index.ts +8 -2
- package/src/transferUtils.ts +11 -0
- package/src/workerError.test.ts +98 -0
- package/src/workerError.ts +52 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cfasim-ui/shared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared utilities for cfasim-ui",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
],
|
|
18
18
|
"exports": {
|
|
19
19
|
".": "./src/index.ts",
|
|
20
|
-
"./transfer": "./src/transferUtils.ts"
|
|
20
|
+
"./transfer": "./src/transferUtils.ts",
|
|
21
|
+
"./workerError": "./src/workerError.ts"
|
|
21
22
|
},
|
|
22
23
|
"peerDependencies": {
|
|
23
24
|
"vue": "^3.5.0"
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ export {
|
|
|
4
4
|
postErrorWithTransfer,
|
|
5
5
|
unwrapResponse,
|
|
6
6
|
} from "./transferUtils.js";
|
|
7
|
-
export type { TransferableResponse } from "./transferUtils.js";
|
|
7
|
+
export type { TransferableResponse, WasmTrapKind } from "./transferUtils.js";
|
|
8
8
|
export { ModelOutput, isoDate, DATE_MISSING } from "./ModelOutput.js";
|
|
9
9
|
export type {
|
|
10
10
|
ColumnDescriptor,
|
|
@@ -19,7 +19,13 @@ export type {
|
|
|
19
19
|
ModelOutputsFileHeader,
|
|
20
20
|
ModelOutputsFileTable,
|
|
21
21
|
} from "./decodeModelOutputs.js";
|
|
22
|
-
export {
|
|
22
|
+
export {
|
|
23
|
+
describeWorkerError,
|
|
24
|
+
describeWasmTrap,
|
|
25
|
+
describeWasmInitTrap,
|
|
26
|
+
isWasmTrap,
|
|
27
|
+
MESSAGEERROR_TEXT,
|
|
28
|
+
} from "./workerError.js";
|
|
23
29
|
export { formatNumber, isNumberFormat } from "./formatNumber.js";
|
|
24
30
|
export type { NumberFormat, NumberFormatPreset } from "./formatNumber.js";
|
|
25
31
|
export {
|
package/src/transferUtils.ts
CHANGED
|
@@ -8,6 +8,8 @@ interface ModelOutputsResponsePayload {
|
|
|
8
8
|
>;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export type WasmTrapKind = "call" | "init";
|
|
12
|
+
|
|
11
13
|
export interface TransferableResponse {
|
|
12
14
|
id: number;
|
|
13
15
|
result?: unknown;
|
|
@@ -15,6 +17,13 @@ export interface TransferableResponse {
|
|
|
15
17
|
bufferType?: string;
|
|
16
18
|
modelOutputs?: ModelOutputsResponsePayload;
|
|
17
19
|
error?: string;
|
|
20
|
+
/**
|
|
21
|
+
* The call died in a WebAssembly trap and the instance is no longer
|
|
22
|
+
* trustworthy: `"call"` inside the function, `"init"` while the module was
|
|
23
|
+
* initializing (its glue keeps the half-initialized instance, so only a new
|
|
24
|
+
* worker can retry).
|
|
25
|
+
*/
|
|
26
|
+
trap?: WasmTrapKind;
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
const TYPED_ARRAY_CONSTRUCTORS: Record<
|
|
@@ -94,10 +103,12 @@ export function postErrorWithTransfer(
|
|
|
94
103
|
port: typeof globalThis,
|
|
95
104
|
id: number,
|
|
96
105
|
error: unknown,
|
|
106
|
+
options: { trap?: WasmTrapKind } = {},
|
|
97
107
|
): void {
|
|
98
108
|
port.postMessage({
|
|
99
109
|
id,
|
|
100
110
|
error: error instanceof Error ? error.message : String(error),
|
|
111
|
+
...(options.trap ? { trap: options.trap } : {}),
|
|
101
112
|
} satisfies TransferableResponse);
|
|
102
113
|
}
|
|
103
114
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
describeWasmTrap,
|
|
4
|
+
describeWasmInitTrap,
|
|
5
|
+
describeWorkerError,
|
|
6
|
+
isWasmTrap,
|
|
7
|
+
} from "./workerError.js";
|
|
8
|
+
import { postErrorWithTransfer } from "./transferUtils.js";
|
|
9
|
+
|
|
10
|
+
describe("describeWasmTrap", () => {
|
|
11
|
+
it("reports a recorded panic with its location", () => {
|
|
12
|
+
expect(
|
|
13
|
+
describeWasmTrap(
|
|
14
|
+
"unreachable",
|
|
15
|
+
"panicked at src/lib.rs:21:9: rate must be <= 5, got 9",
|
|
16
|
+
true,
|
|
17
|
+
),
|
|
18
|
+
).toBe("Model panicked at src/lib.rs:21:9: rate must be <= 5, got 9");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("explains an unreachable trap with no panic as OOM or a replaced hook", () => {
|
|
22
|
+
expect(describeWasmTrap("unreachable", undefined, true)).toMatch(
|
|
23
|
+
/aborted without a recorded panic message.*out of memory.*replaced the panic hook/,
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("recognizes every engine's wording of the unreachable trap", () => {
|
|
28
|
+
for (const text of [
|
|
29
|
+
"unreachable executed",
|
|
30
|
+
"Unreachable code should not be executed (evaluating 'wasm.simulate(a)')",
|
|
31
|
+
]) {
|
|
32
|
+
expect(describeWasmTrap(text, undefined, true)).toMatch(/aborted/);
|
|
33
|
+
expect(describeWasmTrap(text, undefined, false)).toMatch(/rebuild/);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("asks for a rebuild against current cfasim-model when the hook is missing", () => {
|
|
38
|
+
expect(describeWasmTrap("unreachable", undefined, false)).toMatch(
|
|
39
|
+
/rebuild it against the current cfasim-model/,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("passes other trap messages through", () => {
|
|
44
|
+
expect(
|
|
45
|
+
describeWasmTrap("memory access out of bounds", undefined, true),
|
|
46
|
+
).toBe("Model trapped: memory access out of bounds");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("describeWasmInitTrap", () => {
|
|
51
|
+
it("explains an abort in init and passes other traps through", () => {
|
|
52
|
+
expect(describeWasmInitTrap("unreachable executed")).toMatch(
|
|
53
|
+
/panicked or aborted while initializing/,
|
|
54
|
+
);
|
|
55
|
+
expect(describeWasmInitTrap("Maximum call stack size exceeded")).toBe(
|
|
56
|
+
"Model trapped while initializing: Maximum call stack size exceeded",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("isWasmTrap", () => {
|
|
62
|
+
it("recognizes traps and engine stack overflows, not ordinary errors", () => {
|
|
63
|
+
expect(isWasmTrap(new WebAssembly.RuntimeError("unreachable"))).toBe(true);
|
|
64
|
+
expect(isWasmTrap(new RangeError("Maximum call stack size exceeded"))).toBe(
|
|
65
|
+
true,
|
|
66
|
+
);
|
|
67
|
+
const spiderMonkey = new Error("too much recursion");
|
|
68
|
+
spiderMonkey.name = "InternalError";
|
|
69
|
+
expect(isWasmTrap(spiderMonkey)).toBe(true);
|
|
70
|
+
expect(isWasmTrap(new RangeError("Invalid array length"))).toBe(false);
|
|
71
|
+
expect(isWasmTrap(new Error("rate must be <= 5"))).toBe(false);
|
|
72
|
+
expect(isWasmTrap("unreachable")).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("postErrorWithTransfer", () => {
|
|
77
|
+
it("flags a trap only when asked", () => {
|
|
78
|
+
const port = { postMessage: vi.fn() } as unknown as typeof globalThis;
|
|
79
|
+
postErrorWithTransfer(port, 1, new Error("boom"));
|
|
80
|
+
postErrorWithTransfer(port, 2, "plain", { trap: "call" });
|
|
81
|
+
expect(port.postMessage).toHaveBeenNthCalledWith(1, {
|
|
82
|
+
id: 1,
|
|
83
|
+
error: "boom",
|
|
84
|
+
});
|
|
85
|
+
expect(port.postMessage).toHaveBeenNthCalledWith(2, {
|
|
86
|
+
id: 2,
|
|
87
|
+
error: "plain",
|
|
88
|
+
trap: "call",
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("describeWorkerError", () => {
|
|
94
|
+
it("falls back to a hint when the event is sanitized", () => {
|
|
95
|
+
const e = { message: "", filename: "", lineno: 0 } as ErrorEvent;
|
|
96
|
+
expect(describeWorkerError(e)).toMatch(/no error details/);
|
|
97
|
+
});
|
|
98
|
+
});
|
package/src/workerError.ts
CHANGED
|
@@ -15,3 +15,55 @@ export function describeWorkerError(e: ErrorEvent): string {
|
|
|
15
15
|
|
|
16
16
|
export const MESSAGEERROR_TEXT =
|
|
17
17
|
"Worker received a message it could not deserialize (messageerror)";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Message for a call that died in a WebAssembly trap. `panic` is the text
|
|
21
|
+
* recorded by `cfasim-model`'s panic hook (`panicked at <file>:<line>:<col>:
|
|
22
|
+
* <message>`) when the model panicked. `canRecord` says the module exports
|
|
23
|
+
* that hook at all; when it does not, the model is built on a cfasim-model
|
|
24
|
+
* too old to say. An `unreachable` trap with nothing recorded is an abort:
|
|
25
|
+
* the allocator giving up, or a panic after the model replaced the hook.
|
|
26
|
+
* Engines word the trap differently ("unreachable", "unreachable executed",
|
|
27
|
+
* "Unreachable code should not be executed"), hence the loose match.
|
|
28
|
+
*/
|
|
29
|
+
export function describeWasmTrap(
|
|
30
|
+
trapMessage: string,
|
|
31
|
+
panic: string | undefined,
|
|
32
|
+
canRecord: boolean,
|
|
33
|
+
): string {
|
|
34
|
+
if (panic) return `Model ${panic}`;
|
|
35
|
+
if (!/unreachable/i.test(trapMessage)) return `Model trapped: ${trapMessage}`;
|
|
36
|
+
if (canRecord) {
|
|
37
|
+
return "Model aborted without a recorded panic message: it ran out of memory, or it replaced the panic hook outside a start function";
|
|
38
|
+
}
|
|
39
|
+
return "Model panicked or aborted (unreachable); rebuild it against the current cfasim-model to see the panic message";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Message for a trap while the module initialized. The panic hook is
|
|
44
|
+
* installed after init, so a start-function panic has no recorded message.
|
|
45
|
+
*/
|
|
46
|
+
export function describeWasmInitTrap(trapMessage: string): string {
|
|
47
|
+
if (/unreachable/i.test(trapMessage)) {
|
|
48
|
+
return "Model panicked or aborted while initializing, before the panic hook could be installed";
|
|
49
|
+
}
|
|
50
|
+
return `Model trapped while initializing: ${trapMessage}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether an error thrown by a wasm call left the instance unusable. A
|
|
55
|
+
* `WebAssembly.RuntimeError` is any trap (a Rust panic or abort ends in
|
|
56
|
+
* `unreachable`). A stack overflow is not a trap but unwinds through the
|
|
57
|
+
* wasm frames without running their epilogues, so the shadow stack pointer
|
|
58
|
+
* is left wherever it was: V8 throws a `RangeError`, SpiderMonkey an
|
|
59
|
+
* `InternalError`. A JS-side overflow in the worker would match too; the
|
|
60
|
+
* worker has no recursion of its own, and the cost is one spare replacement.
|
|
61
|
+
*/
|
|
62
|
+
export function isWasmTrap(error: unknown): boolean {
|
|
63
|
+
if (error instanceof WebAssembly.RuntimeError) return true;
|
|
64
|
+
if (!(error instanceof Error)) return false;
|
|
65
|
+
return (
|
|
66
|
+
(error.name === "RangeError" || error.name === "InternalError") &&
|
|
67
|
+
/call stack|too much recursion/i.test(error.message)
|
|
68
|
+
);
|
|
69
|
+
}
|