Skip to content

feat!: reconnect IPC and recover subscriptions after a backend restart - #226

Open
gmaclennan wants to merge 32 commits into
fix/android-ipc-reconnectfrom
feat/rpc-transport-recovery
Open

feat!: reconnect IPC and recover subscriptions after a backend restart#226
gmaclennan wants to merge 32 commits into
fix/android-ipc-reconnectfrom
feat/rpc-transport-recovery

Conversation

@gmaclennan

@gmaclennan gmaclennan commented Aug 13, 2026

Copy link
Copy Markdown
Member

Makes an unexpected death of the :ComapeoCore foreground service recoverable end to end: the sockets reconnect on their own, in-flight RPC calls fail fast instead of hanging 30 s, event subscriptions are replayed, and the app is told — once, and only when it is true — that the backend lost its in-memory state.

This PR now carries the Kotlin reconnect layer from #225 unchanged (its five commits are patch-identical here, verified by range-diff; that PR is closed in favour of one review surface) plus the recovery layers above it, aligned with @comapeo/ipc v10 (digidem/comapeo-ipc#88).

Kotlin: reconnect on drop

NodeJSIPC gains reconnectOnDrop: exponential backoff (250 ms → 4 s, 120 s deadline), epoch-scoped single-flight teardown so a stale disconnect can never tear down its replacement connection, and imperative delivery of terminal transitions (the conflating StateFlow could drop a Disconnected that auto-reconnect CASes away microseconds later). The data socket also gains a transportStateChange event into JS — previously it had no state callback at all, which is why a backgrounded app never recovered.

JS: two-phase recovery

On disconnected/error, notifyTransportReset rejects every pending call on both clients with RPC_CHANNEL_CLOSED — no more 30 s hangs, and reads become distinguishable-retryable while mutations surface the error. On connected with the control channel STARTED, resubscribe replays every live subscription; under v10's stable per-project channels that transparently re-opens the projects the app is actually observing. Nothing resubscribes at drop time (ON frames written into a down transport feed the native reconnect a hot loop). There are no wrapper hard-closes and no generation guards — v10 made both unnecessary, and the earlier TransportClosedError is gone in favour of the shipped code.

Boot nonce: restarts are detected, reconnects are not

subscribeToBackendRestart(listener) fires only on a genuine backend restart. Detection is a per-process nonce in the control channel's ready frame ({type: "ready", bootNonce}) — necessary because the control server deliberately replays ready to late-connecting clients, so state transitions alone cannot distinguish an app-side reconnect (backend alive, caches warm, must not fire) from a real restart (must fire). The frame is backward compatible (a bare ready parses with a null nonce, which conservatively fires). A nonce change observed outside a recovery window also fires, as defense in depth against a reconnect racing the control socket's drop.

e2e

The v9 close-semantics specs are replaced by v10 contract specs: calls transparently survive a backend-side project close, and getProject hands back the same working reference afterwards. Backend-side closes are driven through a debug-only lifecycle channel (@@comapeo-debug/lifecycle), served per-connection only when COMAPEO_DEBUG_LIFECYCLE=1 and invisible to the production router. The suite probes the channel once per session: unserved costs a single 10 s timeout and the specs self-mark pending; once the probe succeeds, a hang is a failure, not a pending. Plumbing the env flag into the native start path is follow-up work, so these specs report pending on device today.

State of the branch

Root/backend lockfiles are intentionally untouched: manifests declare @comapeo/ipc ^10.0.0 (and the backend rpc-reflector ^4.5.0), both unpublished, so npm ci fails until digidem/rpc-reflector#55 and digidem/comapeo-ipc#88 release — local development links the checkouts with npm install <path> --no-save. Test state: root suite 89 passing, backend suite 106 passing, Kotlin JVM unit suite green, e2e typechecks; on-device e2e needs the published packages. docs/ARCHITECTURE.md §5.8 documents the recovery design, including one known pre-existing limitation: past the 120 s reconnect deadline both sockets go terminal, and the control socket only reconnects on the next foreground transition.

@github-actions github-actions Bot added the feature New feature (changelog) label Aug 13, 2026
@gmaclennan

Copy link
Copy Markdown
Member Author

Reworked in response to review (this PR + its stack base + the upstream PRs all got follow-ups):

  • Two-phase recovery: drop time now only rejects in-flight calls and hard-closes stale project clients (upstream notify*TransportReset no longer resubscribes); subscription replay and the restart signal wait until BOTH the backend is STARTED again and the message socket has reconnected, in whichever order those arrive. This fixes the hot retry loop the review found (drop-time ON frames nudging the socket out of its terminal Error state at cold-start cadence while the backend stays down) and the message-socket-only-drop case where the lifecycle state never leaves STARTED and nothing told the app layer to re-fetch.
  • Restart listener dispatch is exception-isolated.
  • TransportClosedError docs corrected: the response will never arrive — the call itself may or may not have executed; retry-safety wording unchanged.
  • §5.8 documents the recovery-window state flap and the surfaces recovery does not cover.
  • The conflation hazard (JS missing the drop event entirely) is fixed at its source in the stack base (fix(android): reconnect IPC after unexpected socket drop #225): terminal transitions are now delivered imperatively to the observer.

Jest 85/85, build + lint clean against the branch-built upstream APIs.

@gmaclennan
gmaclennan force-pushed the feat/rpc-transport-recovery branch from dcbf6de to 039359e Compare August 13, 2026 21:47
gmaclennan and others added 27 commits August 17, 2026 18:07
Moves the embedded runtime from nodejs-mobile v18.20.4 to the digidem
fork's v24.19.0-0. The fork versions releases `<node>-<mobile-rev>` and
renamed its assets, so the download script changes shape as well as
version.

Two things the upgrade forces rather than merely allows:

`--no-experimental-fetch` is gone from iOS argv — Node removed the flag
in 23, and passing an unknown flag aborts before any JS runs. The iOS
build now serves WebAssembly through a polyfill inside nodejs-mobile
itself and has a working `fetch`, so the whole iOS-only shim stack goes
with it: `index.ios.js`, the polywasm/undici installers, the SIMD-wasm
alias and the loader-entry redirect. Both platforms now bundle from one
entry, differing only in the `__loadAddon` banner.

better-sqlite3 11 doesn't compile against V8 13.6 (Node 24 dropped the
`ObjectTemplate::SetAccessor` overload it uses), and as a raw V8 addon it
needs an ABI-matched prebuild per Node version, so the tree is pinned to
one 12.10.0 through `overrides` and consumes the new ABI 137 prebuilds.

Also picked up along the way:

- TMPDIR now points at a real directory. An Android app process has none
  and there is no `/tmp`, which is where `os.tmpdir()` otherwise lands;
  reading it needed the credentials fix that arrived with this release.
- V8's on-disk code cache is enabled via `NODE_COMPILE_CACHE`. Env var
  rather than `module.enableCompileCache()` so it covers `loader.mjs` and
  the Sentry chunk, which compile before any of our JS could call the
  runtime API. The backend flushes it at `ready` instead of leaving it to
  node's exit hook — the low-memory killer and iOS's suspended-app kill
  both skip that hook, so the cache would rarely be written at all.
- The 24.x NodeMobile.xcframework has no x86_64 simulator slice, so the
  x64-simulator prebuild leg and its `lipo` pass are gone.
- `readNodeJsMobileVersions()` matched `NODE_MODULE_VERSION (.+)`, which
  on the Node 24 header hits the `NODE_EMBEDDER_MODULE_VERSION`
  passthrough first and yields a garbage ABI in prebuild URLs.
- Node 24's `v8config.h` #errors below C++20; the NDK defaults to gnu++17.
- Sentry events carry the mobile revision as a `nodejs_mobile` tag;
  `contexts.runtime` only has the upstream Node version.
`lite` drops ICU, the inspector, `node:sqlite` and TypeScript type-stripping,
and on iOS also V8's compiled tiers — dead weight there, since it runs jitless
and serves WebAssembly through the bundled polyfill.

None of it is a loss. The v18 build we came from reports 0 ICU symbols and no
`icudt` data, i.e. it was already `--with-intl=none`, so `full` would *add* an
`Intl` this backend has never had; the two `Intl` references that survive
bundling are a Sentry helper that returns early unless `process.versions.icu`
is set, and mapbox style-spec expression evaluators whose exposure is unchanged
from v18. We use the `better-sqlite3` addon rather than `node:sqlite`, ship
plain JS, and every `node:inspector` reference in the bundle is an
`await import()` inside a try/catch or an opt-in Sentry integration.

Addon prebuilds are flavour-neutral: both flavours ship byte-identical headers
and export the same V8 symbol set, and every symbol our better-sqlite3 prebuild
imports resolves against the lite `libnode`.

Sizes against the v18 build this replaces, rather than against full:

  android arm64    61 -> 65 MB
  android armv7a   57 -> 59 MB
  android x86_64   63 -> 64 MB
  ios device slice 53 -> 44 MB
Bumps the minor-and-patch group with 1 update in the /backend directory: [rolldown](https://github.com/rolldown/rolldown/tree/HEAD/packages/rolldown).


Updates `rolldown` from 1.2.3 to 1.2.4
- [Release notes](https://github.com/rolldown/rolldown/releases)
- [Changelog](https://github.com/rolldown/rolldown/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rolldown/rolldown/commits/v1.2.4/packages/rolldown)

---
updated-dependencies:
- dependency-name: rolldown
  dependency-version: 1.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
12.10.0 built against Node 24 crashes on real hardware. The backend reaches
ready and aborts ~86ms later, on every boot:

  # node[2591]: node::RemoveEnvironmentCleanupHook(...) at ../src/api/hooks.cc:142
  # Assertion failed: (env) != nullptr
  Fatal signal 6 (SIGABRT)

Everything downstream in the e2e run is fallout — RPC_TIMEOUT, socket connect
timeouts, `device-id` never rendering. `libbetter-sqlite3__12.10.0.so` was the
only shipped addon importing `Add`/`RemoveEnvironmentCleanupHook`, the raw V8
embedder API; the other six are N-API and import none of it.

13.0.0 migrated to N-API. The shipped binary now has zero V8 symbols and zero
cleanup-hook references, so the crash class is gone by construction rather than
patched. It also stops being ABI-locked: one artifact serves every Node
version, which is why `usesNapi` flips and the prebuild URL loses its
`-node-<abi>` infix.

13.x dropped `bindings` for its own `lib/binding.js`, which knows only
linux/darwin/win32 prebuild paths before falling back to node-gyp build dirs —
none of which exist on device — so the addon-loader gains a pattern for it.
Because every pattern here matches upstream source text, a miss would silently
ship a resolver that only fails on a device; the plugin now fails the build
instead when that specific resolver goes unrewritten.

Verified: 11/11 Android lifecycle tests and 8/8 iOS XCTest, 12 clean boots,
zero occurrences of the crash signature.
Removing this alias alongside the polywasm/fetch installers was wrong. Those
installers are genuinely redundant now — the runtime supplies `WebAssembly`
and `fetch` — but the alias targets a different thing: the npm `undici@6` this
backend bundles, not Node's built-in undici.

nodejs-mobile 24's bootstrap sets `UNDICI_NO_WASM_SIMD=1` to steer undici off
the SIMD build of llhttp, but that env var is an undici 7.x feature, so the
bundled 6.23.0 copy ignores it and calls
`WebAssembly.compile(llhttp_simd-wasm)` unconditionally. polywasm compiles
function bodies lazily, so that compile succeeds and then throws
`Unsupported instruction: 0xFD` on the first parser callback — past the
try/catch undici wraps the compile in.

`@comapeo/core`'s maps plugin and `secret-stream-http` (via
`@comapeo/map-server`) both import `fetch` from that bundled copy, so this
covers online map styles and peer blob/SMP fetches. Verified: the SIMD payload
is present in both bundles without the alias, and absent from the iOS bundle
with it.

The e2e suite could not have caught this — its map-server tests use the
built-in fallback map only, with no project, no uploaded SMP and no network.

Also from review: don't set TMPDIR/NODE_COMPILE_CACHE to a directory we failed
to create (and check `isDirectory`, since `mkdirs()` returns false when the
directory already exists); correct the JNI comment, which claimed TMPDIR must
be set before startup — only NODE_COMPILE_CACHE must; drop stale `lipo` and
non-NAPI references.
…bundle

The redirect that keeps undici's SIMD llhttp away from polywasm matches an
upstream import specifier, so an undici reshuffle turns it into a silent no-op
and the SIMD bytes come back. Nothing downstream notices: the failure needs a
real network fetch on a jitless device, and the e2e map-server tests
deliberately use the built-in fallback map with no project, no uploaded SMP and
no network. That is why removing the redirect went unnoticed in the first
place.

Assert the outcome rather than the mechanism — after writing the iOS bundle,
fail if it still contains the SIMD payload. The marker is the first slice of
that module's base64 that differs from its non-SIMD sibling, so it identifies
the specific wasm build without depending on module names surviving
minification. No undici installed means nothing to assert, which is the right
answer if the dependency ever goes away (see #232).

Verified in both directions: the build passes as-is, and fails with an
actionable message when the redirect is made not to match.
- Drop NODE_COMPILE_CACHE_PORTABLE on both platforms. It keys cache entries
  by path relative to the cache dir, which only helps when the modules and the
  cache move together. Ours never do: the cache is in cacheDir/Library Caches
  and the JS is in filesDir/the app bundle, so the relative path still contains
  the varying component. Verified by moving the module dir between runs — the
  entry hash changes either way, so the setting bought nothing.

- Report a failure to create either directory to Sentry via `logCapture`, the
  existing helper for notable non-exception events, rather than a local log
  line. A device that can never write there silently pays the cold-compile cost
  on every launch, so the rate is worth seeing.

- Trim the addon-loader comments to what a maintainer of the current code
  needs: no better-sqlite3 version history, and no restating between the file
  header and the pattern it describes.
`broadcast()` only queues a frame — streamx defers the real write to the
next tick — while `ServerHelper.close()` calls `destroySoon()` in the same
tick. The deferred write then hit an ended socket, so the `stopping` frame
never reached native, `close()` rejected on the resulting socket error and
left `#state` stuck at "closing", and every shutdown logged a bogus
"Client sent invalid message" plus an `ipcError` metric sample.

`SocketMessagePort` now exposes `drained()`, `SimpleRpcServer.close()`
awaits it for every client before delegating to `super.close()`, and
`ServerHelper.close()` waits on the socket's "close" event directly
instead of `once()`, which rejects if the socket errors first.

Closes #231
nodejs-mobile 24 embeds undici, so the npm copy that @comapeo/core and
secret-stream-http import was 389 KB of duplicate bundle — the largest
package in the backend bundle. Alias `undici` at bundle time to a shim
that exports the global `fetch` and recovers `Agent` from Node's global
dispatcher.

The dispatcher only exists once Node's internal undici has initialised,
which `new Request()` forces cheaply; secret-stream-http subclasses
`Agent` at module scope, so the shim has to resolve eagerly and throws
if it cannot.

With the npm copy gone, the llhttp wasm payloads leave the bundle
entirely, so `aliasUndiciSimdWasmPlugin` and its build-time assertion
have nothing left to guard.
The in-app jasmine suite intermittently never renders all-tests-done, and a
stalled run left nothing to diagnose: no spec name in CI output, no device
logs, no screenshot (Maestro's extendedWaitUntil dies before the screenshot
step).

In the e2e app, always render the currently-running spec (testID
current-spec), give the progress counter a testID (test-progress), and wrap
jasmineEnv.execute() in a 240s watchdog — inside Maestro's 300s window — that
renders all-tests-done plus a 'Suite timed out during: <spec>' failure, so a
stall reaches the screenshot step and names the culprit. Replace the
NoopGlobalErrors stub with a real implementation over ErrorUtils and Hermes'
rejection tracker (neither is wired up in Release), so uncaught errors and
unhandled rejections are console.error'd, routed to jasmine, and surfaced in
the UI instead of silently hanging the suite.

In the run-browserstack-maestro action, on the final failed attempt download
each failed session's device logs and screenshots (URLs verified against a
real past build's session JSON), print the app-tagged device-log lines into
the job log, and upload everything as a browserstack-diagnostics-<platform>
artifact. Collection is best-effort and the upload step runs only after the
run step has already failed, so diagnostics can never mask the real failure
or change the action's exit semantics.
… reconnect window

Review follow-ups on the reconnect machinery:

- Deliver Disconnected/Error to the connection-state observer imperatively
  at the two terminal commit points (teardown completion, connect failure):
  the conflating StateFlow lets the auto-reconnect CAS Disconnected away
  before the collector runs, so a drop could be missed entirely — and JS
  recovery hangs off that exact emission. Duplicate delivery is documented
  as part of the observer contract; close() still suppresses all calls.
- Guard ComapeoCoreModule's State.Error mapping to STARTING/STARTED so
  reconnect exhaustion after a graceful stop cannot reclassify a clean
  STOPPED as a spurious terminal ERROR (mirrors the Disconnected guard).
- Widen the auto-reconnect window 60s -> 120s: a debug build or slow device
  can take over a minute to restart the backend, and the FGS-kill test
  itself budgets 90s for a debug boot.
- Close the stale-epoch rollback wedge: a current-epoch disconnect now
  waits out a transient Disconnecting (which a stale disconnect rolls back
  to Connected) instead of being swallowed, so state can no longer wedge
  at Connected with dead IO loops.
- Test hardening: retain accepted server sockets in closeSuppressesReconnect
  (GC finalization could close them mid-test and fake a reconnect), and
  assert >= 2 ready frames in FgsKillRecoveryTest (each control connection
  gets a ready replay, so a transient drop legitimately delivers more).
… backend restart

When Android kills and restarts the :ComapeoCore service, the sockets
now reconnect (PR #225) — but in-flight RPC calls still hung until the
30s timeout, and the restarted backend had lost every event
subscription, so listeners went permanently deaf.

The message socket now reports its connection state to JS as a
transportStateChange event (declared on iOS for parity; never fires
there — in-process Node death ends the app). On a drop, the module
calls @comapeo/ipc's transport-reset helpers: in-flight calls reject
with TransportClosedError (code RPC_TRANSPORT_CLOSED, re-exported
here) so callers can tell "backend restarted, a read is safe to
retry" from a real failure; subscriptions on the long-lived channels
are re-sent through the native send queue; stale per-project clients
are hard-closed so the next getProject mints a working one.

subscribeToBackendRestart() fires once the backend is STARTED again
after a drop — wire it to @comapeo/core-react's new
subscribeToBackendRestart provider prop so its query caches re-fetch.
docs/ARCHITECTURE.md §5.8 documents the recovery layers and the
host-app state (module-scope captures in comapeo-mobile) that recovery
cannot reach.

Requires @comapeo/ipc >= the release containing
digidem/comapeo-ipc#87 (which itself needs digidem/rpc-reflector#52);
the dependency bump lands here once released.
… sockets

Review found three interacting flaws in the drop-time wiring: resubscribing
at drop time nudged the native connect out of its terminal Error state in a
tight retry loop for as long as the backend stayed down; the restart signal
keyed solely on the control-socket lifecycle state, so a message-socket-only
drop hard-closed project clients but never told the app layer to re-fetch;
and one throwing restart listener silenced the rest.

Recovery is now two-phase: at drop time the ipc reset helpers only reject
in-flight calls and hard-close stale project clients (the reshaped upstream
API no longer bundles resubscription); once BOTH the transport is
reconnected and the backend reports STARTED — in whichever order — the
module replays subscriptions (idempotent server-side) and fires restart
listeners, each isolated in try/catch.

Also corrects the TransportClosedError wording (the response will never
arrive; the call may have executed) and documents the recovery-window state
flap and the not-covered surfaces in ARCHITECTURE.md §5.8.
@comapeo/ipc v10 makes project channels stable per projectPublicId and
owns project lifecycle server-side, so the client-side recovery no
longer hard-closes project references or distinguishes core/services
reset helpers. Phase 1 (drop) calls the polymorphic
notifyTransportReset on both clients, rejecting in-flight calls with
rpc-reflector's ChannelClosedError (RPC_CHANNEL_CLOSED); phase 2
(reconnected AND STARTED) calls resubscribe on both. TransportClosedError
no longer exists in v10 — RpcChannelClosedError is re-exported instead.

The dependency is bumped to ^10.0.0 in the module and the backend;
until v10 publishes it must be installed from a local checkout
(npm install ../comapeo-ipc ../rpc-reflector --no-save), so the
lockfiles intentionally stay at 9.0.1 and npm ci will fail.
The control server replays started/ready to late-connecting clients,
so state transitions cannot distinguish an app-side reconnect (backend
alive — must not fire restart listeners) from a real backend restart
(must fire). The backend now generates one crypto.randomUUID() per
process and stamps it on the ready frame; SimpleRpcServer replays the
extra fields identically to late clients. The Kotlin control observer
parses the nonce into ControlFrame.Ready, stores it, carries it on the
stateChange payload, and exposes it via getBootNonce (a repeat ready
with a NEW nonce re-emits STARTED even without an observed intermediate
state). JS recovery compares the post-recovery nonce with the last one
seen outside a recovery window: same nonce resubscribes silently, a new
nonce also fires subscribeToBackendRestart listeners, the first-ever
nonce is initial boot (no signal), and a missing nonce (older backend)
fires conservatively. A ready frame without the field still parses, so
mixed versions degrade gracefully.
@comapeo/ipc v10 removes the client-side project close, so the v9
close-semantics specs (close then expect rejections; close/re-open via a
fresh getProject) no longer describe the contract. Replace them with the
v10 contract: calls transparently survive a backend-side project close,
and getProject returns the same permanent reference afterwards. The
per-test mass-close teardown goes away with them — references are
permanent and there is nothing to tear down.

A backend-side close needs a trigger the production surface deliberately
does not expose, so the backend gains an e2e-only RPC channel,
@@comapeo-debug/lifecycle (rpc-reflector server over an { id, message }
sub-channel on the message socket), exposing closeProject(projectPublicId)
= manager.getProject + project.close. It is served only when the backend
starts with COMAPEO_DEBUG_LIFECYCLE=1; otherwise the id is ignored like
any foreign channel (@comapeo/ipc routes only its own @@comapeo/ prefix)
and debug calls time out — the specs then mark themselves pending, since
the flag is not yet plumbed through the native start path.

Covered by backend unit tests (served/unserved/coexistence over a real
socket). The e2e specs typecheck but are untested on device here. The
e2e app's @comapeo/core devDependency moves to 7.2.0 to match the
version @comapeo/ipc v10 types against.
Every closeProjectOnBackend call paid the full 10s RPC timeout before
concluding the debug channel is unserved — ~100s per run across the ten
lifecycle specs, and today every run pays it since the env flag is not
yet plumbed into the native start path. Worse, the timeout was
ambiguous: with the channel served, a closeProject that hung >10s was
mistaken for an unserved channel and silently marked the spec pending.

The debug server gains a ping() liveness method; the e2e client probes
it once per app session and caches the result. An unserved channel now
costs a single 10s probe with every later call skipping straight to
pending, and once the probe has succeeded any closeProject rejection —
timeouts included — propagates as a real failure.
If the message socket reconnects to a restarted backend before its drop
event is processed, no recovery window ever opens and the new boot nonce
arrives via stateChange with transportDropped false — previously it was
recorded silently and the restart never fired. A nonce change observed
outside a recovery window now runs the same resubscribe-then-fire
sequence recovery would have (the transport is by definition connected
at that point). Covered by a unit test for that ordering.

Also documents in ARCHITECTURE.md §5.8 the pre-existing post-deadline
wedge from the reconnect layer: past the ~120s reconnect window both
sockets sit in terminal Error, a later RPC revives only the message
socket, and the control socket reconnects only on the next
foreground transition — until then getState() stays ERROR and
resubscription is deferred while calls quietly work without events.
@gmaclennan
gmaclennan force-pushed the feat/rpc-transport-recovery branch from 039359e to 0bf57a8 Compare August 20, 2026 17:50
@gmaclennan
gmaclennan marked this pull request as ready for review August 20, 2026 17:50
@gmaclennan gmaclennan changed the title feat: fail in-flight RPC calls fast and recover subscriptions after a backend restart feat!: reconnect IPC and recover subscriptions after a backend restart Aug 20, 2026
@github-actions github-actions Bot added the breaking Breaking change (changelog) label Aug 20, 2026

@RangerMauve RangerMauve left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall. minor comments

@@ -1,187 +1,341 @@
import { useState } from 'react'
import { useState } from "react";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we undo these style changes or maybe do them in main then rebase so that we can have less noise?

Comment thread src/ComapeoCore.types.ts
* late-connecting clients, so a reconnect delivers the SAME nonce when the
* backend kept running and a NEW one when it restarted; the recovery logic
* fires `subscribeToBackendRestart` listeners only on a nonce change.
* Absent on iOS and from backends that predate the field.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does from backends that predate the field mean?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaking change (changelog) feature New feature (changelog)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants