Skip to content

Live docs: watch lifecycle — reap resource watches on web-client disconnect (lease/heartbeat) #1426

Description

@nick-pape

Carve-out from #1395 (PR #1424). Part of epic #1394.

Problem

The Server-side resource-watch registry (packages/plugin-core/src/resource-handlers.ts) cleans up a watch via exactly two early paths:

  1. Explicit — the web client calls UnwatchResource (last ref → subscription.close() → wire unsubscribe).
  2. Environment disconnect — the 30s reaper (reapDeadWatches) drops any watch whose adapterManager.getConnection(environmentId) is now undefined.

There is a gap between them: if a web client vanishes (browser crash, laptop sleep, network drop, tab killed) while its environment stays connected, neither path fires:

  • No UnwatchResource is sent (client is gone).
  • getConnection() still returns a live connection, so the reaper skips the entry.

Result: the WatchEntry + PowerLine chokidar watcher stay alive watching a file nobody is viewing, until the environment eventually disconnects or the server restarts.

Why this was deferred (v0 tradeoff)

The blast radius is small and self-healing, so it was intentionally left for v1:

  • Magnitude is one idle chokidar handle per (environment, uri, recursive) a dead client had open (ref-count dedup means N dead clients of the same file = one watcher).
  • Leaked resource.changed events are harmless — live useResources hooks don't have the URI cached and no-op.
  • Any environment stop/reconnect cycle reaps the leak via path 2.

The proper fix needs a liveness signal tying a watch to a specific web client, which the current global StreamEvents broadcast substrate doesn't carry (the server can't tell which client a watch belongs to, nor when one client's stream drops).

Proposed approaches (pick one during design)

  • Lease / heartbeat (lightest): WatchResource returns a lease the client renews on an interval (or fold renewal into the existing periodic activity); the reaper drops leases not renewed within ~2× the interval. Add a renewWatch RPC (or reuse an existing periodic call).
  • Per-client watch ownership: bind each watch to the client's StreamEvents connection and reap on that stream's close. Requires the watch registry to know about individual event-stream subscriptions (it currently doesn't).

Acceptance criteria

  • A watch opened by a web client that then disconnects (without UnwatchResource) is released within a bounded TTL, even while its environment stays connected.
  • The wire unsubscribe is sent so PowerLine releases its chokidar watcher.
  • Existing explicit-unwatch and environment-disconnect reap paths keep working; ref-counting across multiple viewers of the same file is preserved.
  • Unit coverage for the new reap path (lease expiry / client-stream close).

References

  • Registry + reaper: packages/plugin-core/src/resource-handlers.ts (reapDeadWatches, WATCH_REAPER_INTERVAL_MS, unwatchResource)
  • Wire watch: packages/powerline/src/ahp-handlers.ts (createResourceWatch / unsubscribe)

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew functionalityserver@grackle/server

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions