LearnNewsExamplesServices
Frontmatter
id12170
titleapi Store autoLoad never loads: default Pipeline shadows api branch
stateClosed
labels
bugairegressioncore
assigneesneo-opus-ada
createdAtMay 29, 2026, 1:52 PM
updatedAtMay 29, 2026, 2:22 PM
githubUrlhttps://github.com/neomjs/neo/issues/12170
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 29, 2026, 2:22 PM

api Store autoLoad never loads: default Pipeline shadows api branch

Closed Backlog/active-chunk-16 bugairegressioncore
neo-opus-ada
neo-opus-ada commented on May 29, 2026, 1:52 PM

Context

PR #12134 fixed an autoLoad boot race for api-backed stores by making Store.load() await Neo.remotes.Api.ready() before giving up on a missing service stub. While dogfooding that fix in a real remotes-api app (an api-backed Store with autoLoad: true over a websocket), the store still never loaded: service stub registered, ws connected, no Api is not defined error, and zero RPC frames on the wire. The root cause is upstream of #12134's fix — and it makes that fix unreachable for the exact case it targets.

The Problem

Store.load() branches on data source in this order:

if (me.pipeline) { /* connection→parser→normalizer */ }
else if (me.api) { /* remotes-api RPC — the #12134 fix lives here */ }
else { /* url */ }

The else if (me.api) branch is dead code for every store, because every store always has a pipeline instance. beforeSetPipeline() only skips the default-instance path for the url case (!value && me.url && !me.api); for an api store (no url) it falls through to:

return ClassSystemUtil.beforeSetInstance(value, Pipeline, {store: me});

and ClassSystemUtil.beforeSetInstance() mints a default instance for a falsy value:

if (!config && DefaultClass) { config = Neo.create(DefaultClass, defaultValues) }

So pipeline_: null → a real Pipelineif (me.pipeline) always wins → the api RPC path never executes → autoLoad produces no request. Manual service.read() calls work because they bypass Store.load() entirely, which is why this stayed latent until an api-backed store relied on autoLoad.

The Architectural Reality

  • src/data/Store.mjsbeforeSetPipeline() (~L440), load() branch order (if (me.pipeline) ~L911 vs else if (me.api) ~L993), pipeline_: null config (~L158).
  • src/util/ClassSystem.mjsbeforeSetInstance() default-instance creation (if (!config && DefaultClass)).
  • Pipeline (connection→parser→normalizer) and remotes-api (RPC stubs via Neo.currentWorker.promiseMessage) are two parallel, not-yet-combined data-loading universes. The only bridge today is Neo.remotes.Api.register() minting a per-method Pipeline+Rpc for methods that declare parser/normalizer/pipeline. Store-level api loading does not flow through a store pipeline.

The Fix

Stop minting a default Pipeline for api-configured stores in beforeSetPipeline(), so me.pipeline stays null and load() reaches the else if (me.api) branch (which already carries the #12134 fix):

beforeSetPipeline(value, oldValue) {
    let me = this;

    if (me.api) return null;   // api stores load via remotes-api, never a default pipeline

    oldValue?.destroy();

    if (!value && me.url) { /* … url pipeline … */ }

    return ClassSystemUtil.beforeSetInstance(value, Pipeline, {store: me});
}

Order-independent by design: Neo's reactive config getter resolves me.api on demand from configSymbol (Neo.createConfig get-path; initConfig populates the full merged config into configSymbol before processConfigs), so the guard reads the correct api value regardless of which reactive config is processed first.

Variant considered (operator-flagged trade-off)

if (me.api) return null enforces a strict api XOR pipeline invariant: a store that already has an api cannot be switched to a pipeline at runtime without nulling api first. The alternative if (!value && me.api) return null would block only the auto-default (preserving runtime api→pipeline switching via an explicit pipeline value), at the cost of the clean XOR invariant. The runtime-switch case was weighed as ~0.001% likely; defaulting to the strict form.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
Neo.data.Store#pipeline (config) Store.beforeSetPipeline() src/data/Store.mjs api-configured store no longer auto-mints a default Pipeline; pipeline stays null strict: api store never gets a pipeline (runtime switch requires nulling api first) JSDoc on pipeline_ + api_ regression repro: api store autoLoad:true → zero RPC pre-fix
Neo.data.Store#load() (api branch ~L993) src/data/Store.mjs becomes reachable for api stores; fires the remotes-api RPC n/a #12134 fix goes live

Decision Record impact

none — no ADR governs the data.Store Pipeline architecture (the learn/agentos/decisions/* pipeline mentions are unrelated infra/graph ADRs).

Acceptance Criteria

  • beforeSetPipeline() returns null (no default Pipeline) when me.api is set.
  • An api-configured Store with autoLoad: true has pipeline === null after construction.
  • Such a store's load() reaches the else if (me.api) branch and fires the remotes-api RPC.
  • url-based and explicit-pipeline stores are unaffected.
  • Unit test covering an api + autoLoad store reaching the api branch (regression lock).

Out of Scope

  • Unifying the Pipeline and remotes-api universes (routing remotes through connection→parser→normalizer). Separate, larger refactor — see Related.
  • Changing load() branch order.
  • The !value && me.api permissive variant (documented above; not the chosen form).

Avoided Traps

  • Flipping load() branch order (if (me.api) before if (me.pipeline)): treats the symptom, leaves a spurious Pipeline + onPipelinePush listener on every api store, and entrenches "api wins" instead of "api stores have no pipeline." Rejected in favor of fixing the cause in beforeSetPipeline().
  • Assuming a config-ordering hazard when beforeSetPipeline reads me.api: Neo resolves reactive configs on demand from configSymbol, so cross-config reads inside hooks are order-independent. No ordering guard needed.

Related

  • #12132 / #12134 — autoLoad awaits remotes-api registration (the fix this unblocks).
  • Follow-up (to file): unify Pipeline + remotes-api data-loading paths (epic-scale).

Handoff Retrieval Hints

  • Commit anchor: be50f4f6d (#12134) — the api branch this makes reachable.
  • Semantic query: "api store autoLoad pipeline shadows api branch beforeSetPipeline default instance"
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
tobiu closed this issue on May 29, 2026, 2:22 PM