Fix Electric create sync txids

Closed
KY
4mo ago
+750-1116

Summary

Hardens workspace/project/chat writes against Electric sync races by returning correct txids from backend mutations, awaiting them in renderer collection handlers, and removing the parallel in-memory workspace-create sidecar that modeled in-flight cloud rows outside TanStack DB.

User-visible impact: Workspace create, rename, delete, import/adopt, and secondary navigation surfaces no longer race ahead of Electric confirmation, and optimistic UI is gated on sync/delete state.

Approach

Electric txid correctness

  • getCurrentTxid() now returns pg_current_xact_id()::xid::text, parsed and validated as a safe integer.
  • Added a shared ELECTRIC_WRITE_SYNC_TIMEOUT_MS = 30_000 constant for all write confirmation waits.
  • Collection mutation handlers return a txid match only when numeric, using the shared timeout.
  • v2Workspaces.onUpdate now uses the same txid matching helper (previously returned { txid: result.txid } directly).
  • No-op mutations return no txid instead of inventing a confirmation token.
  • Backend update/delete paths now use .returning() or equivalent affected-row checks before calling getCurrentTxid(), so a raced no-op DML cannot fabricate a txid Electric will never emit.

Note on xid casting: We discovered getCurrentTxid() was using pg_current_xact_id()::text without the ::xid:: cast. Electric receives 32-bit xid values through logical replication, so the correct form is pg_current_xact_id()::xid::text. Without the cast, the values happen to match on fresh databases (epoch = 0), but would diverge after ~4.29B txids when the epoch increments. Whether this contributed to reported timeouts is unconfirmed (depends on transaction volume), but it's a correctness fix regardless.

Workspace create/import flow

  • v2Workspace.create, v2Project.create, and chat.createSession wrap writes in transactions and return txids.
  • useWorkspaceCreates.submit() flow:
    1. Optimistic v2Workspaces.insert()
    2. Await TanStack tx.isPersisted.promise
    3. Wait for canonical workspace id to exist locally with $synced === true
    4. Write local sidebar/pane state
    5. Return the canonical id
  • Existing unsynced optimistic rows no longer short-circuit as successful creates.
  • Canonical-id mismatches still rollback the optimistic id, but only after the canonical row is confirmed.
  • V1 workspace import/adopt now waits for the adopted v2 workspace row to be synced before writing sidebar state.

Navigation and UI guards

  • New workspace modal and task sidebar OpenInWorkspaceV2 no longer navigate optimistically; they await submit() and open the confirmed id.
  • V2WorkspaceLayout refuses to mount host-backed workspace UI for unsynced optimistic rows ($synced check).
  • Sidebar workspace data and /v2-workspaces list filter out unsynced rows and rows being deleted, preventing stale actions and hotkey targets.
  • Command palette workspace context is only populated for synced, non-deleting workspaces.
  • Resource monitor and sidebar ports now filter workspace metadata through the same synced/non-deleting boundary before exposing navigation.

Delete/update coverage

  • v2Workspace.delete and deleteMainForHost now return txids for real deletes and return no txid for already-gone raced deletes.
  • Renderer waits for the cloud delete txid and for the row to disappear from the local Electric collection before considering delete complete.
  • If Electric confirmation is slow, the renderer hides the entry and keeps it in deleting state rather than re-enabling stale actions.
  • Auxiliary mutations (setTask, updateNameFromHost) return txids for real writes.
  • Host membership rename/remove/role update paths no longer return txids unless the underlying replicated row actually changed.
  • Chat delete no-op no longer returns a txid.

Removed workspace-create sidecar

Removed the renderer-memory workspace-create model and related fallback UI:

  • WorkspaceCreatingState/, WorkspaceCreateErrorState/, workspace-creates/Manager.tsx, workspace-creates/store.ts
  • Sidebar creationStatus plumbing, pending/failed in-flight row injection
  • V2WorkspaceTitle in-flight fallback

The remaining create path uses TanStack DB's optimistic entry plus Electric confirmation only.

Key Invariants

  • getCurrentTxid() must be called inside the same SQL transaction as a real replicated write.
  • The returned txid must match Electric's logical-replication xid form: pg_current_xact_id()::xid::text.
  • Renderer navigation into host-backed workspace UI only happens after the cloud row is present locally with $synced === true.
  • Sidebar, /v2-workspaces, command palette, resource monitor, and port actions only see synced rows not currently being deleted.
  • No collection handler returns { txid: null } or { txid: undefined } to TanStack's Electric adapter.

Non-goals

  • Didn't address unrelated pre-existing crypto/Buffer type errors in packages/trpc.
  • No changes to Electric's logical replication protocol itself; only how we match against it.
  • The legacy host-service/CLI workspace.delete path is not converted into a renderer-observed Electric wait. The desktop delete dialog path now uses workspaceCleanup.destroy with Electric confirmation.

Verification

bun --cwd apps/desktop typecheck
bun --cwd packages/host-service typecheck
bun --cwd packages/db typecheck
bun --cwd apps/electric-proxy typecheck
bun test packages/trpc/src/router/v2-project/v2-project.test.ts
bun test packages/trpc/src/router/task/task.test.ts
bun run lint:fix
bun run lint

Latest reviewer-driven follow-up validation also passed:

bun --cwd apps/desktop typecheck
bun --cwd packages/host-service typecheck
bun run lint:fix
bun run lint

Full bun --cwd packages/trpc typecheck has unrelated pre-existing crypto/Buffer type errors.