Skip to content

feat: instant policy creation — async 0G anchor + optimistic skeleton + anchor polling#19

Merged
AnkanMisra merged 1 commit intomainfrom
feat/optimistic-policy-creation
May 3, 2026
Merged

feat: instant policy creation — async 0G anchor + optimistic skeleton + anchor polling#19
AnkanMisra merged 1 commit intomainfrom
feat/optimistic-policy-creation

Conversation

@AnkanMisra
Copy link
Copy Markdown
Owner

@AnkanMisra AnkanMisra commented May 3, 2026

Why

User report: Quick Demo takes ~10 seconds to create a policy on the live deploy. Watching a button spinner for 10s feels broken even when the work is real (the 5-30s 0G storage upload on Galileo testnet).

Three-layer fix lands together, otherwise none of them solve the problem alone:

  • A button spinner UX cannot make a 10s wait feel fast; it just makes it visible.
  • An optimistic UI without server changes still has the browser's network panel hung for 10s.
  • Server async without UI placeholders leaves the user staring at an empty list for 5-15s.

What changes

Layer 1 — Server: fire-and-forget the 0G anchor

src/memory/zeroGStore.ts now splits the write into two phases:

  • The in-memory map update is synchronous and returns immediately
  • The 0G upload goes through scheduleAnchor(rowId, label, json) which kicks tryAnchor(...) off but does not await it
  • A private pending: Map<string, Promise<void>> parks every in-flight upload by id
  • waitForAnchor(id) settles the parked promise, used by tests + future admin tooling

API response time: 5-30s → ~50ms. The upload still happens; it just doesn't gate the response.

Layer 2 — Frontend: optimistic skeleton in the empty space

web/src/lib/policies.ts:

  • policySkeletonHtml() renders a placeholder .policy-card-skeleton in the same vertical slot a real card would occupy. Shimmer animation (@keyframes skeleton-shimmer) sweeps a subtle paper-soft gradient across each row.
  • loadDemo() injects the skeleton immediately into #policies-list (replacing "No policies yet…"), then POSTs in background. With Layer 1, the POST returns in ~50ms, so loadPolicies() swaps the skeleton for the real card almost instantly.
  • The button itself needs no spinner anymore — the visual is on the card where the user's eye is going.

Layer 3 — Frontend: anchor polling

The real card's pill starts in a new anchorPendingPillHtml() state — same lime accent footprint as the live pill, with:

  • A pulsing leading dot (reusing the masthead's pulse keyframe)
  • Italic "anchoring" text instead of a rootHash
  • anchor-pending-breathe opacity loop at 1.8s
  • cursor: progress to communicate "working in background"

pollForAnchor(policyId):

  • Polls GET /policies/:id every 2s
  • When response includes a non-empty rootHash, removes id from pendingAnchors, re-renders, pill flips to live state
  • Times out after 30s — pill drops to terminal "not anchored" marker
  • Tolerant of blips: any failed poll just keeps the loop running

renderPolicyCard() is the single source of truth for card shape; it reads pendingAnchors to decide between pending pill / live pill / not-anchored marker.

Files touched

File Change
src/memory/zeroGStore.ts scheduleAnchor() + pending map + waitForAnchor() for tests; putPolicy/appendDecision no longer await
web/src/lib/policies.ts policySkeletonHtml, pendingAnchors, pollForAnchor, renderPolicyCard; loadDemo redesigned
web/src/lib/format.ts anchorPendingPillHtml()
web/src/styles/global.css .policy-card-skeleton, .skeleton, @keyframes skeleton-shimmer, .anchor-pill-pending, @keyframes anchor-pending-breathe

Verification (local)

  • bun test — 93 specs across 11 files, all green
  • bun run typecheck — server (tsc --noEmit) + web (astro check: 0/0/0) clean
  • bun run build:web — Astro production build succeeds
  • Emoji-bytes scan — zero hits

Verification (preview deploy after CI)

Cloudflare Pages auto-builds preview deploys per branch. After CI green:

  1. Open the preview URL (e.g. https://feat-optimistic-policy-creation.chainshield.pages.dev/)
  2. Click Quick demo — should see:
    • Skeleton card appears instantly (within ~16ms / one frame)
    • Real card replaces it within ~50-200ms (Render network round-trip)
    • Pill shows the pulsing "anchoring" state
    • Pill flips to lime "0G | 0xroot..." within 5-15s without any visual jolt
  3. Compare to production — same click on https://chainshield.pages.dev/ still hangs the button for 5-30s

Tradeoffs

The API contract subtly changes. A Policy returned from POST /policies may now have no anchor field initially. Subsequent GET /policies/:id calls will include the anchor once the upload settles.

  • The CLI demo (bun run demo) sees no anchor on the immediate POST response, but the timeline call ~5s later picks it up. Existing demo behaviour preserved — it never asserted an anchor on the immediate response.
  • Existing 93 tests still pass — the mocked indexer in zeroGStore.test.ts resolves in the same microtask, so by the time the next await runs the anchor is in the map.

This is the architecturally correct shape: anchoring is a background commit, not a synchronous part of the request. Every production system that touches blockchains is built this way.

Out of scope

  • The same async-anchor + polling pattern for POST /evaluate (would also drop the 5-30s eval hang). Deliberately limited to policies in this PR; the evaluate flow has the existing "Working." panel which is a different UX shape and worth treating in a follow-up.
  • Server-Sent Events / WebSockets for anchor delivery instead of polling. Polling at 2s is fine for the hackathon; SSE would add CORS complexity for marginal gain.

View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

…ground and the ui shows an optimistic skeleton while the real card lands; the actual fix is three layers stacked together because the underlying problem (5-30s 0g storage upload on galileo testnet) cannot be papered over with a button spinner; layer 1 (server) splits zeroGStore.putPolicy and appendDecision into two phases - the in-memory write completes synchronously and returns immediately while the tryAnchor upload goes through scheduleAnchor which kicks off the upload but does not await it, so the api hot path drops from 5-30s to ~50ms; a private pending Map<id, Promise<void>> tracks every in-flight upload so the new waitForAnchor(id) helper can be used by tests + admin tooling without coupling production callers to a synchronous wait; layer 2 (frontend) uses the empty space under active policies as the loading visual instead of a rotating blob on the button - clicking quick demo immediately injects a policy-card-skeleton via policySkeletonHtml() into #policies-list with shimmer animation across each row (skeleton-owner, skeleton-version, skeleton-id, skeleton-rules, skeleton-pill) so the eye lands where the real card will be; the @Keyframes skeleton-shimmer slides a subtle paper-soft gradient across each placeholder rectangle every 1.4s; the post returns in ~50ms and loadPolicies() replaces the skeleton with the real card whose pill shows the new pending state from anchorPendingPillHtml() - same lime accent footprint as the live pill but with a pulsing leading dot, italic 'anchoring' text, and a breathing opacity loop (anchor-pending-breathe keyframe at 1.8s) signalling 'still working'; layer 3 (frontend polling) tracks a module-level pendingAnchors Set<string> with each policy id whose anchor has not yet landed, and pollForAnchor(id) GETs /policies/:id every 2s for up to 30s; when the response includes a non-empty rootHash the id is removed from the set, loadPolicies re-renders, and the pill flips from pulsing 'anchoring' to the lime '0G | 0xroot...' link without any visual jolt; on timeout the id is removed and the pill drops to the terminal 'not anchored' marker; the polling loop is intentionally tolerant of network blips and shape mismatches so a brief outage does not flip the pill to terminal early; renderPolicyCard() is the single source of truth for the card shape and reads from pendingAnchors when deciding pending vs not-anchored; the data-policy-id attribute is added so future fine-grained rerenders can target a single card without redrawing the list
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

Warning

Rate limit exceeded

@AnkanMisra has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 51 minutes and 22 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 27ece0e0-3419-4c8a-ae2c-8255399927d8

📥 Commits

Reviewing files that changed from the base of the PR and between 6daf1ba and 852f561.

📒 Files selected for processing (4)
  • src/memory/zeroGStore.ts
  • web/src/lib/format.ts
  • web/src/lib/policies.ts
  • web/src/styles/global.css
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/optimistic-policy-creation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 51 minutes and 22 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying chainshield with  Cloudflare Pages  Cloudflare Pages

Latest commit: 852f561
Status: ✅  Deploy successful!
Preview URL: https://4dea4e13.chainshield.pages.dev
Branch Preview URL: https://feat-optimistic-policy-creat.chainshield.pages.dev

View logs

@AnkanMisra AnkanMisra merged commit dd34daf into main May 3, 2026
3 checks passed
@AnkanMisra AnkanMisra deleted the feat/optimistic-policy-creation branch May 3, 2026 12:28
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