Engineering learnings
TL;DR — State, testing, and resource-cleanup failures that keep recurring, and the fixes that stuck. Written so future-me stops re-learning them. (Demo content.)
State & lifecycle
- Separate sign-out from data ownership. In an offline-first app such as PixelPaws, retain unsynced progress only under its owner's identity. Do not upload it as the next account. Private fetched content needs its own sign-out clearing policy; keeping all local data is not universal.
- Cancel before you start. A render loop started without cancelling the
previous
requestAnimationFrameruns twice as fast on replay. Cancel at the top ofstart(). - Guard double-submit synchronously. Set the "busy" flag before any
await, not after — otherwise a fast second click slips through. - Invalidate old account work. Capture an account generation before loading, restoring, or saving; discard late cache and UI commits after identity changes.
- Return from auth callbacks before re-entering the SDK. Some SDKs hold an internal lock while callbacks run. Defer SDK-dependent work and test the full initialization path against that contract.
Offline sync
- Acknowledge the uploaded revision, not just its score. A streak or another progress field can change while the best remains equal. Keep its retry when the current blob differs from the submitted snapshot; never clear unrelated retries after a batch upload. This does not solve server-side write ordering.
- Resolved errors are still failures. An SDK result containing an error must not clear retries or turn null leaderboard counts into first place.
- Allow recovery after failed initialization. Memoizing a rejected or failed initialization forever makes reconnect ineffective. Test failure then success.
Caching & PWAs
- Bump the asset version when you edit a non-hashed file, or the service worker keeps serving the old one.
- Network-first for documents, cache-first for hashed assets. Mixing them up is why "my change didn't deploy" is usually a cache, not a build.
- Preflight releases before deleting assets. Check every target's fresh build and asset directory before changing any served files. A redirect stub left by a previous promotion is not a fresh build. Include hidden apps in build coverage.
Testing
- Test pure logic and its integration boundaries. Models establish rules; DOM and SDK tests establish event ordering, lifecycle, and error handling; browser checks establish rendered behavior. See learnings/README.md.
- Make the test's starting state discriminating. A random board can make a swipe legitimately do nothing. Fix the setup to require a move, not the assertion.
- Count parsed elements, not commented source. Use actual card elements for featured counts and compare structured metadata to them. Preserve duplicates until uniqueness is checked.
- Match the reporting calendar. Use the backend's timezone for date keys and labels; test midnight boundaries and late responses after filter or auth changes.
- State what each gate proves. Audit readers must cover every supported product and report unreadable rows. Check deployed artifact bytes as well as workflow status; a visible sign-in button does not prove an OAuth flow.
Resource cleanup
- Ownership before deletion. Match installed app identities, running helpers, open files, and storage paths. An app missing from one directory is not proof that its remaining files are disposable. Different release channels and products from one vendor need separate checks; preserve shared helpers.
- A cache label is not a data contract. Inspect the specific children of app storage. Profiles, offline databases, recordings, updater executables, and even tracked source can live under familiar cache or build-directory names.
- Retire the owner, not a restart loop. Repeated worker respawns call for an authorized supervisor/extension change. Registration, installed package files, and running workers are separate states; verify all three afterward. See runtime resource lifecycle.
- Preserve evidence when retiring worktrees. Compare revisions, inspect ignored and untracked files as well as tracked changes, preserve unique QA output, and use normal Git worktree removal. Folder removal is not branch deletion.
- Measure like with like. Use the same volume and units before/after each batch. Summed RSS is not guaranteed freed RAM; directory sizes and a later UI availability reading are not proof of physically reclaimed disk space.
- Report the boundary. Permission-denied locations remain unverified; do not bypass OS protections or delete recovery snapshots to complete a checklist. Separate approval is needed for irreplaceable data. A local uninstall does not revoke online account access. See regenerable cache recovery.
Guidance updated: 2026-09-14. Examples remain fictional; no private project evidence is stored here.