Skip to content

Commit dead50c

Browse files
committed
Merge remote-tracking branch 'origin/codex/fix-node-auto-execution-mode' into codex/merge-open-fix-prs
# Conflicts: # CHANGELOG.md
2 parents f31dcb7 + 8e93379 commit dead50c

File tree

8 files changed

+63
-9
lines changed

8 files changed

+63
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The format is based on Keep a Changelog and the project follows Semantic Version
1414
- **core/layout**: Layout and interactive prop validators now reject non-object prop bags instead of silently treating them as empty props.
1515
- **core/layout**: Invalid-prop diagnostics now stay safe when rendering the received value would throw during stringification.
1616
- **core/runtime**: `internal_onRender` and runtime breadcrumb render timing now use the always-on monotonic clock even when perf instrumentation is disabled.
17+
- **node/backend**: Auto execution mode now falls back to inline for headless worker-ineligible runs, and worker environment checks reject empty `nativeShimModule` strings.
1718

1819
### Tests
1920

@@ -23,6 +24,7 @@ The format is based on Keep a Changelog and the project follows Semantic Version
2324
- **core/layout**: Added regressions covering non-object prop bags for stack/box layout validators and all top-level interactive validators.
2425
- **core/layout**: Added a regression covering hostile invalid prop values that throw during diagnostic stringification.
2526
- **core/runtime**: Added deterministic regressions for widget-mode breadcrumb render timing and draw-mode `internal_onRender` timing.
27+
- **node/backend**: Added node backend regressions for auto-mode fallback selection and worker environment support checks.
2628

2729
## [0.1.0-alpha.60] - 2026-03-14
2830

docs/architecture/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ Both formats are little-endian, 4-byte aligned, and versioned. Mismatched versio
6969

7070
The Node/Bun backend supports three execution modes:
7171

72-
- **`"auto"`** (default): selects `"inline"` when `fpsCap <= 30`, otherwise `"worker"`.
72+
- **`"auto"`** (default): selects `"inline"` when `fpsCap <= 30`; otherwise prefers `"worker"` and falls back to `"inline"` when no TTY or `nativeShimModule` is available.
7373
- **`"worker"`**: native engine runs on a dedicated worker thread. Main thread is never blocked by terminal I/O.
7474
- **`"inline"`**: engine runs on the main thread. Lower latency, but main thread blocks during I/O.
7575

docs/backend/node.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ knobs aligned:
2626

2727
- `maxEventBytes` is applied to both app parsing and backend transport buffers.
2828
- `fpsCap` is the single scheduling knob.
29-
- `executionMode: "auto"` resolves to inline when `fpsCap <= 30`, worker otherwise.
29+
- `executionMode: "auto"` resolves to inline when `fpsCap <= 30`, otherwise it prefers worker mode and falls back to inline for headless runs without a TTY or `nativeShimModule`.
3030

3131
Development hot reload:
3232

@@ -40,7 +40,7 @@ Development hot reload:
4040

4141
Execution mode details:
4242

43-
- `auto` (default): select inline for low-fps workloads (`fpsCap <= 30`), worker otherwise.
43+
- `auto` (default): select inline for low-fps workloads (`fpsCap <= 30`); otherwise prefer worker mode and fall back to inline when no TTY or `nativeShimModule` is available.
4444
- `worker`: force worker-thread engine execution. With the real native addon this
4545
requires an interactive TTY; test harnesses can provide `nativeShimModule`
4646
instead.

docs/backend/worker-model.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Rezi supports three backend execution modes via `config.executionMode`:
44

5-
- `auto` (default): inline when `fpsCap <= 30`, worker otherwise
5+
- `auto` (default): inline when `fpsCap <= 30`; otherwise prefer worker and fall back to inline when no TTY or `nativeShimModule` is available
66
- `worker`: run native engine/polling on a worker thread
77
- `inline`: run native engine inline on the main JS thread
88

docs/packages/node.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ bun add @rezi-ui/node
2525

2626
Set `config.executionMode` on `createNodeApp(...)`:
2727

28-
- `auto` (default): inline when `fpsCap <= 30`, worker otherwise
28+
- `auto` (default): inline when `fpsCap <= 30`; otherwise prefer worker and fall back to inline when no TTY or `nativeShimModule` is available
2929
- `worker`: always run the engine on a worker thread
3030
- `inline`: run the engine inline on the main JS thread
3131

packages/node/src/__tests__/config_guards.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from "@rezi-ui/core";
1414
import { ZrUiError } from "@rezi-ui/core";
1515
import { selectNodeBackendExecutionMode } from "../backend/nodeBackend.js";
16+
import { hasWorkerEnvironmentSupport } from "../backend/nodeBackend/executionMode.js";
1617
import { createNodeApp, createNodeBackend } from "../index.js";
1718

1819
async function withTempDir<T>(run: (dir: string) => Promise<T> | T): Promise<T> {
@@ -204,6 +205,39 @@ test("config guard: worker mode with shim does not fallback", () => {
204205
});
205206
});
206207

208+
test("config guard: auto mode falls back to inline when worker prerequisites are unavailable", () => {
209+
const selection = selectNodeBackendExecutionMode({
210+
requestedExecutionMode: "auto",
211+
fpsCap: 60,
212+
hasAnyTty: false,
213+
});
214+
assert.deepEqual(selection, {
215+
resolvedExecutionMode: "worker",
216+
selectedExecutionMode: "inline",
217+
fallbackReason: "worker backend requires a TTY or nativeShimModule in auto mode",
218+
});
219+
});
220+
221+
test("config guard: explicit worker mode stays on worker without silent fallback", () => {
222+
const selection = selectNodeBackendExecutionMode({
223+
requestedExecutionMode: "worker",
224+
fpsCap: 60,
225+
hasAnyTty: false,
226+
});
227+
assert.deepEqual(selection, {
228+
resolvedExecutionMode: "worker",
229+
selectedExecutionMode: "worker",
230+
fallbackReason: null,
231+
});
232+
});
233+
234+
test("config guard: worker environment support rejects empty shim strings", () => {
235+
assert.equal(hasWorkerEnvironmentSupport(undefined, false), false);
236+
assert.equal(hasWorkerEnvironmentSupport("", false), false);
237+
assert.equal(hasWorkerEnvironmentSupport("mock://native-shim", false), true);
238+
assert.equal(hasWorkerEnvironmentSupport(undefined, true), true);
239+
});
240+
207241
test("createNodeApp constructs a compatible app/backend pair", () => {
208242
const app = createNodeApp({
209243
initialState: { value: 0 },

packages/node/src/backend/nodeBackend/executionMode.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,17 @@ export function hasInteractiveTty(): boolean {
3737
);
3838
}
3939

40+
export function hasWorkerEnvironmentSupport(
41+
nativeShimModule: string | undefined,
42+
hasAnyTty: boolean,
43+
): boolean {
44+
return hasAnyTty || (typeof nativeShimModule === "string" && nativeShimModule.length > 0);
45+
}
46+
4047
export function selectNodeBackendExecutionMode(
4148
input: NodeBackendExecutionModeSelectionInput,
4249
): NodeBackendExecutionModeSelection {
43-
const { requestedExecutionMode, fpsCap } = input;
50+
const { requestedExecutionMode, fpsCap, hasAnyTty, nativeShimModule } = input;
4451
const resolvedExecutionMode: "worker" | "inline" =
4552
requestedExecutionMode === "inline"
4653
? "inline"
@@ -49,6 +56,17 @@ export function selectNodeBackendExecutionMode(
4956
: fpsCap <= 30
5057
? "inline"
5158
: "worker";
59+
if (
60+
requestedExecutionMode === "auto" &&
61+
resolvedExecutionMode === "worker" &&
62+
!hasWorkerEnvironmentSupport(nativeShimModule, hasAnyTty)
63+
) {
64+
return {
65+
resolvedExecutionMode,
66+
selectedExecutionMode: "inline",
67+
fallbackReason: "worker backend requires a TTY or nativeShimModule in auto mode",
68+
};
69+
}
5270
return {
5371
resolvedExecutionMode,
5472
selectedExecutionMode: resolvedExecutionMode,
@@ -57,8 +75,7 @@ export function selectNodeBackendExecutionMode(
5775
}
5876

5977
export function assertWorkerEnvironmentSupported(nativeShimModule: string | undefined): void {
60-
if (nativeShimModule !== undefined) return;
61-
if (hasInteractiveTty()) return;
78+
if (hasWorkerEnvironmentSupport(nativeShimModule, hasInteractiveTty())) return;
6279
throw new ZrUiError(
6380
"ZRUI_BACKEND_ERROR",
6481
'Worker backend requires a TTY when using @rezi-ui/native. Use `executionMode: "inline"` for headless runs or pass `nativeShimModule` in test harnesses.',

packages/node/src/backend/nodeBackend/shared.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import type { DebugBackend, RuntimeBackend } from "@rezi-ui/core";
33
export type NodeBackendConfig = Readonly<{
44
/**
55
* Runtime execution mode:
6-
* - "auto": pick inline only for very low fps caps (<=30), worker otherwise
6+
* - "auto": pick inline for very low fps caps (<=30); otherwise prefer worker
7+
* and fall back to inline when no TTY/native shim is available
78
* - "worker": worker-thread engine ownership
89
* - "inline": single-thread inline backend (no worker-hop transport)
910
*/

0 commit comments

Comments
 (0)