Context
Wiring a Store to a websocket backend through remotes-api — autoLoad: 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:
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:
config.remotesApiUrl && import('../remotes/Api.mjs').then(module => module.default.load());
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:
service = Neo.ns(apiArray.join('.'));
if (!service) { console.error('Api is not defined', this) }
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.mjs — onConstructed() one-microtask autoLoad trigger; the api branch of load() that console.errors and returns when the stub is absent.
src/remotes/Api.mjs — load() (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):
- 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.
- 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.
- Store-side retry/backoff on a missing stub — rejected; polling is a worse shape than awaiting a deterministic ready signal.
Acceptance Criteria
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.autoLoad ↔ remotes.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).
Context
Wiring a
Storeto a websocket backend throughremotes-api—autoLoad: trueplusapi: {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 theautoLoadload()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-apistubs, however, register asynchronously behind a dynamic import and a networkfetch:// 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'sload()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 loadResult: a store with
autoLoad: true+ a remoteapireliably 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.mjs—onConstructed()one-microtask autoLoad trigger; theapibranch ofload()thatconsole.errors and returns when the stub is absent.src/remotes/Api.mjs—load()(async fetch) andregister()(buildsNeo.ns('<ns>.<Service>').<method>);generateRemote()routes the call viaNeo.currentWorker.promiseMessage('data', {action: 'rpc', ...}).src/worker/App.mjs— the fire-and-forgetApi.load()kickoff, uncoordinated with app / view / store construction.apps/colorscalls its remote fromViewportController.onComponentConstructed → ColorService.read()— a post-construction controller hook — rather than storeautoLoad. That is why it works, and it confirms the transport + wiring are sound; anautoLoad-driven store simply has no equivalent late hook.The Fix
Make an api-store's first
autoLoadload()awaitremotes-apiregistration. Candidate shapes (recommend #1):Neo.remotes.Api.Api.load()resolves aregisteredpromise (or fires an event) afterregister()completes.Store.load()'s api branch, when the stub is missing,awaits it and re-resolves — replacing theconsole.error('Api is not defined')bail with a deferred resolve. Localized to remote stores; no boot delay for non-remote apps.Api.load()before main-view creation insrc/worker/App.mjs. Simplest, but delays boot for every app declaringremotesApiUrl, including those not using autoLoad.Acceptance Criteria
StorewithautoLoad: true+ websocketapi: {read: '...'}loads on boot with no controller-side workaround."Api is not defined"bail occurs for a correctly-wired remote api store during boot.url/ static autoLoad stores are unaffected; apps withoutremotesApiUrlincur no added boot latency.apps/colorsexplicit controller-call pattern still works (no regression).Out of Scope
generateRemoteStream) — this is the request/responsereadpath only.Avoided Traps
remotesApiUrlapp, including non-autoLoad ones (kept as fallback #2, not preferred).Decision Record impact
none— no existing ADR governsStore.autoLoad↔remotes.Apicoordination. The chosen shape may warrant a short ADR if accepted.Related
Empirically reproduced against a live
autoLoad: true+ websocket-apistore; theapps/colorsexample demonstrates the working controller-hook alternative.Handoff Retrieval Hint:
"Store autoLoad remotes-api registration boot race"(query_summaries / query_raw_memories).