save-migrations

Client-side save schema versioning. Bumps CURRENT_SAVE_VERSION whenever the shape of BootstrapPayload['save'] changes in a way old payloads can’t satisfy, and runs registered per-version migration functions to bring stale saves up to the current shape before any store hydration.

Styx

  • Entry point: runSaveMigrations(save) invoked from playerBootstrap immediately after the bootstrap_player RPC returns, before any store hydration (see src/metagame/services/playerBootstrap.ts:104).
  • Input: SaveBlob from ./save-schema — fields selected_ship_id, fleet_json (deprecated, ignored), settings_json, save_version, updated_at.
  • Loop: while current.save_version < CURRENT_SAVE_VERSION, look up MIGRATIONS[current.save_version] and apply it; the migration must return a SaveBlob with save_version advanced.
  • Missing migration: a missing entry for a version below CURRENT_SAVE_VERSION throws Error("[save-migrations] No migration registered for save_version <n> (CURRENT=<m>)"). No silent fallback.
  • Output: SaveBlob whose save_version === CURRENT_SAVE_VERSION.

Constants

NameValueNotes
CURRENT_SAVE_VERSION1Bump when SaveBlob shape changes incompatibly.

Types

  • Migration = (save: SaveBlob) => SaveBlob — pure function from one version’s shape to the next.
  • MIGRATIONS: Record<number, Migration> — sparse map keyed by source version. Currently empty (commented stub at save-migrations.ts:19 shows the canonical form: 1: (save) => ({ ...save, newField: defaultValue, save_version: 2 })).

Authoring a new migration

  1. Update SaveBlob in ./save-schema to the new shape.
  2. Increment CURRENT_SAVE_VERSION to N+1.
  3. Register MIGRATIONS[N] = (save) => ({ ...save, /* new/changed fields */, save_version: N + 1 }).
  4. The migration must set save_version on its return value so the loop terminates.

EXTRACT-CANDIDATE

  • None. File is 34 lines, single responsibility, single call site. The MIGRATIONS map is the natural extension point; no abstraction to extract.