---
number: 12134
title: 'fix(data): autoLoad awaits remotes-api registration (#12132)'
author: neo-opus-ada
state: MERGED
createdAt: '2026-05-28T13:22:49Z'
updatedAt: '2026-05-28T13:33:43Z'
closedAt: '2026-05-28T13:33:39Z'
mergedAt: '2026-05-28T13:33:39Z'
head: feature/autoload-remotes-registration
base: dev
url: 'https://github.com/neomjs/neo/pull/12134'
---
Authored by Claude Opus 4.7 (Claude Code) as @neo-opus-ada. Session provenance: lane-claim A2A `MESSAGE:894591f6` (#12132).

FAIR-band: **operator-directed pickup** — the operator explicitly assigned #12132 (and #12133) this session, superseding the standing "no new items until open team PRs merge" hold. Author-lane pickup discipline is N/A for directed work.

Resolves #12132

A `Store` with `autoLoad: true` + a websocket `api` lost a boot race: `Store.onConstructed()` fires `load()` one microtask after construction, but the `remotes-api` stubs register behind an async dynamic import + `fetch`, so `load()` found no stub and bailed at `console.error('Api is not defined')` — no RPC, no load.

The fix reuses the existing `core.Base` `initAsync()`/`ready()` lifecycle rather than a hand-rolled promise:
- `Neo.remotes.Api` awaits `load()` inside `initAsync()`, so the singleton only reaches `isReady` once the api JSON is fetched and the service stubs are registered.
- `Store.load()`'s `api` branch awaits `Neo.remotes.Api.ready()` when the stub is missing (gated on `Neo.config.remotesApiUrl`) before giving up, then re-resolves the stub.
- `src/worker/App.mjs` now just imports the module (creation triggers `initAsync` → `load`); the explicit `.then(module.default.load())` is removed to avoid a double-load.

`url`/static `autoLoad` stores and apps without `remotesApiUrl` are unaffected; no boot latency added for non-remote apps.

Evidence: L1 (53/53 `test/playwright/unit/data/` regression green; rides the `core/Base` `initAsync`/`ready()` primitive) → L3 required (api-store `autoLoad`s on boot — observable runtime effect across the App↔Data worker boundary the single-thread unit sim cannot model). Residual: runtime AC verified via integration with a real `autoLoad:true` + websocket-`api` store. [#12132]

## Deltas from ticket
- The ticket proposed a "registration-ready signal on `Neo.remotes.Api`"; implemented via the existing `initAsync`/`ready()` primitive instead of a bespoke promise — cleaner, less new surface.
- Incidental: removed pre-existing trailing whitespace flagged by the `check-shorthand` pre-commit hook in the two touched files (`App.mjs` duplicated JSDoc blocks, `Api.mjs`). Mechanical, no logic change.

## Test Evidence
- `npm run test-unit -- test/playwright/unit/data/` → **53 passed** (no regression from the `Store.load` change).
- `node --check` clean on all three changed files.
- A dedicated boot-race unit test is impractical: the `api` load path bottoms out in a cross-worker RPC (`promiseMessage('data', {action:'rpc'})`) the single-thread unit sim does not model, and the remotes `Api` singleton is created at import-time (mocks cannot be injected before `initAsync`). The fix rides the already-covered `initAsync`/`ready()` primitive.

## Post-Merge Validation
- [ ] In a live app, a `Store` with `autoLoad: true` + websocket `api: {read}` populates on boot with no controller-side workaround.
- [ ] No `"Api is not defined"` console error for a correctly-wired remote `api` store on boot.

## Cross-family review
Per §6.1 this needs a cross-family Approved review before merge; will route to a primary reviewer once CI is green.
