LearnNewsExamplesServices
Frontmatter
id8952
titleBug: Dynamic Worker Start triggers Double App Init & Canvas Race Condition
stateClosed
labels
bugai
assigneestobiu
createdAtFeb 2, 2026, 10:30 PM
updatedAtFeb 2, 2026, 10:39 PM
githubUrlhttps://github.com/neomjs/neo/issues/8952
authortobiu
commentsCount2
parentIssue8948
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtFeb 2, 2026, 10:36 PM

Bug: Dynamic Worker Start triggers Double App Init & Canvas Race Condition

Closed v12.0.0 bugai
tobiu
tobiu commented on Feb 2, 2026, 10:30 PM

Issue 1: Double Application Initialization In src/worker/Manager.mjs, the onWorkerConstructed method checks me.constructedThreads === me.activeWorkers + 1 to decide when to call me.loadApplication(). When startWorker() is called dynamically:

  1. me.activeWorkers is incremented.
  2. The new worker constructs and sends a message.
  3. me.constructedThreads is incremented.
  4. The equality check passes AGAIN (e.g., 5 === 4 + 1), causing loadApplication() to run a second time.

Issue 2: Sparkline Race Condition Neo.component.Canvas (base class) calls Neo.worker.Canvas.registerCanvas in afterSetMounted. When using dynamic workers, initAsync (which starts the worker) might still be running when afterSetMounted fires. This leads to TypeError: Cannot read properties of undefined (reading 'registerCanvas') because the remote method hasn't been registered yet.

Proposed Fixes:

  1. Manager: Add a applicationLoaded flag to prevent multiple loadApplication calls.
  2. SparklineComponent: Override afterSetMounted to await this.ready() before calling super.afterSetMounted().
tobiu added the bug label on Feb 2, 2026, 10:30 PM
tobiu added the ai label on Feb 2, 2026, 10:30 PM
tobiu added parent issue #8948 on Feb 2, 2026, 10:31 PM
tobiu referenced in commit 827e43e - "fix: Resolve Dynamic Worker race conditions and double app init (#8952)" on Feb 2, 2026, 10:35 PM
tobiu assigned to @tobiu on Feb 2, 2026, 10:35 PM
tobiu
tobiu Feb 2, 2026, 10:35 PM

Input from Gemini 3 Pro:

✦ Resolved two critical issues with dynamic worker initialization:

  1. Double Application Initialization:

    • Cause: startWorker incremented activeWorkers, causing the constructedThreads === activeWorkers + 1 check in onWorkerConstructed to pass a second time (e.g., 5 === 4+1), triggering loadApplication again.
    • Fix: Added applicationLoaded flag to src/worker/Manager.mjs to ensure loadApplication is called exactly once.
  2. Sparkline Initialization Race Condition:

    • Cause: initAsync (which starts the worker) and afterSetMounted (which registers the canvas) were racing. The base afterSetMounted would call registerCanvas before initAsync completed (worker booting + remote registration).
    • Fix: Overrode afterSetMounted in SparklineComponent.mjs to await this.ready() before calling super.afterSetMounted(). This enforces strict ordering: the component waits for initAsync (and thus the worker start + module load) to finish before attempting registration.
tobiu closed this issue on Feb 2, 2026, 10:36 PM
tobiu referenced in commit e3fd79d - "#8952 removed useCanvasWorker from the devrank neo-config.json" on Feb 2, 2026, 10:37 PM
tobiu
tobiu Feb 2, 2026, 10:39 PM

Input from Gemini 3 Pro:

Context: The issues resolved here (Double App Init & Race Condition) were triggered specifically when removing useCanvasWorker: true from apps/devrank/neo-config.json.

This confirmed that the "Dynamic Worker" architecture (Epic #8948) works as intended:

  1. The App starts without the Canvas Worker (saving resources).
  2. When SparklineComponent mounts, it dynamically starts the Canvas Worker.
  3. The fixes in this ticket ensure this late-binding process is smooth and doesn't re-trigger application load logic.

The goal of "Zero-Config" usage for worker-based components is now achieved.