fix: persist freelance escrow job state - #209
Conversation
collinsezedike
left a comment
There was a problem hiding this comment.
The persistence idea itself is right. Four issues, all inline, one of them (the blank-page gate) is serious enough to block merge on its own.
| ); | ||
|
|
||
| if (!initialReconciliationComplete) { | ||
| return null; |
There was a problem hiding this comment.
JobsProvider returns null for the whole subtree until wallet detection and reconciliation finish, but App.tsx mounts WalletButton, RoleSwitcher, and the job list as children of JobsProvider. Any reload where a restored job has a milestone with assertionId, essentially every returning user once this ships, blanks the entire page, including for users with no wallet installed at all, until every reconciliation read resolves. This should gate only the milestone data that needs it, not the whole app shell.
| } | ||
|
|
||
| const parsed: unknown = JSON.parse(stored); | ||
| return Array.isArray(parsed) && parsed.every(isJob) ? parsed : seedJobs; |
There was a problem hiding this comment.
parsed.every(isJob) discards the entire stored jobs array if even one job or milestone fails validation, instead of dropping just the invalid entry. A user with several valid jobs plus one corrupted or future-schema entry loses all of their real data on reload, replaced with the 3 seed jobs. Filter out invalid entries individually instead of failing the whole array.
| const [jobs, setJobs] = useState<Job[]>(seedJobs); | ||
| const [jobs, setJobs] = useState<Job[]>(loadJobsFromStorage); | ||
| const reconcileTrackerRef = useRef<ReconcileTracker>({ counter: 0, applied: new Map() }); | ||
| const needsInitialReconciliation = jobs.some((job) => |
There was a problem hiding this comment.
needsInitialReconciliation re-scans every job and milestone on every render, but its result is only ever consumed once by the lazy useState initializer below. Every subsequent setJobs call (createJob, submitMilestone, reconcileFromChain, etc.) re-runs this full scan for a value that's already been discarded. Compute it once via a ref or inside the lazy initializer itself.
| })(); | ||
| } | ||
|
|
||
| void initialReconciliationRef.current.finally(() => { |
There was a problem hiding this comment.
This effect re-registers a new .finally() on the same shared promise every time jobs changes during reconciliation. While Promise.all reconciles N milestones, each of the N setJobs calls re-runs this effect; since initialReconciliationRef.current is already set it skips creating a new promise but still calls .finally() again, so N handler closures stack up on one promise and all fire when it settles. Register the callback once, not on every dependency change.
Summary
Persist the freelance-escrow demo's jobs and milestone activity in a namespaced, versioned localStorage entry so created jobs and their on-chain assertion mappings survive page reloads.
The provider now restores validated data synchronously during state initialization, falls back to the seed jobs when storage is missing, malformed, or unavailable, and writes every jobs update without letting storage failures break the in-memory experience. Because on-chain reconciliation is already available, restored milestones with assertion IDs are reconciled before child UI renders when an authorized wallet is available.
Closes #148
Test plan
cargo fmt --check,cargo clippy --workspace --all-targets -- -D warnings, andcargo testpass locally (Rust sources are unchanged; these are left to the required CI job)CONTRACT.mdupdated if the public interface changed (not applicable; no contract interface changes)scripts/testnet-smoke.shrun against testnet, if this changes contract behavior in a way that affects the deployed flow (not applicable; no contract behavior changes)pnpm install --frozen-lockfile,pnpm lint, andpnpm buildpass fordemos/freelance-escrowwith CI's pinned pnpm 10pnpm install --frozen-lockfileandpnpm buildpass forpackages/tholos-sdkwith CI's pinned pnpm 10tholos.freelance-escrow.jobs.v1, reloaded, and confirmed the job and milestone were restored