Skip to content

fix(sdk): prevent recursive filled-host comparisons - #3861

Merged
dr-bonez merged 1 commit into
masterfrom
fix/host-watch-deep-equal
Sep 2, 2026
Merged

fix(sdk): prevent recursive filled-host comparisons#3861
dr-bonez merged 1 commit into
masterfrom
fix/host-watch-deep-equal

Conversation

@MattDHill

@MattDHill MattDHill commented Aug 28, 2026

Copy link
Copy Markdown
Member

Summary

FilledAddress objects exposed helper methods and derived address views as enumerable properties. Comparing two filled hosts with deepEqual therefore traversed the recursively derived views and could overflow the stack when a host watch received an update.

This change:

  • makes filled-address helpers and derived views non-enumerable, leaving only address data in enumeration, serialization, and equality checks
  • adds an identity fast path to deepEqual
  • logs errors that terminate a Watchable.const() subscription
  • adds coverage for filled-host equality, changed address data, enumeration, and helper access

Testing

  • start-core test suite: 123 passed, 9 skipped

`sdk.host.get(...).const()` with no `map` compares whole `FilledHost` records
with the default `deepEqual`. A filled address carries enumerable
`nonLocal`/`public`/`bridge` getters that each return another filled address
with the same getters, and `deepEqual` had no identity short-circuit, so the
walk never terminated — it overflowed the stack on any pair, an object against
itself included.

`watchGen` only calls `eq` from the second value onward, so `const()` resolved
normally and the throw waited for the first actual change. There it landed in
`const()`'s rejection handler, which aborted the watch without calling
`constRetry` and without logging, so a package watching a dependency's address
simply stopped reacting: no restart, no error, nothing in the logs.

Three fixes, each of which independently closes the failure:

- `deepEqual` returns true for arguments that are already the same reference.
- A filled address's helpers and derived getters are non-enumerable, so it
  compares, serializes and spreads as the data it wraps.
- `const()`'s rejection path logs. Whatever kills a watch, it says so rather
  than leaving the context silently unreactive. It still aborts rather than
  retrying: the same path takes a failed fetch, and restarting there would
  spin.

Found via simplex-websocket-bridge-startos, whose relays went stale after the
SimpleX Server was reinstalled. `swatcher` and `i2pd-startos` have the same
unmapped read today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@MattDHill
MattDHill force-pushed the fix/host-watch-deep-equal branch from 19ebfb1 to fc1a559 Compare August 28, 2026 20:04
@helix-nine helix-nine changed the title fix(sdk): keep .const() host watches alive across a change fix(sdk): prevent recursive filled-host comparisons Sep 2, 2026
@dr-bonez
dr-bonez merged commit e200d39 into master Sep 2, 2026
29 checks passed
@dr-bonez
dr-bonez deleted the fix/host-watch-deep-equal branch September 2, 2026 20:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants