feat: instant policy creation — async 0G anchor + optimistic skeleton + anchor polling#19
Conversation
…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
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 51 minutes and 22 seconds.Comment |
Deploying chainshield with
|
| Latest commit: |
852f561
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://4dea4e13.chainshield.pages.dev |
| Branch Preview URL: | https://feat-optimistic-policy-creat.chainshield.pages.dev |
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:
What changes
Layer 1 — Server: fire-and-forget the 0G anchor
src/memory/zeroGStore.tsnow splits the write into two phases:scheduleAnchor(rowId, label, json)which kickstryAnchor(...)off but does not await itpending: Map<string, Promise<void>>parks every in-flight upload by idwaitForAnchor(id)settles the parked promise, used by tests + future admin toolingAPI 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-skeletonin 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, soloadPolicies()swaps the skeleton for the real card almost instantly.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:pulsekeyframe)anchor-pending-breatheopacity loop at 1.8scursor: progressto communicate "working in background"pollForAnchor(policyId):GET /policies/:idevery 2srootHash, removes id frompendingAnchors, re-renders, pill flips to live staterenderPolicyCard()is the single source of truth for card shape; it readspendingAnchorsto decide between pending pill / live pill / not-anchored marker.Files touched
src/memory/zeroGStore.tsscheduleAnchor()+pendingmap +waitForAnchor()for tests;putPolicy/appendDecisionno longer awaitweb/src/lib/policies.tspolicySkeletonHtml,pendingAnchors,pollForAnchor,renderPolicyCard; loadDemo redesignedweb/src/lib/format.tsanchorPendingPillHtml()web/src/styles/global.css.policy-card-skeleton,.skeleton,@keyframes skeleton-shimmer,.anchor-pill-pending,@keyframes anchor-pending-breatheVerification (local)
bun test— 93 specs across 11 files, all greenbun run typecheck— server (tsc --noEmit) + web (astro check: 0/0/0) cleanbun run build:web— Astro production build succeedsVerification (preview deploy after CI)
Cloudflare Pages auto-builds preview deploys per branch. After CI green:
https://feat-optimistic-policy-creation.chainshield.pages.dev/)https://chainshield.pages.dev/still hangs the button for 5-30sTradeoffs
The API contract subtly changes. A
Policyreturned fromPOST /policiesmay now have noanchorfield initially. SubsequentGET /policies/:idcalls will include the anchor once the upload settles.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.zeroGStore.test.tsresolves in the same microtask, so by the time the nextawaitruns 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
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.Need help on this PR? Tag
@codesmithwith what you need.