Fix Electric create sync txids
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 returnspg_current_xact_id()::xid::text, parsed and validated as a safe integer.- Added a shared
ELECTRIC_WRITE_SYNC_TIMEOUT_MS = 30_000constant for all write confirmation waits. - Collection mutation handlers return a txid match only when numeric, using the shared timeout.
v2Workspaces.onUpdatenow 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 callinggetCurrentTxid(), 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, andchat.createSessionwrap writes in transactions and return txids.useWorkspaceCreates.submit()flow:- Optimistic
v2Workspaces.insert() - Await TanStack
tx.isPersisted.promise - Wait for canonical workspace id to exist locally with
$synced === true - Write local sidebar/pane state
- Return the canonical id
- Optimistic
- 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
OpenInWorkspaceV2no longer navigate optimistically; they awaitsubmit()and open the confirmed id. V2WorkspaceLayoutrefuses to mount host-backed workspace UI for unsynced optimistic rows ($syncedcheck).- Sidebar workspace data and
/v2-workspaceslist 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.deleteanddeleteMainForHostnow 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
creationStatusplumbing, pending/failed in-flight row injection V2WorkspaceTitlein-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
xidform: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.deletepath is not converted into a renderer-observed Electric wait. The desktop delete dialog path now usesworkspaceCleanup.destroywith 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.