LearnNewsExamplesServices
Frontmatter
id12173
titleAdd backend-first remoteCreate/remoteUpdate/remoteDestroy to data.Store
stateClosed
labels
enhancementaiarchitecturecore
assigneesneo-opus-ada
createdAtMay 29, 2026, 5:50 PM
updatedAtMay 29, 2026, 6:31 PM
githubUrlhttps://github.com/neomjs/neo/issues/12173
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 29, 2026, 6:31 PM

Add backend-first remoteCreate/remoteUpdate/remoteDestroy to data.Store

Closed v13.0.0/archive-v13-0-0-chunk-14 enhancementaiarchitecturecore
neo-opus-ada
neo-opus-ada commented on May 29, 2026, 5:50 PM

Context

The remotes-api read path is complete (#12134 autoLoad-awaits-registration, #12170 api stores skip the default Pipeline). Apps with a remotes-api backend still have no backend-aware mutation: data.Store's add/insert/remove (from collection.Base) are synchronous and local-only. To support full CRUD UIs over a websocket/ajax backend (the immediate driver is a consuming app's CRUD dialogs), we need a backend-first mutation layer. Designed in operator dialogue 2026-05-29.

The Problem

Store mutators are sync (local). A CRUD UI needs: on create/update/delete, call the backend first, and only reflect the change locally if the backend confirms. Making add/insert/remove async would break their synchronous, return-value contract for every existing caller. So the backend-aware operations must be a new async layer that delegates to the sync primitives on success.

The Architectural Reality

  • The api RPC dispatch is already method-generic — no plumbing required:
    • remotes/Api.register() auto-generates an app-side stub for every method in the descriptor (plain → generateRemote; parser/normalizer/pipeline-configured → per-method Pipeline+Rpc).
    • Both converge on Neo.manager.rpc.Message.onMessage({method, service, params}) (src/manager/rpc/Message.mjs:61), which routes by method+type. read is not special; create/update/destroy dispatch identically once defined in the descriptor.
    • data/connection/Rpc already exposes create/read/update + a generic execute(method) (src/data/connection/Rpc.mjs), so destroy needs no wrapper (execute('destroy', …)).
  • data/Record already tracks dirty state (isModified, model.trackModifiedFields, original-data snapshot) → update can send a field delta.
  • Sync mutators stay untouched: collection.Base add(180)/insert(1256)/remove(1395); Store overrides add(258)/insert(857). The new layer wraps, never replaces them.
  • Naming: destroy() is instance-teardown on core.Base(499)/collection.Base(673)/Store(537) and createRecord() is the obj→record converter (Store:597) — hence the remote* prefix: collision-free and it signals the backend round-trip.

The Fix

Add three async methods to Neo.data.Store and extend the api config from {read} to {create, read, update, destroy}:

  • async remoteCreate(data) — if api.create: await the stub; success → add(response.data) (server-authoritative; add() re-sorts when a sorter is present, so position is correct); failure → reject + fire event. No api.create → local add(data).
  • async remoteUpdate(record) — if api.update: send the trackModifiedFields delta; success → apply the server response + clear modified (commit); failure → reject, leave the record modified. No api.update → local commit.
  • async remoteDestroy(record) — if api.destroy: await the stub ({[keyProperty]: id}); success → remove(record); failure → reject + event. No api.destroy → local remove.

A shared private helper resolves api[verb], calls the stub via me.trap(...), and normalizes success/failure.

Error contract: reject (for await … catch callers, e.g. a dialog Save handler) and fire a store event (for passive observers).

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
Neo.data.Store#remoteCreate / #remoteUpdate / #remoteDestroy (new public async methods) src/data/Store.mjs (new) backend-first; gate the sync mutation on success; reject + event on failure absent api.* verb → local sync op JSDoc per method unit tests + consuming-app dogfood
Neo.data.Store#api (config) src/data/Store.mjs api_ extends {read}{create, read, update, destroy} {read}-only stores still valid JSDoc on api_

Decision Record impact

none — no ADR governs data.Store; this extends its public surface additively.

Acceptance Criteria

  • remoteCreate/remoteUpdate/remoteDestroy on data.Store, async, backend-first.
  • Success delegates to the sync primitive with the server-authoritative response (create→add, destroy→remove, update→commit delta).
  • Failure rejects AND fires a store event; the local store stays consistent (no partial mutation).
  • Absent api.* verb → graceful local-only op.
  • update sends the trackModifiedFields delta.
  • Read-only (api:{read}) and non-api stores are unaffected (regression guard).
  • Unit coverage for each verb's success + failure + no-api-fallback paths.

Out of Scope

  • Pipeline ↔ remotes-api unification (the store-level me.pipeline stays its own world).
  • Optimistic-update mode (pessimistic/backend-first is the default; optimistic-with-rollback is a later opt-in config).
  • Batch (array) mutation — single-record first.
  • remoteInsert(index, data) for position-sensitive unsorted stores — deferred until a real case (sorted stores re-sort via add(), making it moot).

Avoided Traps

  • Overloading sync add/insert/remove to be backend-first → would force them async, breaking the synchronous contract for every existing caller. Rejected in favor of a new async layer.
  • Naming the delete hook destroy → collides with instance teardown (core.Base.destroy). Hence remoteDestroy.
  • Routing through the Pipeline universe (which already has create/update) → that's the unification, deliberately deferred; the api RPC backbone already supports CRUD without it.

Related

  • #12134, #12170 — the remotes-api read path this builds on.
  • Consuming-app CRUD wiring is tracked in that app's own tracker (gitlab); it depends on this landing first.

Handoff Retrieval Hints

  • Design dialogue 2026-05-29 (operator-led): the "api path is method-generic" exploration, the remote* naming decision, and the backend-first + server-authoritative + reject/event contract.
  • Anchors: src/data/connection/Rpc.mjs, src/manager/rpc/Message.mjs:61, src/remotes/Api.mjs register().
tobiu referenced in commit 8fec68f - "feat(data): backend-first remoteCreate/remoteUpdate/remoteDestroy on Store (#12173) (#12174) on May 29, 2026, 6:31 PM
tobiu closed this issue on May 29, 2026, 6:31 PM