[Bug] Progress can be lost in "In-Browser Compile" mode after completing a lesson then hard-refreshing #223

Closed
opened 2026-06-14 01:00:44 +00:00 by spikerj · 1 comment
Owner

Classification

Type: Bug
Priority: High
Component: Playgrounds / Progress sync (WASM "In-Browser Compile" path)

Summary

Students using In-Browser Compile mode report losing progress: they complete a lesson (graded locally, shown green / next lesson unlocks), then take an action that should be harmless — most commonly an immediate hard refresh — and the lesson re-locks as if it was never completed.

Root cause

A locally-graded pass is written to the offline outbox (pendingProgress in IndexedDB) and then flushed to POST /Lessons/progress/batch. The flush deletes the outbox row as soon as the server accepts the batch — even when the server only accepts it as unverified and hasn't yet surfaced it through GET /progress (the server has an async unverified → verified lifecycle).

On reload, LanguageRunner seeds completed lessons from server progress ∪ outbox-pending. In the window between "flush accepted" and "server reports the pass", both sources miss it:

  • the outbox row was already deleted on accept, and
  • the server doesn't yet return it from GET /progress.

A hard refresh during that window re-locks the lesson → perceived progress loss.

Expected behavior

Once a lesson is completed locally, completion must never regress on a mere refresh on the same device. The server stays authoritative for cross-device progress, but local completion should be a durable floor.

Fix

Add a durable local completion ledger (a new IndexedDB store, completedLessons) that is:

  • written at pass time, independent of the network flush lifecycle;
  • never deleted when the outbox row is flushed;
  • user- + language-scoped (mirrors the #221 scoping so a shared browser never shows another student's passes);
  • unioned into the reload seeding (getPendingPassedLessonNumbers) alongside the outbox and server progress.

This closes the flush/verify race so a completed lesson survives an immediate hard refresh.

Notes / trade-offs

  • The ledger is local and additive (never subtracts), so a server-side admin reset won't be reflected on a device that has the local floor. Acceptable for the reported UX; can be reconciled later if needed.
  • Follow-up to #221 (session/progress durability).
### Classification **Type:** Bug **Priority:** High **Component:** Playgrounds / Progress sync (WASM "In-Browser Compile" path) ### Summary Students using **In-Browser Compile** mode report losing progress: they complete a lesson (graded locally, shown green / next lesson unlocks), then take an action that should be harmless — most commonly an **immediate hard refresh** — and the lesson re-locks as if it was never completed. ### Root cause A locally-graded pass is written to the offline outbox (`pendingProgress` in IndexedDB) and then flushed to `POST /Lessons/progress/batch`. The flush **deletes the outbox row as soon as the server *accepts* the batch — even when the server only accepts it as _unverified_** and hasn't yet surfaced it through `GET /progress` (the server has an async unverified → verified lifecycle). On reload, `LanguageRunner` seeds completed lessons from `server progress ∪ outbox-pending`. In the window between *"flush accepted"* and *"server reports the pass"*, **both** sources miss it: - the outbox row was already deleted on accept, and - the server doesn't yet return it from `GET /progress`. A hard refresh during that window re-locks the lesson → perceived progress loss. ### Expected behavior Once a lesson is completed locally, completion must **never regress on a mere refresh** on the same device. The server stays authoritative for cross-device progress, but local completion should be a durable floor. ### Fix Add a **durable local completion ledger** (a new IndexedDB store, `completedLessons`) that is: - written at pass time, independent of the network flush lifecycle; - **never deleted** when the outbox row is flushed; - user- + language-scoped (mirrors the #221 scoping so a shared browser never shows another student's passes); - unioned into the reload seeding (`getPendingPassedLessonNumbers`) alongside the outbox and server progress. This closes the flush/verify race so a completed lesson survives an immediate hard refresh. ### Notes / trade-offs - The ledger is local and additive (never subtracts), so a server-side admin reset won't be reflected on a device that has the local floor. Acceptable for the reported UX; can be reconciled later if needed. - Follow-up to #221 (session/progress durability).
Author
Owner

Resolved in spikersoft-angular PR #42 (merged to master). Added a durable completedLessons IndexedDB store (DB v2), written at pass time and never removed by the flush lifecycle, unioned into reload seeding — so a locally-graded pass survives an immediate hard refresh. Closing.

(Fix is in the spikersoft-angular repo; cross-repo #223 doesn't auto-close — closing manually.)

Resolved in spikersoft-angular PR #42 (merged to `master`). Added a durable `completedLessons` IndexedDB store (DB v2), written at pass time and never removed by the flush lifecycle, unioned into reload seeding — so a locally-graded pass survives an immediate hard refresh. Closing. (Fix is in the `spikersoft-angular` repo; cross-repo `#223` doesn't auto-close — closing manually.)
Sign in to join this conversation.