feat: resolve #1020 — Persist minimal simulator state and reconstruct template indexes on startup (#1858)
* feat: persist minimal simulator state and reconstruct template indexes on startup
- Add persistState boolean to ConfigurationData (default: true)
- Add SimulatorState to FileType enum
- Add simulatorState to AsyncLockType enum
- Implement BootstrapStateUtils with:
- readStateFile: reads and validates state.json with schema version check
- writeStateFile: atomic write via tmp+rename with AsyncLock
- reconstructTemplateIndexes: scans per-station config files to rebuild indexes
- deleteStateFile: safe file deletion
- Integrate into Bootstrap:
- reconstructTemplateIndexes called in start() before worker spawn
- State file written on start() (started:true) and stop() (started:false)
- shouldAutoStart() reads state file to control auto-start behavior
- persistStateEnabled getter checks persistState config and SIMULATOR_COLD_START env
- Update start.ts to conditionally start based on shouldAutoStart()
- Handle edge cases: corrupt files, missing fields, incompatible schema version
- Add comprehensive tests for all state persistence and reconstruction scenarios
Closes #1020
* refactor(bootstrap): harmonize persisted state feature with codebase conventions
Address review findings on PR #1858:
- HIGH #1: Phase split start() lifecycle. Add public Bootstrap.startUIServer()
that always brings up the UI server (and reconstructs template indexes
before accepting requests). start.ts unconditionally calls startUIServer()
and only gates Bootstrap.start() on shouldAutoStart(), so a persisted
stopped state no longer turns the simulator into a zombie process.
- HIGH #2: Add canonical default. New defaultPersistState constant and
Configuration.getPersistState() accessor matching the existing
defaultUIServerConfiguration / defaultWorkerConfiguration pattern.
Bootstrap.persistStateEnabled now delegates instead of inlining ?? true.
- MEDIUM #3: Document persistState tunable and SIMULATOR_COLD_START
environment override in README.md.
- MEDIUM #4: writeStateFile and deleteStateFile now swallow filesystem
errors via handleFileException with throwError:false, mirroring
watchJsonFile and the storage layer. Persistence failures no longer
surface as misleading 'Startup error' / 'Shutdown error'.
- MEDIUM #5: reconstructTemplateIndexes runs inside startUIServer() before
uiServer.start(), closing the race where UI requests could arrive before
index reconstruction completes.
- LOW #7: Add formatLogPrefix() utility and use it in BootstrapStateUtils,
removing the leading-space artifact when logPrefixFn is undefined.
- LOW #8: On state file schema mismatch, quarantine to <path>.v<N>.bak
instead of silently deleting, allowing forensics during partial schema
rollouts. JSON parse errors still delete (true corruption, unrecoverable).
- LOW #9: Move state.json from dist/assets/configurations/ to dist/assets/.
Drops the fragile basename-based filter from reconstructTemplateIndexes
and separates control file from per-station configuration files.
Drop shouldAutoStart() from IBootstrap interface (boot-time concern, no UI
service consumer needs it). The method stays public on the concrete
Bootstrap class for start.ts.
Tests: align fixtures with new state.json location, add quarantine assertion,
add non-fatal writeStateFile assertion, drop obsolete state.json filter test.
* test(bootstrap): align BootstrapStateUtils tests with project style guide
- Rename BootstrapState.test.ts to BootstrapStateUtils.test.ts to match the
source module name (TEST_STYLE_GUIDE.md §1: 'Files: ModuleName.test.ts').
- Add mandatory standardCleanup() in afterEach (TEST_STYLE_GUIDE.md §3),
matching the convention used by Configuration.test.ts, FileUtils.test.ts,
ErrorUtils.test.ts and ConfigurationKeyUtils.test.ts.
* fix(bootstrap): address review findings on persisted simulator state
Distinguish UI-initiated stop from signal/restart via a private
StopReason enum, so SIGINT/SIGTERM/SIGQUIT and config-reload restarts
no longer flip the persisted state to stopped (HIGH-1).
Short-circuit persistStateEnabled when the UI server is disabled, since
persistence has no recovery channel without a UI; warn once on the
inconsistent configuration (HIGH-2).
Reconstruct template indexes via a shared prepareTemplateStatistics
helper called from constructor and restart, not only from startUIServer,
to prevent index collisions on config-reload of UI-added stations
(MED-3).
Move state file from dist/assets/state.json (wiped by pnpm build) to
dist/assets/configurations/.simulator-state.json; filter dot-prefixed
metadata files in reconstructTemplateIndexes so the state file co-located
with charging station configurations is silently skipped (MIN-10).
Clean up the .tmp file on atomic-rename failure in writeStateFile and
guard readStateFile against JSON null/primitive/array content with an
explicit message (MIN-7, MIN-8).
Unify path computations into readonly assetsDir/configurationsDir/
stateFilePath fields, deduplicate setChargingStationTemplates via a
syncUIServerTemplates helper, and route the SIMULATOR_COLD_START env
var name through Constants.ENV_SIMULATOR_COLD_START (MIN-5, MIN-6).
Export DEFAULT_PERSIST_STATE and add the persistState entry to
config-template.json so the tunable is discoverable (MED-4).
Update README to document the new state file path, the UI server
requirement, and the signal-shutdown semantics.
Test coverage added for tmp cleanup, JSON null/primitive/array guards,
and dot-prefixed metadata filtering; the misleading 'read-only
directory' test is renamed to reflect the actual OS-rejected-path
scenario (MIN-9).
* docs(bootstrap): clarify persisted state semantics from review feedback
- README: drop developer-internals sentence on template index reconstruction
from the persistState row
- TemplateStatistics: document the `added` (process-scoped) vs `indexes`
(process+disk-scoped) distinction exposed via SimulatorState
- IBootstrap: document the contract scope (UI-server-facing, excluding
process-lifecycle helpers and the internal StopReason)
- formatLogPrefix: document the trailing-space contract
- reconstructTemplateIndexes: refine the warn message wording for non-station
configuration files (level unchanged)
* refactor(utils): default formatLogPrefix prefix to logPrefix
Avoids silent loss of the timestamp when callers do not pass a module-specific
prefix function. Aligns with the existing convention where every module-level
logPrefix delegates to Utils.logPrefix.