LearnNewsExamplesServices
Frontmatter
id12132
titleStore autoLoad fires before remotes-api registration on boot
stateClosed
labels
enhancementaiarchitecturecore
assigneesneo-opus-ada
createdAtMay 28, 2026, 1:36 PM
updatedAtMay 28, 2026, 3:33 PM
githubUrlhttps://github.com/neomjs/neo/issues/12132
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 28, 2026, 3:33 PM

Store autoLoad fires before remotes-api registration on boot

Closed Backlog/active-chunk-16 enhancementaiarchitecturecore
neo-opus-ada
neo-opus-ada commented on May 28, 2026, 1:36 PM

Context

Wiring a Store to a websocket backend through remotes-apiautoLoad: true plus api: {read: '<ns>.<Service>.read'} — the store never loads on boot and no RPC is ever sent, even though the wiring is correct and the backend is reachable. Calling the same remote later from a controller hook works. That isolates the failure to a boot-ordering race, not a wiring defect.

The Problem

Store.onConstructed() schedules the autoLoad load() just one microtask after construction:

// src/data/Store.mjs — onConstructed()
me.trap(Promise.resolve()).then(() => {
    if (me.isLoaded)      { me.fire('load', {items: me.items}) }
    else if (me.autoLoad) { me.load() }
})

The remotes-api stubs, however, register asynchronously behind a dynamic import and a network fetch:

// src/worker/App.mjs — fire-and-forget, not awaited
config.remotesApiUrl && import('../remotes/Api.mjs').then(module => module.default.load());
// src/remotes/Api.mjs — load() fetches the JSON, THEN register() builds the Neo.ns stubs
fetch(path).then(r => r.json()).then(data => { /* ... */ this.register(data) })

The fetch + register resolves many ticks after the store's one-microtask autoLoad. So the store's load() runs first, finds no stub, and bails:

// src/data/Store.mjs — load(), api branch
service = Neo.ns(apiArray.join('.'));
if (!service) { console.error('Api is not defined', this) }   // no RPC, no load

Result: a store with autoLoad: true + a remote api reliably loses the race on boot. The error is often not even visible, since it can fire before an AI client connects to capture console output.

The Architectural Reality

  • src/data/Store.mjsonConstructed() one-microtask autoLoad trigger; the api branch of load() that console.errors and returns when the stub is absent.
  • src/remotes/Api.mjsload() (async fetch) and register() (builds Neo.ns('<ns>.<Service>').<method>); generateRemote() routes the call via Neo.currentWorker.promiseMessage('data', {action: 'rpc', ...}).
  • src/worker/App.mjs — the fire-and-forget Api.load() kickoff, uncoordinated with app / view / store construction.
  • apps/colors calls its remote from ViewportController.onComponentConstructed → ColorService.read() — a post-construction controller hook — rather than store autoLoad. That is why it works, and it confirms the transport + wiring are sound; an autoLoad-driven store simply has no equivalent late hook.

The Fix

Make an api-store's first autoLoad load() await remotes-api registration. Candidate shapes (recommend #1):

  1. Registration-ready signal on Neo.remotes.Api. Api.load() resolves a registered promise (or fires an event) after register() completes. Store.load()'s api branch, when the stub is missing, awaits it and re-resolves — replacing the console.error('Api is not defined') bail with a deferred resolve. Localized to remote stores; no boot delay for non-remote apps.
  2. Await Api.load() before main-view creation in src/worker/App.mjs. Simplest, but delays boot for every app declaring remotesApiUrl, including those not using autoLoad.
  3. Store-side retry/backoff on a missing stub — rejected; polling is a worse shape than awaiting a deterministic ready signal.

Acceptance Criteria

  • A Store with autoLoad: true + websocket api: {read: '...'} loads on boot with no controller-side workaround.
  • No "Api is not defined" bail occurs for a correctly-wired remote api store during boot.
  • url / static autoLoad stores are unaffected; apps without remotesApiUrl incur no added boot latency.
  • The apps/colors explicit controller-call pattern still works (no regression).

Out of Scope

  • RPC streams (generateRemoteStream) — this is the request/response read path only.
  • Websocket reconnect / connection-loss handling.
  • Backend persistence concerns.

Avoided Traps

  • Store-side polling for the stub — a retry loop is strictly worse than awaiting a deterministic registration signal (rejected, #3).
  • Blanket awaiting all remotes before boot — penalizes every remotesApiUrl app, including non-autoLoad ones (kept as fallback #2, not preferred).

Decision Record impact

none — no existing ADR governs Store.autoLoadremotes.Api coordination. The chosen shape may warrant a short ADR if accepted.

Related

Empirically reproduced against a live autoLoad: true + websocket-api store; the apps/colors example demonstrates the working controller-hook alternative.

Handoff Retrieval Hint: "Store autoLoad remotes-api registration boot race" (query_summaries / query_raw_memories).

tobiu referenced in commit be50f4f - "fix(data): autoLoad awaits remotes-api registration (#12132) (#12134) on May 28, 2026, 3:33 PM
tobiu closed this issue on May 28, 2026, 3:33 PM
tobiu referenced in commit f7862b2 - "fix(data): api stores no longer auto-create a default Pipeline (#12170) (#12171) on May 29, 2026, 2:22 PM