Skip to content

feat: optimistic UI updates with chain-truth reconciliation#15

Open
Sentinel-Bluebuilder wants to merge 2 commits into
masterfrom
feat/fe-optimistic-reconcile
Open

feat: optimistic UI updates with chain-truth reconciliation#15
Sentinel-Bluebuilder wants to merge 2 commits into
masterfrom
feat/fe-optimistic-reconcile

Conversation

@Sentinel-Bluebuilder

Copy link
Copy Markdown
Owner

What

Makes mutating actions feel instant by updating the UI optimistically, then
reconciling against chain truth on a short delay + bounded retry — and
invalidating the operator's read caches server-side so the reconcile refetch
sees fresh data.

Why

After link/unlink, add-subscriber, or provider-register, operators had to wait
out the read-cache TTL (30–120s) plus chain indexing before the change
showed. Symptoms: "added nodes show after a big delay", stale subscriber counts,
provider registration not appearing immediately.

Frontend (public/index.html)

  • optimisticThenReconcile(url, { mutate, render, page, delayMs }) — generic
    apply-now-then-refetch wrapper.
  • reconcileProviderViews, reconcilePlanNodes(planId, predicate, {attempts, baseMs}),
    reconcileSubscriberViews — view-specific reconcilers with backoff that
    re-poll until chain truth matches the optimistic state (or attempts run out).
  • seedYourNodesWithAdded(addrs) — surfaces just-linked nodes in Your Nodes
    immediately.

Backend (server.js)

  • invalidateSubscriberCaches(planId) — drops the operator-scoped
    planMembers / ownSub / planStats / uniqueWallets caches, keyed by
    getAddr() to match how they were written.
  • Called on subscribe, add-subscriber, and add-subscribers success
    paths (alongside the existing invalidatePlanSubs).
  • getUniqueWallets wrapped in cached() (120s TTL, plan-keyed) so the
    up-to-10k subscription query stops re-firing on every plan-detail view — and
    so the uniqueWallets:<id> key actually exists for the invalidation above to
    drop.

Notes

  • RPC-first preserved throughout; LCD only as fallback.
  • Pure additive on the server (new function + four invalidation calls + one
    cache wrapper); no existing route behavior changes on a cache miss.

🤖 Generated with Claude Code

After a mutating action (link/unlink node, add subscriber, register
provider) the operator had to wait out the read-cache TTL plus chain
indexing before the change appeared — "added nodes show after a big
delay", "subscriber count stale", etc.

Frontend: apply the change optimistically, then reconcile against chain
truth on a short delay and on a bounded retry schedule, so the UI is
instant but self-corrects if the chain disagrees.
  - optimisticThenReconcile(url, { mutate, render, page }) — generic
    optimistic-then-refetch wrapper
  - reconcileProviderViews / reconcilePlanNodes / reconcileSubscriberViews
    — view-specific reconcilers with backoff (attempts/baseMs)
  - seedYourNodesWithAdded — show just-linked nodes immediately in Your Nodes

Backend: invalidate the operator-scoped read caches the moment a sub
lands so the reconcile refetch sees fresh data instead of a 30-120s
stale snapshot.
  - invalidateSubscriberCaches(planId) drops planMembers/ownSub/planStats/
    uniqueWallets for the operator
  - called on subscribe, add-subscriber, and add-subscribers success
  - getUniqueWallets wrapped in cached() (120s TTL) so its key exists to
    be invalidated and the up-to-10k subscription query stops re-firing
    on every plan detail view
The plan-card pill (totalNodes) snapped back to the PRE-link count after
adding or removing nodes: the chain indexes a link/unlink 1-6s after the
broadcast, so any /api/my-plans refetch (or background SWR revalidate) in
that window returned the stale count and clobbered the optimistic bump.

Add a per-plan sticky override (S.pendingNodeCount):
- setPendingNodeCount(planId, value, dir) records the optimistic value.
- applyPendingNodeCounts(plans) re-paints that value IN PLACE on every
  fetched plans array until the chain count reconciles (>= for adds,
  <= for removes), with a hard 30s safety timeout so a stuck override can
  never pin a wrong number forever. Returns whether anything is still
  pending so refetchMyPlans keeps polling.

Wire it through:
- refetchMyPlans now waits before the first fetch when an override is
  pending (give the indexer a head start), applies overrides each attempt,
  and keeps polling until reconciled (attempts 4 -> 8).
- _ensurePlansLoaded applies overrides on both the SWR onRefresh path and
  the initial fetch so a background revalidate can't snap the card back.
- linkBatch / unlinkBatch register an 'up' / 'down' override alongside the
  existing optimistic bump.

Complements the existing reconcilePlanNodes() node-list reconciliation;
this fixes the card-pill count specifically.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant