Skip to content

fix(api): rebuild every in-memory index on hot reload - #159

Merged
themightychris merged 4 commits into
developfrom
fix/hot-reload-stale-indices
Sep 9, 2026
Merged

fix(api): rebuild every in-memory index on hot reload#159
themightychris merged 4 commits into
developfrom
fix/hot-reload-stale-indices

Conversation

@themightychris

Copy link
Copy Markdown
Member

Summary

swapInPlace (apps/api/src/store/memory/reload.ts) named each Map on InMemoryState by hand and skipped three secondary indices: projectIdByLegacyId, buzzIdBySlug, and slugHistory. After POST /api/_internal/reload-data those three still described the pre-reload state.

Because the laddr importer mints fresh UUIDv7 ids on every run, a re-import merged into published followed by a hot reload left projectIdByLegacyId pointing at project ids that no longer existed. Legacy /projects?ID=<n> and /project-updates?ProjectID=<n> redirects fell through to the SPA (404) until the pod restarted; /project-buzz/<slug> and slug-history 301s went stale the same way.

Changes

  • Spec (specs/behaviors/storage.md → Hot reload → Atomicity): states that every collection on the live state, primary maps and every secondary index including legacy-id, buzz-by-slug, and slug-history, is replaced from the fresh state.
  • Fix: swapInPlace enumerates the fresh state's own properties instead of maintaining a list, and throws if a property is ever not a Map so a future field gets handled deliberately instead of skipped.
  • Unit guard (apps/api/tests/reload-swap.test.ts): builds two states with one record of every entity type (different ids, same legacy ids), swaps, and asserts every own property was replaced while Map identities are preserved. Fails on the old code for all three missing indices.
  • Integration guard (apps/api/tests/internal-reload.test.ts): re-import scenario through the real webhook. Project and buzz are replaced with fresh ids and slugs plus a slug-history record; the legacy-id, buzz-slug, and old-slug redirects must all land on the new slug afterwards. Fails on the old code.
  • Plan: plans/hot-reload-stale-indices.md.

Validation

  • npm run type-check clean
  • npm run lint clean
  • npm test: api 34 files / 428 tests, web 23 / 89, shared 3 / 75 — all green

Note

While writing the integration test I found that boot-time reconcile does not re-open the gitsheets store snapshot the way the webhook path does, so if the local clone is behind at boot the in-memory state is built from the pre-fast-forward tree. Production pods bare-clone fresh on every boot so this doesn't bite there; filed as a follow-up issue, out of scope here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr

themightychris added a commit that referenced this pull request Sep 8, 2026
themightychris and others added 4 commits September 8, 2026 20:12
The hot-reload section described the rebuild as "mutate the live Maps
in place" without saying which Maps. The implementation had quietly
skipped three secondary indices (legacy-id, buzz-by-slug, slug-history),
which is exactly the gap an unqualified sentence leaves open. State the
invariant explicitly: every collection on the live state, primary and
secondary, is replaced from the fresh one, so no lookup path can serve
pre-reload contents after a reload.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
swapInPlace named each Map on InMemoryState by hand and skipped three
secondary indices: projectIdByLegacyId, buzzIdBySlug, and slugHistory.
After POST /api/_internal/reload-data those three still described the
pre-reload state. Because the laddr importer mints fresh UUIDv7 ids on
every run, a re-import merged into `published` followed by a hot reload
left projectIdByLegacyId pointing at project ids that no longer existed,
so legacy /projects?ID=<n> redirects fell through to the SPA until the
pod restarted; /project-buzz/<slug> and slug-history 301s went stale the
same way.

Enumerate the fresh state's own properties instead of maintaining a
list, and throw if a property is ever not a Map so a future field is
handled deliberately rather than skipped again.

Guards against recurrence:

- reload-swap.test.ts builds two states with one record of every entity
  type (different ids, same legacy ids), swaps, and asserts every own
  property of the fresh state was replaced while Map identities are kept.
  Fails on the old code for all three missing indices.
- internal-reload.test.ts gains a re-import scenario through the real
  webhook: the project and buzz are replaced with fresh ids and slugs
  plus a slug-history record, and the legacy-id, buzz-slug, and old-slug
  redirects must all land on the new slug afterwards.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
@themightychris
themightychris force-pushed the fix/hot-reload-stale-indices branch from 825c2e1 to d7c5f9f Compare September 9, 2026 00:12
@themightychris
themightychris merged commit 8357170 into develop Sep 9, 2026
1 check failed
@themightychris
themightychris deleted the fix/hot-reload-stale-indices branch September 9, 2026 00:12
@themightychris themightychris mentioned this pull request Sep 9, 2026
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