Skip to content

feat(mapgen-studio): migrate authoring/run state to Zustand persist stores, reads to TanStack Query, and fix runInGame contract selectedConfig.id optional#1614

Merged
mateicanavra merged 2 commits into
mainfrom
design/data-model
Jun 12, 2026
Merged

feat(mapgen-studio): migrate authoring/run state to Zustand persist stores, reads to TanStack Query, and fix runInGame contract selectedConfig.id optional#1614
mateicanavra merged 2 commits into
mainfrom
design/data-model

Conversation

@mateicanavra

Copy link
Copy Markdown
Owner

Migrates StudioShell's scattered state and data-fetching to a coherent architecture: persisted Zustand stores, oRPC-native TanStack Query reads, and a contract type-hole fix.

Persisted Zustand stores

Introduces authoringStore and runStore to replace the six authoring useStates and the run/save correlation useStates in StudioShell, along with the manual saveStudioAuthoringState persistence effect and the scattered localStorage.setItem calls. Both stores delegate to the existing serializers and localStorage keys verbatim — STUDIO_AUTHORING_STATE_KEY with its schemaVersion:1/savedAt envelope for authoring, and the four RUN_IN_GAME_LAST_*/MAP_CONFIG_SAVE_LAST_REQUEST_KEY keys for run state — so the on-disk schema is byte-identical before and after.

oRPC-native TanStack Query reads

Replaces the hand-rolled load/retry/focus-refetch effect for saved setup configs and the setup catalog with useSetupDataQueries, which uses orpc.civ7.savedConfigs.queryOptions() and orpc.civ7.setupCatalog.queryOptions() into useQuery. The derived view shapes consumed by setupControlOptions are unchanged; retry-on-failure and refetch-on-window-focus are provided by the shared query client defaults.

Replaces the self-rescheduling setTimeout status-poll effects for run-in-game and save-deploy with useOperationStatusPolls, which uses refetchInterval-driven useQuerys keyed on the active request id (sourced from runStore). The adaptive document.hidden ? 3000 : 1000 ms cadence, terminal-stop predicate, and 404 → synthetic uncertain/operation-status-missing mapping are all preserved. The terminal run-in-game toast is kept as a derived-state effect inside the same hook. The live-runtime status/snapshot poll remains imperative (its request-key staleness gate and adaptive backoff are not migrated).

Contract fix

Changes selectedConfig.id in the runInGame.start contract from z.string() to z.string().optional(), aligning it with parseRunInGameSetupRequest, which already reads selected.id defensively. This removes the as unknown as Parameters<typeof orpcClient.runInGame.start>[0] cast in features/runInGame/api.ts and restores full input type checking on the assertNoRawControlFields-protected start path.

mateicanavra commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions

Copy link
Copy Markdown

Railway preview (MapGen Studio): not provisioned for this PR.

Policy (Graphite stacks): previews are created only for the top-of-stack PR by default.

  • To force a preview for this PR: add label railway-preview
  • To suppress a preview: add label no-railway-preview

Debug: {"isClosed":false,"isFork":false,"suppress":false,"force":false,"isTopOfStack":false,"hasToken":true,"draft":false}

This was referenced Jun 12, 2026

mateicanavra commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

Merge activity

  • Jun 12, 8:50 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jun 12, 9:11 PM UTC: Graphite rebased this pull request as part of a merge.
  • Jun 12, 9:11 PM UTC: @mateicanavra merged this pull request with Graphite.

@mateicanavra
mateicanavra changed the base branch from design/craft-a11y to graphite-base/1614 June 12, 2026 21:08
@mateicanavra
mateicanavra changed the base branch from graphite-base/1614 to main June 12, 2026 21:09
mateicanavra and others added 2 commits June 12, 2026 21:10
Close the highest-stakes type hole on the assertNoRawControlFields-protected
run-in-game start path: the contract declared selectedConfig.id as required
(z.string()) while parseRunInGameSetupRequest + the engine read it defensively
(optional, defaulting to "studio-current" for disposable runs). Make id
optional to match the validator, then drop the
`as unknown as Parameters<typeof orpcClient.runInGame.start>[0]` cast in
features/runInGame/api.ts so the assembled request is fully input-typed end to
end. No behavior change: omitting selectedConfig when absent is wire-identical
to the prior explicit-undefined (server reads `selectedConfig ?? {}`).

Also lands the OpenSpec change scaffold (proposal/tasks/design/spec) for the
data-model slice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…thoring/run stores

Complete the data model deferred from the client-data slice (architecture/10 §2–§3):

Reads → oRPC-native TanStack Query:
- useSetupDataQueries: savedConfigs + setupCatalog now load via
  orpc.civ7.<proc>.queryOptions() into useQuery; retry + refetch-on-focus come from
  the query client defaults (replacing the hand-rolled load/retry/focus effect). The
  derived { status, directory, configurations, updatedAt, error } / { status, catalog,
  updatedAt, error } view shapes are unchanged.
- useOperationStatusPolls: run-in-game + save-deploy status polling moves from
  self-rescheduling setTimeout effects to useQuery refetchInterval polls keyed on the
  active runStore request id. Cadence (document.hidden ? 3000 : 1000), terminal/non-
  running stop, and the 404 → synthetic uncertain / operation-status-missing mapping
  are preserved verbatim (the queryFn invokes the existing refresh* callbacks).
  initialData keeps the seeded operation fresh for one interval so there is no extra
  immediate mount fetch. The live status/snapshot poll + its inlined setupConfig read
  stay imperative (hard core).

Persisted Zustand stores (localStorage schema is hard-core parity — copied, not fixed):
- authoringStore (persist): owns worldSettings/recipeSettings/setupConfig/
  pipelineConfig/overridesDisabled/repoBackedPresetOverridesByRecipe. Its persist
  storage adapter delegates to the reference impl (loadStudioAuthoringState /
  saveStudioAuthoringState, STUDIO_AUTHORING_STATE_KEY), so the on-disk schema is
  byte-identical; the manual save effect is removed.
- runStore (persist): owns the run/save correlation bridge. Its fan-out storage adapter
  maps the persisted slice onto the SAME four legacy keys (RUN_IN_GAME_LAST_REQUEST/
  SNAPSHOT/SOURCE + MAP_CONFIG_SAVE_LAST_REQUEST) with the SAME serializers; lastRun
  Snapshot/lastSaveDeployConfig stay session-only (not persisted), as before.

StudioShell drops the corresponding useState + manual persistence/localStorage effects
for store selectors. No query results are mirrored into Zustand.

Verified: tsc clean (app + studio-server), build + worker-bundle check, 138/138 tests
(incl. AppFooter). Runtime: zero /api calls; savedConfigs/setupCatalog + run/save status
poll over /rpc; status polls fire once then stop on 404 (no retry storm); live poll
verbatim; localStorage round-trips the same authoring + four run/save keys byte-for-byte;
zero console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mateicanavra
mateicanavra merged commit c457d60 into main Jun 12, 2026
This was referenced Jun 12, 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