@slowclap/rezi-native 0.1.0-fork.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 ADDED
@@ -0,0 +1,38 @@
1
+ # @rezi-ui/native
2
+
3
+ Rust + `napi-rs` Node-API addon that hosts the Zireael C engine and exposes a minimal, safe JS API for the Node backend.
4
+
5
+ This package is not used directly by most applications. Install and use `@rezi-ui/node`, which depends on this package.
6
+
7
+ ## Local development
8
+
9
+ Build the native addon for your host platform:
10
+
11
+ ```bash
12
+ npm -w @rezi-ui/native run build:native
13
+ ```
14
+
15
+ Smoke test:
16
+
17
+ ```bash
18
+ npm -w @rezi-ui/native run test:native:smoke
19
+ ```
20
+
21
+ Vendoring integrity check:
22
+
23
+ ```bash
24
+ npm run check:native-vendor
25
+ ```
26
+
27
+ ## Design and constraints
28
+
29
+ - Engine placement is controlled by `@rezi-ui/node` `executionMode` (`auto` | `worker` | `inline`).
30
+ - `executionMode: "auto"` selects inline when `fpsCap <= 30`, worker otherwise.
31
+ - All buffers across the boundary are caller-owned; binary formats are validated strictly.
32
+ - Native compilation reads `packages/native/vendor/zireael` (not `vendor/zireael`).
33
+ - `packages/native/vendor/VENDOR_COMMIT.txt` must match the `vendor/zireael` gitlink commit.
34
+
35
+ See:
36
+
37
+ - [Native addon docs](../../docs/backend/native.md)
38
+ - [Releasing](../../RELEASING.md)
package/index.d.ts ADDED
@@ -0,0 +1,97 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /* auto-generated by NAPI-RS */
5
+
6
+ export interface DebugStats {
7
+ totalRecords: bigint;
8
+ totalDropped: bigint;
9
+ errorCount: number;
10
+ warnCount: number;
11
+ currentRingUsage: number;
12
+ ringCapacity: number;
13
+ }
14
+ export interface DebugQueryResult {
15
+ recordsReturned: number;
16
+ recordsAvailable: number;
17
+ oldestRecordId: bigint;
18
+ newestRecordId: bigint;
19
+ recordsDropped: number;
20
+ }
21
+ export declare function engineDebugEnable(
22
+ engineId: number,
23
+ config?: object | undefined | null,
24
+ ): number;
25
+ export declare function engineDebugDisable(engineId: number): number;
26
+ export declare function engineDebugQuery(
27
+ engineId: number,
28
+ query: object | undefined | null,
29
+ outHeaders: Uint8Array,
30
+ ): DebugQueryResult;
31
+ export declare function engineDebugGetPayload(
32
+ engineId: number,
33
+ recordId: bigint,
34
+ outPayload: Uint8Array,
35
+ ): number;
36
+ export declare function engineDebugGetStats(engineId: number): DebugStats;
37
+ export declare function engineDebugExport(engineId: number, outBuf: Uint8Array): number;
38
+ export declare function engineDebugReset(engineId: number): number;
39
+ export interface EngineMetrics {
40
+ structSize: number;
41
+ negotiatedEngineAbiMajor: number;
42
+ negotiatedEngineAbiMinor: number;
43
+ negotiatedEngineAbiPatch: number;
44
+ negotiatedDrawlistVersion: number;
45
+ negotiatedEventBatchVersion: number;
46
+ frameIndex: bigint;
47
+ fps: number;
48
+ bytesEmittedTotal: bigint;
49
+ bytesEmittedLastFrame: number;
50
+ dirtyLinesLastFrame: number;
51
+ dirtyColsLastFrame: number;
52
+ usInputLastFrame: number;
53
+ usDrawlistLastFrame: number;
54
+ usDiffLastFrame: number;
55
+ usWriteLastFrame: number;
56
+ eventsOutLastPoll: number;
57
+ eventsDroppedTotal: number;
58
+ arenaFrameHighWaterBytes: bigint;
59
+ arenaPersistentHighWaterBytes: bigint;
60
+ damageRectsLastFrame: number;
61
+ damageCellsLastFrame: number;
62
+ damageFullFrame: boolean;
63
+ }
64
+ export interface TerminalCaps {
65
+ /** Color mode: 0=unknown, 1=16, 2=256, 3=rgb */
66
+ colorMode: number;
67
+ supportsMouse: boolean;
68
+ supportsBracketedPaste: boolean;
69
+ supportsFocusEvents: boolean;
70
+ supportsOsc52: boolean;
71
+ supportsSyncUpdate: boolean;
72
+ supportsScrollRegion: boolean;
73
+ supportsCursorShape: boolean;
74
+ supportsOutputWaitWritable: boolean;
75
+ supportsUnderlineStyles: boolean;
76
+ supportsColoredUnderlines: boolean;
77
+ supportsHyperlinks: boolean;
78
+ /** Bitmask of supported SGR attributes */
79
+ sgrAttrsSupported: number;
80
+ }
81
+ export declare function engineCreate(config?: object | undefined | null): number;
82
+ export declare function engineDestroy(engineId: number): void;
83
+ export declare function engineSubmitDrawlist(engineId: number, drawlist: Uint8Array): number;
84
+ export declare function enginePresent(engineId: number): number;
85
+ export declare function enginePollEvents(
86
+ engineId: number,
87
+ timeoutMs: number,
88
+ out: Uint8Array,
89
+ ): number;
90
+ export declare function enginePostUserEvent(
91
+ engineId: number,
92
+ tag: number,
93
+ payload: Uint8Array,
94
+ ): number;
95
+ export declare function engineSetConfig(engineId: number, cfg?: object | undefined | null): number;
96
+ export declare function engineGetMetrics(engineId: number): EngineMetrics;
97
+ export declare function engineGetCaps(engineId: number): TerminalCaps;
package/index.js ADDED
@@ -0,0 +1,21 @@
1
+ import native from "./loader.cjs";
2
+
3
+ export const {
4
+ engineCreate,
5
+ engineDestroy,
6
+ engineSubmitDrawlist,
7
+ enginePresent,
8
+ enginePollEvents,
9
+ enginePostUserEvent,
10
+ engineGetMetrics,
11
+ engineSetConfig,
12
+ engineGetCaps,
13
+ // Debug trace API
14
+ engineDebugEnable,
15
+ engineDebugDisable,
16
+ engineDebugQuery,
17
+ engineDebugGetPayload,
18
+ engineDebugGetStats,
19
+ engineDebugExport,
20
+ engineDebugReset,
21
+ } = native;
package/loader.cjs ADDED
@@ -0,0 +1,134 @@
1
+ const { readdirSync } = require("node:fs");
2
+
3
+ let native = null;
4
+ let lastErr = null;
5
+ const tried = [];
6
+
7
+ function tryDynamicRequire(file) {
8
+ tried.push(file);
9
+ try {
10
+ return require(`./${file}`);
11
+ } catch (err) {
12
+ lastErr = err;
13
+ return null;
14
+ }
15
+ }
16
+
17
+ if (process.platform === "linux" && process.arch === "x64") {
18
+ tried.push("rezi_ui_native.linux-x64-gnu.node");
19
+ try {
20
+ native = require("./rezi_ui_native.linux-x64-gnu.node");
21
+ } catch (err) {
22
+ lastErr = err;
23
+ }
24
+
25
+ if (!native) {
26
+ tried.push("rezi_ui_native.linux-x64-musl.node");
27
+ try {
28
+ native = require("./rezi_ui_native.linux-x64-musl.node");
29
+ } catch (err) {
30
+ lastErr = err;
31
+ }
32
+ }
33
+
34
+ if (!native) {
35
+ tried.push("rezi_ui_native.linux-x64.node");
36
+ try {
37
+ native = require("./rezi_ui_native.linux-x64.node");
38
+ } catch (err) {
39
+ lastErr = err;
40
+ }
41
+ }
42
+ } else if (process.platform === "linux" && process.arch === "arm64") {
43
+ tried.push("rezi_ui_native.linux-arm64-gnu.node");
44
+ try {
45
+ native = require("./rezi_ui_native.linux-arm64-gnu.node");
46
+ } catch (err) {
47
+ lastErr = err;
48
+ }
49
+
50
+ if (!native) {
51
+ tried.push("rezi_ui_native.linux-arm64-musl.node");
52
+ try {
53
+ native = require("./rezi_ui_native.linux-arm64-musl.node");
54
+ } catch (err) {
55
+ lastErr = err;
56
+ }
57
+ }
58
+
59
+ if (!native) {
60
+ tried.push("rezi_ui_native.linux-arm64.node");
61
+ try {
62
+ native = require("./rezi_ui_native.linux-arm64.node");
63
+ } catch (err) {
64
+ lastErr = err;
65
+ }
66
+ }
67
+ } else if (process.platform === "darwin" && process.arch === "x64") {
68
+ tried.push("rezi_ui_native.darwin-x64.node");
69
+ try {
70
+ native = require("./rezi_ui_native.darwin-x64.node");
71
+ } catch (err) {
72
+ lastErr = err;
73
+ }
74
+ } else if (process.platform === "darwin" && process.arch === "arm64") {
75
+ tried.push("rezi_ui_native.darwin-arm64.node");
76
+ try {
77
+ native = require("./rezi_ui_native.darwin-arm64.node");
78
+ } catch (err) {
79
+ lastErr = err;
80
+ }
81
+ } else if (process.platform === "win32" && process.arch === "x64") {
82
+ tried.push("rezi_ui_native.win32-x64-msvc.node");
83
+ try {
84
+ native = require("./rezi_ui_native.win32-x64-msvc.node");
85
+ } catch (err) {
86
+ lastErr = err;
87
+ }
88
+ } else if (process.platform === "win32" && process.arch === "arm64") {
89
+ tried.push("rezi_ui_native.win32-arm64-msvc.node");
90
+ try {
91
+ native = require("./rezi_ui_native.win32-arm64-msvc.node");
92
+ } catch (err) {
93
+ lastErr = err;
94
+ }
95
+ }
96
+
97
+ if (!native) {
98
+ const platformCandidates = [
99
+ "rezi_ui_native.linux-x64-gnu.node",
100
+ "rezi_ui_native.linux-x64-musl.node",
101
+ "rezi_ui_native.linux-x64.node",
102
+ "rezi_ui_native.linux-arm64-gnu.node",
103
+ "rezi_ui_native.linux-arm64-musl.node",
104
+ "rezi_ui_native.linux-arm64.node",
105
+ "rezi_ui_native.darwin-x64.node",
106
+ "rezi_ui_native.darwin-arm64.node",
107
+ "rezi_ui_native.win32-x64-msvc.node",
108
+ "rezi_ui_native.win32-arm64-msvc.node",
109
+ ];
110
+
111
+ let discovered = [];
112
+ try {
113
+ discovered = readdirSync(__dirname).filter((file) => file.endsWith(".node"));
114
+ } catch {
115
+ discovered = [];
116
+ }
117
+
118
+ const candidates = ["index.node", "rezi_ui_native.node", ...platformCandidates, ...discovered];
119
+
120
+ for (const file of new Set(candidates)) {
121
+ native = tryDynamicRequire(file);
122
+ if (native) break;
123
+ }
124
+ }
125
+
126
+ if (!native) {
127
+ const extra =
128
+ lastErr instanceof Error ? `\n\nLast error:\n${lastErr.stack ?? lastErr.message}` : "";
129
+ throw new Error(
130
+ `Failed to load @rezi-ui/native binary. Tried: ${[...new Set(tried)].join(", ")}${extra}`,
131
+ );
132
+ }
133
+
134
+ module.exports = native;
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@slowclap/rezi-native",
3
+ "version": "0.1.0-fork.1",
4
+ "description": "Native addon for Rezi (napi-rs + Zireael engine).",
5
+ "license": "Apache-2.0",
6
+ "homepage": "https://rezitui.dev",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/RtlZeroMemory/Rezi.git",
10
+ "directory": "packages/native"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/RtlZeroMemory/Rezi/issues"
14
+ },
15
+ "type": "module",
16
+ "main": "./index.js",
17
+ "types": "./index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./index.d.ts",
21
+ "default": "./index.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "index.js",
26
+ "loader.cjs",
27
+ "index.d.ts",
28
+ "*.node",
29
+ "scripts/*.mjs",
30
+ "package.json"
31
+ ],
32
+ "scripts": {
33
+ "build:native": "node ./scripts/build-native.mjs",
34
+ "test:native:smoke": "node ./scripts/smoke.mjs"
35
+ },
36
+ "devDependencies": {
37
+ "@napi-rs/cli": "^2.18.4"
38
+ },
39
+ "engines": {
40
+ "node": ">=18",
41
+ "bun": ">=1.3.0"
42
+ },
43
+ "napi": {
44
+ "name": "rezi_ui_native"
45
+ }
46
+ }
Binary file
Binary file
@@ -0,0 +1,487 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import { copyFileSync, existsSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { delimiter, join } from "node:path";
5
+
6
+ function splitPathList(value, delim) {
7
+ return value
8
+ .split(delim)
9
+ .map((p) => p.trim().replace(/^"+|"+$/g, ""))
10
+ .filter(Boolean);
11
+ }
12
+
13
+ function envHasLib(env, libName) {
14
+ if (process.platform !== "win32") return true;
15
+ const libPath = env.LIB;
16
+ if (typeof libPath !== "string" || libPath.length === 0) return false;
17
+ for (const dir of splitPathList(libPath, ";")) {
18
+ if (existsSync(join(dir, libName))) return true;
19
+ }
20
+ return false;
21
+ }
22
+
23
+ function parseCmdSetOutput(output) {
24
+ const next = {};
25
+ for (const raw of output.split(/\r?\n/)) {
26
+ const line = raw.trimEnd();
27
+ if (line.length === 0) continue;
28
+ const idx = line.indexOf("=");
29
+ if (idx <= 0) continue;
30
+ const key = line.slice(0, idx);
31
+ // cmd.exe prints internal per-drive cwd entries like `=C:=C:\...`.
32
+ if (key.startsWith("=")) continue;
33
+ next[key] = line.slice(idx + 1);
34
+ }
35
+ return next;
36
+ }
37
+
38
+ function runBatchAndGetEnv(batchFile, batchArgs, env) {
39
+ const args = Array.isArray(batchArgs) ? batchArgs : [];
40
+
41
+ // Avoid nested quoting issues between Node/Windows argument escaping and cmd.exe parsing
42
+ // by writing a temporary .cmd file. This keeps the `call "C:\\path with spaces\\..."` intact.
43
+ const scriptPath = join(
44
+ tmpdir(),
45
+ `rezi-ui-native-msvc-env-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.cmd`,
46
+ );
47
+ const script = `@echo off\r\ncall "${batchFile}" ${args.join(" ")}\r\nset\r\n`;
48
+ writeFileSync(scriptPath, script, { encoding: "utf8" });
49
+
50
+ try {
51
+ const out = execFileSync("cmd.exe", ["/d", "/s", "/c", scriptPath], { encoding: "utf8", env });
52
+ return parseCmdSetOutput(out);
53
+ } finally {
54
+ rmSync(scriptPath, { force: true });
55
+ }
56
+ }
57
+
58
+ function findVswhere() {
59
+ const pf86 = process.env["ProgramFiles(x86)"];
60
+ const pf = process.env.ProgramFiles;
61
+ const candidates = [
62
+ pf86 ? join(pf86, "Microsoft Visual Studio", "Installer", "vswhere.exe") : null,
63
+ pf ? join(pf, "Microsoft Visual Studio", "Installer", "vswhere.exe") : null,
64
+ ].filter(Boolean);
65
+ for (const p of candidates) {
66
+ if (p && existsSync(p)) return p;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ function tryGetVisualStudioInstallPath(env) {
72
+ const vswhere = findVswhere();
73
+ if (!vswhere) return null;
74
+ try {
75
+ const out = execFileSync(
76
+ vswhere,
77
+ [
78
+ "-latest",
79
+ "-products",
80
+ "*",
81
+ "-requires",
82
+ "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
83
+ "-property",
84
+ "installationPath",
85
+ ],
86
+ { encoding: "utf8", env },
87
+ );
88
+ const installPath = out.trim();
89
+ return installPath.length > 0 ? installPath : null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ function getMsvcArchFromRustTarget(hostTargetTriple) {
96
+ if (typeof hostTargetTriple !== "string") return "x64";
97
+ if (hostTargetTriple.startsWith("x86_64-")) return "x64";
98
+ if (hostTargetTriple.startsWith("i686-")) return "x86";
99
+ if (hostTargetTriple.startsWith("aarch64-")) return "arm64";
100
+ return "x64";
101
+ }
102
+
103
+ function getMsvcHostArch() {
104
+ if (process.arch === "x64") return "x64";
105
+ if (process.arch === "arm64") return "arm64";
106
+ if (process.arch === "ia32") return "x86";
107
+ return "x64";
108
+ }
109
+
110
+ function compareSemverDirs(a, b) {
111
+ // Windows SDK folders look like `10.0.22621.0`. Compare numerically.
112
+ const pa = a.split(".").map((n) => Number(n));
113
+ const pb = b.split(".").map((n) => Number(n));
114
+ const len = Math.max(pa.length, pb.length);
115
+ for (let i = 0; i < len; i++) {
116
+ const da = pa[i] ?? 0;
117
+ const db = pb[i] ?? 0;
118
+ if (da !== db) return da - db;
119
+ }
120
+ return 0;
121
+ }
122
+
123
+ function tryGetLatestWindowsSdkVersion(sdkRoot) {
124
+ const libRoot = join(sdkRoot, "Lib");
125
+ if (!existsSync(libRoot)) return null;
126
+
127
+ try {
128
+ const dirs = readdirSync(libRoot, { withFileTypes: true })
129
+ .filter((d) => d.isDirectory() && /^\d+\.\d+\.\d+\.\d+$/.test(d.name))
130
+ .map((d) => d.name)
131
+ .sort(compareSemverDirs);
132
+ return dirs.length > 0 ? dirs[dirs.length - 1] : null;
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ function withWindowsSdkEnv(env, hostTargetTriple) {
139
+ if (process.platform !== "win32") return env;
140
+
141
+ const arch = getMsvcArchFromRustTarget(hostTargetTriple);
142
+ const pf86 = env["ProgramFiles(x86)"] ?? process.env["ProgramFiles(x86)"];
143
+ const sdkRoot =
144
+ typeof pf86 === "string" && pf86.length > 0
145
+ ? join(pf86, "Windows Kits", "10")
146
+ : join("C:\\", "Program Files (x86)", "Windows Kits", "10");
147
+
148
+ const sdkVersion = tryGetLatestWindowsSdkVersion(sdkRoot);
149
+ if (!sdkVersion) return env;
150
+
151
+ const libUcrt = join(sdkRoot, "Lib", sdkVersion, "ucrt", arch);
152
+ const libUm = join(sdkRoot, "Lib", sdkVersion, "um", arch);
153
+ const incRoot = join(sdkRoot, "Include", sdkVersion);
154
+ const incUcrt = join(incRoot, "ucrt");
155
+ const incShared = join(incRoot, "shared");
156
+ const incUm = join(incRoot, "um");
157
+ const incWinrt = join(incRoot, "winrt");
158
+ const incCppWinrt = join(incRoot, "cppwinrt");
159
+ const binVersioned = join(sdkRoot, "Bin", sdkVersion, arch);
160
+ const binFallback = join(sdkRoot, "Bin", arch);
161
+
162
+ const next = { ...env };
163
+
164
+ // SDK variables are not strictly required for linking, but setting them helps
165
+ // downstream tools (rc.exe, mt.exe, and various build scripts) behave.
166
+ if (typeof next.WindowsSdkDir !== "string" || next.WindowsSdkDir.length === 0) {
167
+ next.WindowsSdkDir = `${sdkRoot}\\`;
168
+ }
169
+ if (typeof next.WindowsSDKVersion !== "string" || next.WindowsSDKVersion.length === 0) {
170
+ next.WindowsSDKVersion = `${sdkVersion}\\`;
171
+ }
172
+
173
+ const addTo = (key, paths) => {
174
+ const current = typeof next[key] === "string" ? next[key] : "";
175
+ const parts = current.length > 0 ? splitPathList(current, ";") : [];
176
+ const seen = new Set(parts.map((p) => p.toLowerCase()));
177
+ for (const p of paths) {
178
+ if (!p || !existsSync(p)) continue;
179
+ const lower = p.toLowerCase();
180
+ if (seen.has(lower)) continue;
181
+ parts.push(p);
182
+ seen.add(lower);
183
+ }
184
+ next[key] = parts.join(";");
185
+ };
186
+
187
+ addTo("LIB", [libUcrt, libUm]);
188
+ addTo("INCLUDE", [incUcrt, incShared, incUm, incWinrt, incCppWinrt]);
189
+ addTo("PATH", [binVersioned, binFallback]);
190
+
191
+ return next;
192
+ }
193
+
194
+ function withMsvcDevEnv(env, hostTargetTriple) {
195
+ if (process.platform !== "win32") return env;
196
+
197
+ // If the environment already has the MSVC/SDK lib paths, don't mutate it.
198
+ if (envHasLib(env, "msvcrt.lib") && envHasLib(env, "kernel32.lib")) return env;
199
+
200
+ const vsInstallPath = tryGetVisualStudioInstallPath(env);
201
+ if (!vsInstallPath) return env;
202
+
203
+ const vsDevCmd = join(vsInstallPath, "Common7", "Tools", "VsDevCmd.bat");
204
+ const vcvars64 = join(vsInstallPath, "VC", "Auxiliary", "Build", "vcvars64.bat");
205
+ const batchFile = existsSync(vsDevCmd) ? vsDevCmd : existsSync(vcvars64) ? vcvars64 : null;
206
+ if (!batchFile) return env;
207
+
208
+ const targetArch = getMsvcArchFromRustTarget(hostTargetTriple);
209
+ const hostArch = getMsvcHostArch();
210
+
211
+ try {
212
+ const batchArgs = batchFile.toLowerCase().endsWith("vsdevcmd.bat")
213
+ ? ["-no_logo", `-arch=${targetArch}`, `-host_arch=${hostArch}`]
214
+ : [];
215
+ const next = runBatchAndGetEnv(batchFile, batchArgs, env);
216
+ return envHasLib(next, "msvcrt.lib") ? next : env;
217
+ } catch {
218
+ return env;
219
+ }
220
+ }
221
+
222
+ function spawnNpm(args, options) {
223
+ // Prefer the npm CLI path provided by npm itself when running under `npm run`.
224
+ // This avoids shell differences on Windows (cmd.exe vs bash) and PATH issues.
225
+ const npmExecPath = process.env.npm_execpath;
226
+ if (npmExecPath && existsSync(npmExecPath)) {
227
+ return spawnSync(process.execPath, [npmExecPath, ...args], options);
228
+ }
229
+
230
+ const candidates = process.platform === "win32" ? ["npm", "npm.cmd"] : ["npm"];
231
+ let last = null;
232
+ for (const cmd of candidates) {
233
+ const res = spawnSync(cmd, args, options);
234
+ last = res;
235
+ if (!res.error) return res;
236
+ }
237
+ return last;
238
+ }
239
+
240
+ function canRunCargo(env) {
241
+ try {
242
+ execFileSync("cargo", ["--version"], { stdio: "ignore", env });
243
+ return true;
244
+ } catch {
245
+ return false;
246
+ }
247
+ }
248
+
249
+ function getCargoExePath(env) {
250
+ const cargoHome =
251
+ typeof env.CARGO_HOME === "string" && env.CARGO_HOME.length > 0
252
+ ? env.CARGO_HOME
253
+ : typeof env.USERPROFILE === "string" && env.USERPROFILE.length > 0
254
+ ? join(env.USERPROFILE, ".cargo")
255
+ : null;
256
+ if (!cargoHome) return null;
257
+ const cargoBin = join(cargoHome, "bin");
258
+ const cargoExe =
259
+ process.platform === "win32" ? join(cargoBin, "cargo.exe") : join(cargoBin, "cargo");
260
+ return existsSync(cargoExe) ? cargoExe : null;
261
+ }
262
+
263
+ function ensureCargoOnPath(env) {
264
+ if (canRunCargo(env)) return env;
265
+
266
+ const cargoExe = getCargoExePath(env);
267
+ if (!cargoExe) {
268
+ const hint =
269
+ process.platform === "win32"
270
+ ? [
271
+ "@rezi-ui/native: cargo not found (Rust toolchain missing).",
272
+ "",
273
+ "Install Rust (includes cargo) via rustup: https://rustup.rs/",
274
+ "Then reopen your terminal and verify:",
275
+ " cargo --version",
276
+ " rustc --version",
277
+ ].join("\n")
278
+ : [
279
+ "@rezi-ui/native: cargo not found (Rust toolchain missing).",
280
+ "",
281
+ "Install Rust via rustup: https://rustup.rs/",
282
+ "Then verify: cargo --version",
283
+ ].join("\n");
284
+ process.stderr.write(`${hint}\n`);
285
+ process.exit(1);
286
+ }
287
+
288
+ const next = { ...env };
289
+ const cargoBin = join(cargoExe, "..");
290
+ const currentPath = typeof next.PATH === "string" ? next.PATH : "";
291
+ next.PATH = `${cargoBin}${delimiter}${currentPath}`;
292
+ if (canRunCargo(next)) return next;
293
+
294
+ process.stderr.write(
295
+ `${[
296
+ "@rezi-ui/native: cargo exists on disk but still isn't runnable from this npm script environment.",
297
+ "",
298
+ `Found cargo at: ${cargoExe}`,
299
+ "",
300
+ "Try running the build from a fresh terminal, or set PATH so it includes your Rust bin directory.",
301
+ "On Windows with rustup, that's usually:",
302
+ ` ${join(String(env.USERPROFILE ?? "C:\\Users\\<you>"), ".cargo", "bin")}`,
303
+ ].join("\n")}\n`,
304
+ );
305
+ process.exit(1);
306
+ }
307
+
308
+ function withRustToolchainOnPath(env) {
309
+ const next = { ...env };
310
+ const currentPath = typeof next.PATH === "string" ? next.PATH : "";
311
+
312
+ // When npm is configured to run scripts via Git Bash on Windows, PATH may not
313
+ // include the Rust toolchain even if it exists on disk. The napi CLI shells
314
+ // out to `cargo`, so ensure it's discoverable.
315
+ const cargoHome =
316
+ typeof next.CARGO_HOME === "string" && next.CARGO_HOME.length > 0
317
+ ? next.CARGO_HOME
318
+ : typeof next.USERPROFILE === "string" && next.USERPROFILE.length > 0
319
+ ? join(next.USERPROFILE, ".cargo")
320
+ : null;
321
+
322
+ if (cargoHome) {
323
+ const cargoBin = join(cargoHome, "bin");
324
+ const cargoExe =
325
+ process.platform === "win32" ? join(cargoBin, "cargo.exe") : join(cargoBin, "cargo");
326
+ if (existsSync(cargoExe) && !currentPath.toLowerCase().includes(cargoBin.toLowerCase())) {
327
+ next.PATH = `${cargoBin}${delimiter}${currentPath}`;
328
+ }
329
+ }
330
+
331
+ // Ensure `napi` can find cargo even if it shells out through cmd.exe with a
332
+ // different PATH resolution behavior.
333
+ if (typeof next.CARGO !== "string" || next.CARGO.length === 0) {
334
+ const cargoExe = getCargoExePath(next);
335
+ if (cargoExe) next.CARGO = cargoExe;
336
+ }
337
+
338
+ return next;
339
+ }
340
+
341
+ function buildWithCargoDirectly(env, host) {
342
+ const cargoExe = getCargoExePath(env) ?? "cargo";
343
+ try {
344
+ execFileSync(cargoExe, ["build", "--release", "--target", host], { stdio: "inherit", env });
345
+ } catch (err) {
346
+ if (process.platform === "win32") {
347
+ process.stderr.write(
348
+ `${[
349
+ "",
350
+ "@rezi-ui/native: cargo build failed on Windows.",
351
+ "If you see linker errors like `LNK1104: cannot open file 'msvcrt.lib'` or missing headers like `stdint.h`,",
352
+ "install Visual Studio Build Tools (MSVC v143 + Windows 10/11 SDK) and run the build from a VS Developer shell.",
353
+ "",
354
+ ].join("\n")}\n`,
355
+ );
356
+ }
357
+ throw err;
358
+ }
359
+
360
+ const crateName = "rezi_ui_native";
361
+ const targetDir = join(process.cwd(), "target", host, "release");
362
+ const built =
363
+ process.platform === "win32"
364
+ ? join(targetDir, `${crateName}.dll`)
365
+ : process.platform === "darwin"
366
+ ? join(targetDir, `lib${crateName}.dylib`)
367
+ : join(targetDir, `lib${crateName}.so`);
368
+
369
+ if (!existsSync(built)) {
370
+ throw new Error(`@rezi-ui/native: cargo build succeeded but output was not found: ${built}`);
371
+ }
372
+
373
+ // Node loads addons as .node (they are native shared libraries under the hood).
374
+ // Keep both filenames as candidates for the JS loader.
375
+ copyFileSync(built, join(process.cwd(), "rezi_ui_native.node"));
376
+ copyFileSync(built, join(process.cwd(), "index.node"));
377
+ }
378
+
379
+ function getHostTargetTriple() {
380
+ let out;
381
+ try {
382
+ out = execFileSync("rustc", ["-vV"], { encoding: "utf8" });
383
+ } catch (err) {
384
+ const hint =
385
+ process.platform === "win32"
386
+ ? "Install Rust from https://rustup.rs/ (or ensure `rustc.exe` is on PATH)."
387
+ : "Install Rust via rustup (https://rustup.rs/) and ensure `rustc` is on PATH.";
388
+ const detail = err instanceof Error ? err.message : String(err);
389
+ throw new Error(`@rezi-ui/native: rustc not found or not runnable.\n${hint}\n\n${detail}`);
390
+ }
391
+ const line = out
392
+ .split("\n")
393
+ .map((l) => l.trim())
394
+ .find((l) => l.startsWith("host: "));
395
+ if (!line) throw new Error("Failed to determine Rust host triple from `rustc -vV`");
396
+ return line.slice("host: ".length).trim();
397
+ }
398
+
399
+ let host;
400
+ try {
401
+ host = getHostTargetTriple();
402
+ } catch (err) {
403
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
404
+ process.exit(1);
405
+ }
406
+
407
+ const scriptEnv = ensureCargoOnPath(
408
+ withWindowsSdkEnv(withMsvcDevEnv(withRustToolchainOnPath(process.env), host), host),
409
+ );
410
+
411
+ // @napi-rs/cli parses Cargo.toml by running `cargo metadata` through cmd.exe on Windows.
412
+ // Some environments can run `cargo` fine from PowerShell, but cmd.exe fails to resolve it.
413
+ // Use a direct cargo build path on Windows to avoid that failure mode.
414
+ if (process.platform === "win32") {
415
+ try {
416
+ buildWithCargoDirectly(scriptEnv, host);
417
+ } catch (err) {
418
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
419
+ process.exit(1);
420
+ }
421
+ } else {
422
+ const res = spawnNpm(
423
+ ["exec", "--", "napi", "build", "--platform", "--release", "--target", host, "--js", "false"],
424
+ {
425
+ stdio: "inherit",
426
+ env: scriptEnv,
427
+ },
428
+ );
429
+
430
+ if (res?.error) {
431
+ const detail = res.error instanceof Error ? res.error.message : String(res.error);
432
+ process.stderr.write(`@rezi-ui/native: failed to invoke npm.\n\n${detail}\n`);
433
+ process.exit(1);
434
+ }
435
+
436
+ if (res.status !== 0) {
437
+ process.exit(res.status ?? 1);
438
+ }
439
+ }
440
+
441
+ if (existsSync("./index.d.ts")) {
442
+ const biomeArgs = ["format", "index.d.ts", "--write"];
443
+
444
+ // On Windows, npm may be configured to run scripts via Git Bash, which can end up
445
+ // selecting the POSIX `.bin/biome` shim that relies on `node` being resolvable via
446
+ // a POSIX-style PATH. Run the Biome entrypoint with Node directly to avoid that.
447
+ const fmt =
448
+ process.platform === "win32"
449
+ ? (() => {
450
+ const initCwd = process.env.INIT_CWD;
451
+ const roots = [
452
+ typeof initCwd === "string" && initCwd.length > 0 ? initCwd : null,
453
+ join(process.cwd(), "..", ".."),
454
+ ].filter(Boolean);
455
+
456
+ for (const root of roots) {
457
+ const biomeEntrypoint = join(root, "node_modules", "@biomejs", "biome", "bin", "biome");
458
+ if (existsSync(biomeEntrypoint)) {
459
+ return spawnSync(process.execPath, [biomeEntrypoint, ...biomeArgs], {
460
+ stdio: "inherit",
461
+ env: scriptEnv,
462
+ });
463
+ }
464
+ }
465
+
466
+ // Fall back to `npm exec` if the entrypoint isn't where we expect.
467
+ return spawnNpm(["exec", "--", "biome", ...biomeArgs], {
468
+ stdio: "inherit",
469
+ env: scriptEnv,
470
+ });
471
+ })()
472
+ : spawnNpm(["exec", "--", "biome", ...biomeArgs], {
473
+ stdio: "inherit",
474
+ env: scriptEnv,
475
+ });
476
+ if (fmt?.error) {
477
+ const detail = fmt.error instanceof Error ? fmt.error.message : String(fmt.error);
478
+ process.stderr.write(
479
+ `@rezi-ui/native: warning: failed to invoke npm for formatting index.d.ts (continuing).\n\n${detail}\n`,
480
+ );
481
+ }
482
+ if (fmt.status !== 0) {
483
+ process.stderr.write(
484
+ `@rezi-ui/native: warning: biome format returned non-zero status (${fmt.status ?? "unknown"}); continuing.\n`,
485
+ );
486
+ }
487
+ }
@@ -0,0 +1,36 @@
1
+ import { parentPort, workerData } from "node:worker_threads";
2
+ import {
3
+ engineDebugDisable,
4
+ enginePostUserEvent,
5
+ enginePresent,
6
+ engineSetConfig,
7
+ } from "../index.js";
8
+
9
+ const { engineId } = workerData;
10
+
11
+ if (!parentPort) {
12
+ throw new Error("smoke-worker: missing parentPort");
13
+ }
14
+
15
+ if (workerData?.phase === "loadOnly") {
16
+ parentPort.postMessage({ phase: "loadOnly" });
17
+ parentPort.close();
18
+ } else {
19
+ const res1 = enginePresent(engineId);
20
+ const res2 = enginePostUserEvent(engineId, 123, new Uint8Array([1, 2, 3]));
21
+ const res3 = engineSetConfig(engineId, { targetFps: 33 });
22
+ const res4 = engineDebugDisable(engineId);
23
+ parentPort.postMessage({
24
+ phase: "alive",
25
+ present: res1,
26
+ postUserEvent: res2,
27
+ setConfig: res3,
28
+ debugDisable: res4,
29
+ });
30
+
31
+ parentPort.on("message", (msg) => {
32
+ if (msg?.type !== "afterDestroy") return;
33
+ const res = enginePostUserEvent(engineId, 456, new Uint8Array([9]));
34
+ parentPort.postMessage({ phase: "destroyed", postUserEvent: res });
35
+ });
36
+ }
@@ -0,0 +1,337 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import {
3
+ engineCreate,
4
+ engineDebugDisable,
5
+ engineDebugEnable,
6
+ engineDebugExport,
7
+ engineDebugGetPayload,
8
+ engineDebugGetStats,
9
+ engineDebugQuery,
10
+ engineDebugReset,
11
+ engineDestroy,
12
+ engineGetMetrics,
13
+ enginePostUserEvent,
14
+ enginePresent,
15
+ engineSetConfig,
16
+ } from "../index.js";
17
+
18
+ const ZR_OK = 0;
19
+ const ZR_ERR_INVALID_ARGUMENT = -1;
20
+ const ZR_ERR_PLATFORM = -6;
21
+ const INVALID_ARG_ERROR_RE = /INVALID_ARGUMENT|InvalidArg|ZR_ERR_INVALID_ARGUMENT/i;
22
+
23
+ function assert(cond, msg) {
24
+ if (!cond) throw new Error(msg);
25
+ }
26
+
27
+ function assertThrows(fn, pattern, msg) {
28
+ let threw = false;
29
+ try {
30
+ fn();
31
+ } catch (err) {
32
+ threw = true;
33
+ if (pattern) {
34
+ const detail = err instanceof Error ? err.message : String(err);
35
+ assert(pattern.test(detail), `${msg}: wrong error detail: ${detail}`);
36
+ }
37
+ }
38
+ assert(threw, `${msg}: expected throw`);
39
+ }
40
+
41
+ function assertMetricsShape(metrics) {
42
+ assert(metrics && typeof metrics === "object", "engineGetMetrics must return an object");
43
+ assert(typeof metrics.frameIndex === "bigint", "metrics.frameIndex must be bigint");
44
+ assert(typeof metrics.bytesEmittedTotal === "bigint", "metrics.bytesEmittedTotal must be bigint");
45
+ assert(
46
+ typeof metrics.arenaFrameHighWaterBytes === "bigint",
47
+ "metrics.arenaFrameHighWaterBytes must be bigint",
48
+ );
49
+ assert(typeof metrics.fps === "number", "metrics.fps must be number");
50
+ assert(
51
+ typeof metrics.negotiatedDrawlistVersion === "number",
52
+ "metrics.negotiatedDrawlistVersion must be number",
53
+ );
54
+ assert(
55
+ typeof metrics.negotiatedEventBatchVersion === "number",
56
+ "metrics.negotiatedEventBatchVersion must be number",
57
+ );
58
+ }
59
+
60
+ function assertDebugStatsShape(stats) {
61
+ assert(stats && typeof stats === "object", "engineDebugGetStats must return an object");
62
+ assert(typeof stats.totalRecords === "bigint", "debug stats totalRecords must be bigint");
63
+ assert(typeof stats.totalDropped === "bigint", "debug stats totalDropped must be bigint");
64
+ assert(typeof stats.errorCount === "number", "debug stats errorCount must be number");
65
+ assert(typeof stats.warnCount === "number", "debug stats warnCount must be number");
66
+ assert(typeof stats.currentRingUsage === "number", "debug stats currentRingUsage must be number");
67
+ assert(typeof stats.ringCapacity === "number", "debug stats ringCapacity must be number");
68
+ }
69
+
70
+ function readU64LE(bytes, offset) {
71
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
72
+ return dv.getBigUint64(offset, true);
73
+ }
74
+
75
+ async function assertWorkerLoadExitCleanly() {
76
+ const worker = new Worker(new URL("./smoke-worker.mjs", import.meta.url), {
77
+ workerData: { phase: "loadOnly" },
78
+ type: "module",
79
+ });
80
+ const exitPromise = new Promise((resolve) => worker.once("exit", resolve));
81
+
82
+ const loadResult = await new Promise((resolve, reject) => {
83
+ const onExit = (code) => reject(new Error(`load-only worker exited with ${code}`));
84
+ const onError = (err) => reject(err);
85
+ const onMessage = (msg) => {
86
+ worker.off("exit", onExit);
87
+ worker.off("error", onError);
88
+ resolve(msg);
89
+ };
90
+ worker.once("exit", onExit);
91
+ worker.once("error", onError);
92
+ worker.once("message", onMessage);
93
+ });
94
+
95
+ assert(loadResult.phase === "loadOnly", "worker load-only phase must complete");
96
+ const exitCode = await exitPromise;
97
+ assert(exitCode === 0, `load-only worker must exit cleanly, got: ${String(exitCode)}`);
98
+ }
99
+
100
+ await assertWorkerLoadExitCleanly();
101
+
102
+ // Unknown / stale id behavior (result-returning functions).
103
+ assert(
104
+ enginePresent(0) === ZR_ERR_INVALID_ARGUMENT,
105
+ "enginePresent(0) must return ZR_ERR_INVALID_ARGUMENT",
106
+ );
107
+ assert(
108
+ enginePresent(0x7fff_fffe) === ZR_ERR_INVALID_ARGUMENT,
109
+ "enginePresent(unknown) must return ZR_ERR_INVALID_ARGUMENT",
110
+ );
111
+ assert(
112
+ engineSetConfig(0, { targetFps: 30 }) === ZR_ERR_INVALID_ARGUMENT,
113
+ "engineSetConfig(0) must return ZR_ERR_INVALID_ARGUMENT",
114
+ );
115
+ assert(
116
+ enginePostUserEvent(0, 1, new Uint8Array([1])) === ZR_ERR_INVALID_ARGUMENT,
117
+ "enginePostUserEvent(0) must return ZR_ERR_INVALID_ARGUMENT",
118
+ );
119
+ assert(
120
+ engineDebugEnable(0, { enabled: true }) === ZR_ERR_INVALID_ARGUMENT,
121
+ "engineDebugEnable(0) must return ZR_ERR_INVALID_ARGUMENT",
122
+ );
123
+ assert(
124
+ engineDebugDisable(0) === ZR_ERR_INVALID_ARGUMENT,
125
+ "engineDebugDisable(0) must return ZR_ERR_INVALID_ARGUMENT",
126
+ );
127
+ assert(
128
+ engineDebugExport(0, new Uint8Array(64)) === ZR_ERR_INVALID_ARGUMENT,
129
+ "engineDebugExport(0) must return ZR_ERR_INVALID_ARGUMENT",
130
+ );
131
+ assert(
132
+ engineDebugReset(0) === ZR_ERR_INVALID_ARGUMENT,
133
+ "engineDebugReset(0) must return ZR_ERR_INVALID_ARGUMENT",
134
+ );
135
+ assertThrows(
136
+ () => engineGetMetrics(0),
137
+ INVALID_ARG_ERROR_RE,
138
+ "engineGetMetrics(0) must throw invalid-arg",
139
+ );
140
+ assertThrows(
141
+ () => engineDebugQuery(0, null, new Uint8Array(40)),
142
+ INVALID_ARG_ERROR_RE,
143
+ "engineDebugQuery(0) must throw invalid-arg",
144
+ );
145
+ assertThrows(
146
+ () => engineDebugGetStats(0),
147
+ INVALID_ARG_ERROR_RE,
148
+ "engineDebugGetStats(0) must throw invalid-arg",
149
+ );
150
+ assertThrows(
151
+ () => engineDebugGetPayload(0, 0n, new Uint8Array(16)),
152
+ INVALID_ARG_ERROR_RE,
153
+ "engineDebugGetPayload(0) must throw invalid-arg",
154
+ );
155
+
156
+ const engineId = engineCreate({});
157
+
158
+ assert(typeof engineId === "number", "engineCreate must return a number");
159
+ if (engineId === ZR_ERR_PLATFORM || engineId === ZR_ERR_INVALID_ARGUMENT) {
160
+ process.stdout.write(
161
+ `native-smoke: SKIP engineCreate() deep checks (engineCreate returned ${engineId}; stdout.isTTY=${String(process.stdout.isTTY)} stdin.isTTY=${String(process.stdin.isTTY)})\n`,
162
+ );
163
+ process.exit(0);
164
+ }
165
+ assert(engineId > 0, `engineCreate must return a non-zero engineId, got: ${engineId}`);
166
+
167
+ assert(
168
+ engineSetConfig(engineId, null) === ZR_ERR_INVALID_ARGUMENT,
169
+ "engineSetConfig(null) must return ZR_ERR_INVALID_ARGUMENT",
170
+ );
171
+ assertThrows(
172
+ () => engineSetConfig(engineId, { plat: 1 }),
173
+ /plat must be an object/i,
174
+ "engineSetConfig must reject non-object plat values",
175
+ );
176
+ assertThrows(
177
+ () => engineSetConfig(engineId, { unknownKey: 1 }),
178
+ /unknown key/i,
179
+ "engineSetConfig with unknown key must throw",
180
+ );
181
+ assert(
182
+ engineSetConfig(engineId, {
183
+ targetFps: 30,
184
+ enableScrollOptimizations: true,
185
+ waitForOutputDrain: false,
186
+ plat: { enableMouse: true, enableBracketedPaste: true },
187
+ }) === ZR_OK,
188
+ "engineSetConfig(valid config) must return ZR_OK",
189
+ );
190
+
191
+ const metricsBefore = engineGetMetrics(engineId);
192
+ assertMetricsShape(metricsBefore);
193
+ assert(
194
+ enginePresent(engineId) === ZR_OK,
195
+ "enginePresent(owner-thread) must return ZR_OK after successful create",
196
+ );
197
+ const metricsAfter = engineGetMetrics(engineId);
198
+ assertMetricsShape(metricsAfter);
199
+ assert(metricsAfter.frameIndex >= metricsBefore.frameIndex, "metrics.frameIndex must be monotonic");
200
+
201
+ assert(
202
+ engineDebugEnable(engineId, {
203
+ enabled: true,
204
+ ringCapacity: 256,
205
+ minSeverity: 0,
206
+ categoryMask: 0xffff_ffff,
207
+ }) === ZR_OK,
208
+ "engineDebugEnable(valid config) must return ZR_OK",
209
+ );
210
+ assertThrows(
211
+ () => engineDebugQuery(engineId, { bogus: true }, new Uint8Array(40)),
212
+ /unknown key/i,
213
+ "engineDebugQuery with unknown key must throw",
214
+ );
215
+ assertThrows(
216
+ () => engineDebugQuery(engineId, { maxRecords: 1 }, new Uint8Array(new ArrayBuffer(41), 1, 40)),
217
+ /align/i,
218
+ "engineDebugQuery with misaligned outHeaders must throw",
219
+ );
220
+
221
+ assert(
222
+ enginePostUserEvent(engineId, 0xbeef, new Uint8Array([1, 2, 3, 4])) === ZR_OK,
223
+ "enginePostUserEvent(owner-thread) must return ZR_OK",
224
+ );
225
+
226
+ const headers = new Uint8Array(40 * 8);
227
+ const query = engineDebugQuery(engineId, { maxRecords: 8 }, headers);
228
+ assert(query && typeof query === "object", "engineDebugQuery must return an object");
229
+ assert(
230
+ typeof query.recordsReturned === "number" &&
231
+ query.recordsReturned >= 0 &&
232
+ query.recordsReturned <= 8,
233
+ "engineDebugQuery.recordsReturned must be in [0, 8]",
234
+ );
235
+ assert(
236
+ typeof query.recordsAvailable === "number" && query.recordsAvailable >= 0,
237
+ "engineDebugQuery.recordsAvailable must be non-negative",
238
+ );
239
+ assert(typeof query.oldestRecordId === "bigint", "engineDebugQuery.oldestRecordId must be bigint");
240
+ assert(typeof query.newestRecordId === "bigint", "engineDebugQuery.newestRecordId must be bigint");
241
+ assert(
242
+ typeof query.recordsDropped === "number" && query.recordsDropped >= 0,
243
+ "engineDebugQuery.recordsDropped must be non-negative",
244
+ );
245
+
246
+ const payloadOut = new Uint8Array(1024);
247
+ const firstRecordId = query.recordsReturned > 0 ? readU64LE(headers, 0) : 0n;
248
+ assertThrows(
249
+ () => engineDebugGetPayload(engineId, 1n << 64n, new Uint8Array(16)),
250
+ /u64/i,
251
+ "engineDebugGetPayload with out-of-range recordId must throw",
252
+ );
253
+ const payloadBytes = engineDebugGetPayload(engineId, firstRecordId, payloadOut);
254
+ assert(
255
+ Number.isInteger(payloadBytes),
256
+ "engineDebugGetPayload must return an integer byte count/result code",
257
+ );
258
+ if (payloadBytes >= 0) {
259
+ assert(
260
+ payloadBytes <= payloadOut.byteLength,
261
+ "engineDebugGetPayload bytes must fit output buffer",
262
+ );
263
+ }
264
+
265
+ const debugStats = engineDebugGetStats(engineId);
266
+ assertDebugStatsShape(debugStats);
267
+
268
+ const exportedBytes = engineDebugExport(engineId, new Uint8Array(64 * 1024));
269
+ assert(Number.isInteger(exportedBytes), "engineDebugExport must return integer bytes/result code");
270
+ if (exportedBytes >= 0) {
271
+ assert(exportedBytes <= 64 * 1024, "engineDebugExport bytes must fit output buffer");
272
+ }
273
+
274
+ assert(engineDebugReset(engineId) === ZR_OK, "engineDebugReset must return ZR_OK");
275
+ assert(engineDebugDisable(engineId) === ZR_OK, "engineDebugDisable must return ZR_OK");
276
+
277
+ const worker = new Worker(new URL("./smoke-worker.mjs", import.meta.url), {
278
+ workerData: { engineId },
279
+ type: "module",
280
+ });
281
+
282
+ const alive = await new Promise((resolve, reject) => {
283
+ const onExit = (code) => reject(new Error(`worker exited with ${code}`));
284
+ const onError = (err) => reject(err);
285
+ const onMessage = (msg) => {
286
+ worker.off("exit", onExit);
287
+ worker.off("error", onError);
288
+ resolve(msg);
289
+ };
290
+ worker.once("exit", onExit);
291
+ worker.once("error", onError);
292
+ worker.once("message", onMessage);
293
+ });
294
+
295
+ assert(alive.phase === "alive", "worker must send alive phase");
296
+ assert(
297
+ alive.present === ZR_ERR_INVALID_ARGUMENT,
298
+ `wrong-thread enginePresent must return ZR_ERR_INVALID_ARGUMENT, got: ${alive.present}`,
299
+ );
300
+ assert(
301
+ alive.postUserEvent === ZR_ERR_INVALID_ARGUMENT,
302
+ `wrong-thread enginePostUserEvent must return ZR_ERR_INVALID_ARGUMENT, got: ${alive.postUserEvent}`,
303
+ );
304
+ assert(
305
+ alive.setConfig === ZR_ERR_INVALID_ARGUMENT,
306
+ `wrong-thread engineSetConfig must return ZR_ERR_INVALID_ARGUMENT, got: ${alive.setConfig}`,
307
+ );
308
+ assert(
309
+ alive.debugDisable === ZR_ERR_INVALID_ARGUMENT,
310
+ `wrong-thread engineDebugDisable must return ZR_ERR_INVALID_ARGUMENT, got: ${alive.debugDisable}`,
311
+ );
312
+
313
+ engineDestroy(engineId);
314
+ engineDestroy(engineId); // idempotent
315
+
316
+ worker.postMessage({ type: "afterDestroy" });
317
+
318
+ const destroyed = await new Promise((resolve, reject) => {
319
+ const onExit = (code) => reject(new Error(`worker exited with ${code}`));
320
+ const onError = (err) => reject(err);
321
+ const onMessage = (msg) => {
322
+ worker.off("exit", onExit);
323
+ worker.off("error", onError);
324
+ resolve(msg);
325
+ };
326
+ worker.once("exit", onExit);
327
+ worker.once("error", onError);
328
+ worker.once("message", onMessage);
329
+ });
330
+
331
+ assert(destroyed.phase === "destroyed", "worker must send destroyed phase");
332
+ assert(
333
+ destroyed.postUserEvent === ZR_ERR_INVALID_ARGUMENT,
334
+ `postUserEvent after destroy must return ZR_ERR_INVALID_ARGUMENT, got: ${destroyed.postUserEvent}`,
335
+ );
336
+
337
+ await worker.terminate();