Files

5.1 KiB

Context

JavaFxRuntime.runLater(...) currently combines lazy JavaFX toolkit startup with task submission. Mission Control and Process Manager use it in the active Hub graph; Composer, Test Tool, Soda Task Manager, and Suwimo Client use it in dormant modern paths. Consequently, any consumer can create global runtime state without a registered component or Hub lifecycle milestone.

The Registry already indexes one registered object through every implemented component interface, and the Hub already starts selected NenjimProcess objects in a hardcoded dependency-safe order and records IDs only after successful return. The new runtime uses those existing mechanisms.

Goals / Non-Goals

Goals:

  • Represent the one JVM-wide JavaFX toolkit as an explicit service object.
  • Keep lifecycle control out of the public service API.
  • Permit exactly one startup attempt and publish availability only after the startup callback configures JavaFX successfully.
  • Reject every submission until successful startup and submit accepted work only through Platform.runLater(...).
  • Inject one shared service into all six modern JavaFX consumers.
  • Start and record the runtime before both enabled JavaFX applications.

Non-Goals:

  • Stop, restart, recovery, supervision, health, status, configuration, events, window discovery/management, automatic dependency startup, Contexts, classloading, headless JavaFX, or replacement of the JavaFX UI APIs.
  • Refactoring unrelated legacy prototype code or adding a general test suite.

Decisions

One object has separate service and process views

JavaFxRuntimeService extends only NenjimComponent and exposes only runLater(Runnable). The public JavaFxRuntimeServiceImpl implements that service plus NenjimProcess. A single Registry registration therefore indexes the identical object through both interfaces without exposing lifecycle on the service contract or requiring a manager/admin API.

Startup is strict, synchronous at the milestone, and non-recovering

An atomic guard is consumed before any JavaFX call, so a second start() fails even after an interrupted or failed first attempt. start() calls Platform.startup(...) exactly once. Its callback sets implicit exit to false, captures callback failure, and releases a latch. The caller waits on that latch before opening a volatile availability gate or returning.

Synchronous toolkit failures, including an externally initialized toolkit, are wrapped with a service-specific startup diagnostic. Callback failure and interruption likewise leave availability closed. No path accepts an existing external toolkit, retries initialization, or rolls JavaFX back.

Submission is gated and remains asynchronous

runLater(...) validates the task before checking availability, so null is always reported as a clear caller error. An unavailable service rejects work without invoking any JavaFX API. After startup it delegates directly to Platform.runLater(...); the service never invokes application work itself, and failures inside queued UI work remain application failures.

Hub ordering expresses the dependency

During application composition the Hub creates one runtime implementation and registers it as nenjim.javafx-runtime.service. The same service reference is injected into Mission Control and Process Manager. In the fixed startup list, the runtime follows the non-JavaFX business processes and precedes Mission Control and Process Manager. The common startProcess helper records its ID only after successful return. A runtime failure therefore propagates before either dependent JavaFX process starts.

Dormant consumers have no compatibility path

Composer, Test Tool, Soda Task Manager, and Suwimo Client receive the service through their only constructors even though the active Hub does not currently construct them. All six consumers retain that dependency and replace static submission with the injected service. Removing the old utility then makes an implicit modern initialization path impossible at compile time.

Risks / Trade-offs

  • [JavaFX is JVM-global and cannot be restarted] → The service deliberately accepts one attempt and treats prior external initialization as failure.
  • [Startup callback execution timing is platform-controlled] → A latch makes the successful service milestone explicit before start() returns.
  • [Interrupting startup can leave a JVM toolkit that later finishes] → The service remains unavailable and non-retryable; no unsupported rollback or recovery semantics are claimed.
  • [Queued UI work can fail later] → Task execution remains application-owned and is not confused with the already completed runtime startup milestone.

Migration Plan

  1. Introduce the service/process implementation and migrate all six consumers.
  2. Register, inject, start, and record the shared object in Hub order.
  3. Delete the static helper and audit modern sources for initialization paths.
  4. Update documentation and this focused active OpenSpec change.
  5. Strictly validate OpenSpec, build the repository, and use disposable probes for lifecycle guards without starting production main().