Skip to content

Commit f0b086f

Browse files
authored
Recover live requests when fetch hangs (#4674)
## Summary - Add a `ShapeStream` request watchdog (`liveRequestTimeoutMs`, default `45_000`) so wedged mobile fetch promises cannot permanently stop the read loop. - Apply the watchdog to both live long-poll requests and refresh catch-up requests, after Hudson’s PR-build test showed non-live catch-up requests can also hang after backgrounding. - Make watchdog restarts independent of platform abort behavior by marking restart intent on the request controller, aborting with `live-request-timeout`, and racing the fetch promise with a local rejection. - Fix wake recovery to keep `isRefreshing` true until the next request-loop tick completes, ensuring foreground/system-wake recovery performs non-live catch-up requests even when URL construction yields. - Make retry backoff abort-aware at sleep entry and document the request-loop invariants in `SPEC.md`. ## Context This follows the React Native/mobile issue where `awaitTxId` can time out permanently after backgrounding. Hudson reproduced a read-loop variant directly: - app ran normally for ~21 minutes, with an earlier 2 minute background/foreground cycle working fine - app then backgrounded for ~6.5 minutes while the backend inserted new Postgres rows - after foregrounding, those backend rows never appeared on the client - subsequent writes succeeded server-side and returned valid txids, but every tx echo timed out - the app did not recover until kill/restart That points at the client read loop getting wedged: once the request loop stops settling, the client observes neither backend-generated rows nor mutation tx echoes. Hudson then tested this PR build on a physical iPhone with Electric request logging. The abort/reconnect path worked: all shape streams aborted after backgrounding and sent non-live catch-up requests. However, polling stopped at those catch-up requests and no further Electric requests were logged. This update extends the watchdog to those refresh catch-up requests as well, so a hung catch-up fetch can no longer stop the loop permanently. ## Changes ### Request watchdog `ShapeStreamOptions` now includes: ```ts liveRequestTimeoutMs?: number | false ``` - defaults to `45_000` - applies to live long-poll requests (`live=true`) and refresh catch-up requests - accepts only positive finite numbers, or `false` to disable - aborts timed-out requests with `live-request-timeout` - rejects locally so the loop can continue even if the runtime fetch ignores abort or never settles ### Wake recovery Wake detection now mirrors `forceDisconnectAndRefresh()` by holding the refresh flag until the next request-loop tick completes. This prevents an async `#constructUrl()` yield from clearing `isRefreshing` before `canLongPoll` is computed, which could otherwise reconnect with another `live=true` long-poll instead of a catch-up request. ### Retry backoff `abortableSleep()` now throws `FetchBackoffAbortError` when entered with an already-aborted signal, instead of silently returning and allowing retry flow to continue as though the sleep completed normally. ## Tests ```sh cd packages/typescript-client pnpm run typecheck pnpm exec vitest --config vitest.unit.config.ts --run test/fetch.test.ts test/wake-detection.test.ts ``` Passed locally: - typecheck - `test/fetch.test.ts`: 39 tests - `test/wake-detection.test.ts`: 10 tests
1 parent b8be884 commit f0b086f

8 files changed

Lines changed: 1061 additions & 82 deletions

File tree

.changeset/rn-fetch-watchdog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@electric-sql/client": patch
3+
---
4+
5+
Add a request watchdog so ShapeStream can recover when mobile fetch implementations hang across app lifecycle or network transitions. Live long-poll requests and refresh catch-up requests now time out after `liveRequestTimeoutMs` (default 45s, or `false` to disable), abort with an internal `live-request-timeout` reason, and restart the request loop even if the platform fetch promise never settles.
6+
7+
Also make retry backoff abort-aware at sleep entry, keep wake-triggered refreshes in non-live catch-up mode until the next request-loop tick completes, and auto-detect React Native AppState when available so mobile apps pause Electric requests while backgrounded and resume catch-up after foregrounding. A runtime visibility adapter hook remains available for other non-browser runtimes.

packages/typescript-client/SPEC.md

Lines changed: 18 additions & 17 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)