feat(service): the Mac's held read is woken by Neon Realtime rather than its poll

Draft
CH
10d ago
+117-11

Stacked on #3266 (luke/neon-realtime-service-wake). Released desktops keep holding GET /api/mac-commands until the version gate cuts them off, so this PR makes that held read wait on Realtime too. Desktops on #3285 don't use this route.

Change

  • macCommandsFor now waits through lookedUntil from #3266, in place of Effect.repeat(Schedule.spaced(POLL)) + timeoutOption(HOLD). Between looks it waits for Neon Realtime to report that the account's unclaimed commands changed. The heartbeat on arrival, the 20 s hold, and the claim at the end are unchanged.
  • New macCommandsWaitingLiveStatement(userId) in mac-command-store.ts: SELECT id FROM mac_command WHERE user_id = $1 AND claimed_at IS NULL. It selects ids only, never the command text, and has no clock window. The read's own look still checks CLAIM_WINDOW. It mirrors #3274's macCommandsLiveStatement, which the Mac subscribes to and which also carries what to run. It's kept separate so that the conflict with #3274 stays to adjacent additions.

Tests

  • mac-command-store.test.ts (PGlite): the wake statement returns the account's unclaimed commands by id only, drops one once it's claimed, and reads nothing of another account's.
  • mac-commands-app.test.ts: with a scripted LiveWake, a held read answers a command queued while it's held as soon as the watch says the rows changed, with no TestClock advance. It also watches only the account's own statement. Without the change the test times out, because the read waits out its poll.

pnpm check:affected passes except apps/desktop/src/main/app-menu.test.ts, which is known to fail in the sandbox.

Made with Luke


Summary by cubic

Makes the Mac's held GET /api/mac-commands read wake on Neon Realtime instead of its 500 ms poll, so queued commands are answered as soon as they're queued.

  • macCommandsFor now waits on Realtime between looks through the new macCommandsWaitingLiveStatement, which selects the account's unclaimed command ids only. It falls back to the poll only where the deployment has no Realtime. The heartbeat, 20 s hold, and final claim are unchanged.