Skip to content

Fix: serverStore cross-store mutation — replace syncProjectRunning with load() #36

@jpeggdev

Description

@jpeggdev

Problem

When the server starts/stops, serverStore calls syncProjectRunning() which directly mutates projectStore.projects and projectStore.activeProject — reaching across store boundaries. This creates two sources of truth for is_running:

  • serverStore.isRunning (current project, set optimistically)
  • project.is_running (per project in projectStore, set by manual patch)

The syncProjectRunning function (serverStore.ts:6-17) hand-rolls a map over store.projects that must match projectStore's internal shape — a fragile duplication. If activeProject is not null, it also patches that separately. The backend is the actual canonical source (serverCommands.isRunning), but neither store consistently defers to it.

Race conditions are possible: projectStore.load() could overwrite the manual patch, or the server-status-changed event could fire setStatus() without calling syncProjectRunning.

Proposed Interface

Zero caller changes. Replace syncProjectRunning() with projectStore.load():

// serverStore.ts — start action (stop is symmetrical)
start: async (projectId) => {
  if (get().starting || get().isRunning) return
  set({ starting: true })
  try {
    const result = await serverCommands.start(projectId)
    set({ isRunning: true, port: result.port, https: result.https, starting: false })
    projectCommands.updateRunningStatus(projectId, true).catch(() => {})
    useProjectStore.getState().load()   // <-- replaces syncProjectRunning()
  } catch (e) {
    console.error('Failed to start server:', e)
    set({ starting: false })
  }
},

syncProjectRunning is deleted entirely. projectStore.load() fetches the authoritative project list from the backend, which already includes the correct is_running state.

Dependency Strategy

  • Category: In-process + remote-owned (Tauri backend is canonical)
  • serverStore already imports projectStore — no new dependency
  • Direction: serverStore calls projectStore.load() (a public method), never setState directly
  • projectStore owns its own state; serverStore triggers a refresh, not a mutation
  • The Tauri event path (server-status-changed -> setStatus) continues to work independently

Testing Strategy

  • New boundary tests: Call serverStore.start(), verify projectStore.load() was called (or that projects reflect the updated state after an async tick)
  • Old tests to delete: Any test that asserts syncProjectRunning patches specific fields
  • Test environment: Standard Vitest with mocked IPC commands

Implementation Recommendations

  • serverStore should own isRunning, port, https for the current project
  • projectStore should own project.is_running as a backend-derived field refreshed via load()
  • No store should directly setState on another store — call public methods only
  • The brief hydration lag (~1-5ms local SQLite read) between serverStore's optimistic update and projectStore's authoritative refresh is imperceptible
  • If the sidebar dot lag is ever noticeable, add projectStore.setRunning(id, running) as a projectStore-owned method rather than reverting to cross-store mutation

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions