Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ _In development — bullets added per PR; finalized at release._

- **fix(ws): start the LiveWS sidecar with `cwd` at the package root (global/systemd installs)** — the standalone LiveWS launcher (`scripts/start-ws-server.mjs`) re-spawns itself with `node --import tsx <self>` but did not set `cwd`. When the WebSocket sidecar was launched from outside the package directory — a global npm/homebrew install, or a `systemd`/`launchd` unit started from `$HOME` — Node could not resolve the `tsx` package (`ERR_MODULE_NOT_FOUND: Cannot find package 'tsx'`), and even from the package directory `tsx` could not resolve the tsconfig `@/*` path aliases (e.g. `@/types/databaseSettings`), so the sidecar never booted. The spawn now pins `cwd` to the package root (the directory above `scripts/`, where `package.json` + `tsconfig.json` live), which resolves both `tsx` discovery and the `@/*` aliases regardless of launch directory. ([#4055](https://github.com/diegosouzapw/OmniRoute/issues/4055) — thanks @Rahulsharma0810)
- **fix(dashboard): Logs page auto-refresh now works in embedded/proxied dashboards** — the Request Logger gated each auto-refresh tick on a static `document.visibilityState === "visible"` read. Hosts that report a permanent non-`"visible"` state without ever firing a `visibilitychange` event (Docker dashboard wrappers, embedded webviews) froze auto-refresh entirely — only the manual Refresh button worked, a regression from 3.8.24's unconditional polling. The pause is now event-driven and fail-open: polling starts enabled and only pauses after a real `visibilitychange` → hidden transition (still preserving the backgrounded-tab optimization for normal browser tabs). ([#4054](https://github.com/diegosouzapw/OmniRoute/issues/4054) — thanks @tjengbudi)
- **fix(docker): raise the build-stage Node heap to stop the production-build OOM** — the Docker `builder` stage ran `npm run build` with V8's default heap ceiling (~2 GB). After #4052 forced the heavier webpack engine (Turbopack panics on this Next.js version), the production optimization pass exceeded that ceiling and the build died with `FATAL ERROR: … JavaScript heap out of memory` at `[builder] npm run build`. The builder stage now sets `NODE_OPTIONS=--max-old-space-size` (default 4096 MB, overridable via `--build-arg OMNIROUTE_BUILD_MEMORY_MB=…`) before the build; the value propagates to the spawned `next build`. Build-only — the runtime heap (`OMNIROUTE_MEMORY_MB` on the runner stage) is unchanged. ([#4076](https://github.com/diegosouzapw/OmniRoute/issues/4076) — thanks @kamenkadmitry)

---

Expand Down
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ RUN --mount=type=cache,target=/root/.npm \
# See docs/ops/QUALITY_GATE_PLAYBOOK.md Parte 6.
ENV OMNIROUTE_USE_TURBOPACK=0

# Raise the V8 heap ceiling for the build. The webpack production optimization
# pass (forced above since Turbopack panics) needs more than V8's default ceiling
# (~2 GB) for a codebase this size; a memory-constrained Docker build otherwise
# dies with "FATAL ERROR: ... JavaScript heap out of memory" at `[builder] npm run
# build` (#4076). NODE_OPTIONS propagates to the spawned `next build` child
# (build-next-isolated.mjs → resolveNextBuildEnv spreads process.env). Build-only;
# the runtime heap is set separately on the runner stage (OMNIROUTE_MEMORY_MB).
# Override for hosts with more/less RAM: `--build-arg OMNIROUTE_BUILD_MEMORY_MB=6144`.
ARG OMNIROUTE_BUILD_MEMORY_MB=4096
ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_BUILD_MEMORY_MB}"

COPY . ./
RUN --mount=type=cache,target=/app/.build/next/cache \
mkdir -p /app/data && npm run build
Expand Down
74 changes: 74 additions & 0 deletions tests/unit/dockerfile-build-heap-4076.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* #4076 — Docker build fails with "JavaScript heap out of memory" during the
* `[builder] npm run build` step. The webpack production optimization pass (forced
* since #4052 replaced the panicking Turbopack engine) needs more heap than V8's
* default ceiling, which a memory-constrained Docker build does not provide.
*
* Fix: the `builder` stage must raise the heap ceiling via NODE_OPTIONS
* (`--max-old-space-size`) BEFORE running `npm run build`, so the value propagates
* to the spawned `next build` child (build-next-isolated.mjs → resolveNextBuildEnv
* spreads process.env). This is a Docker-only change — CI/local builds invoke
* `npm run build` directly and are unaffected.
*
* This guards the mechanism (the heap setting is present and ordered correctly);
* the end-to-end "the OOM is gone" proof is a successful `docker build`.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const dockerfile = fs.readFileSync(path.join(repoRoot, "Dockerfile"), "utf-8");
const lines = dockerfile.split("\n");

/** Line indices that bound the `builder` stage (from its FROM to the next FROM). */
function builderStageRange(): { start: number; end: number } {
const start = lines.findIndex((l) => /^FROM\s+\S+\s+AS\s+builder\b/i.test(l.trim()));
assert.ok(start >= 0, "Dockerfile must declare a `builder` stage");
const after = lines.slice(start + 1).findIndex((l) => /^FROM\s+/i.test(l.trim()));
const end = after === -1 ? lines.length : start + 1 + after;
return { start, end };
}

test("#4076 builder stage raises the Node heap ceiling via NODE_OPTIONS", () => {
const { start, end } = builderStageRange();
const stage = lines.slice(start, end);
const heapLineIdx = stage.findIndex(
(l) => /NODE_OPTIONS/.test(l) && /--max-old-space-size/.test(l)
);
assert.ok(
heapLineIdx >= 0,
"builder stage must set NODE_OPTIONS with --max-old-space-size to avoid the #4076 build OOM"
);
});

test("#4076 the heap ceiling is set BEFORE `npm run build` so it reaches `next build`", () => {
const { start, end } = builderStageRange();
const stage = lines.slice(start, end);
const heapLineIdx = stage.findIndex(
(l) => /NODE_OPTIONS/.test(l) && /--max-old-space-size/.test(l)
);
const buildLineIdx = stage.findIndex((l) => /npm run build\b/.test(l));
assert.ok(buildLineIdx >= 0, "builder stage must run `npm run build`");
assert.ok(heapLineIdx >= 0, "builder stage must set the heap ceiling");
assert.ok(
heapLineIdx < buildLineIdx,
"NODE_OPTIONS heap ceiling must be set before the `npm run build` step"
);
});

test("#4076 the build heap default is at least 4096 MB (the V8 default ~2 GB OOMed)", () => {
const { start, end } = builderStageRange();
const stage = lines.slice(start, end).join("\n");
// Match the literal default in either `--max-old-space-size=N` or an ARG default
// referenced by the ENV (e.g. ARG OMNIROUTE_BUILD_MEMORY_MB=4096).
const direct = stage.match(/--max-old-space-size=(\d+)/);
const argDefault = stage.match(/ARG\s+\w*MEMORY\w*\s*=\s*(\d+)/i);
const value = Number(direct?.[1] ?? argDefault?.[1] ?? 0);
assert.ok(
value >= 4096,
`build heap default must be >= 4096 MB to clear the #4076 OOM (found ${value})`
);
});
Loading