Skip to content

fix(js-sdk): treat WatchHandle.stop() as a clean watch end - #1912

Merged
devin-ai-integration[bot] merged 4 commits into
e2b-dev:devin/1790858689-watch-stop-clean-exitfrom
harshitgavita-07:fix/js-sdk-watch-stop-clean-exit
Oct 1, 2026
Merged

devin-ai-integration[bot] merged 4 commits into
e2b-dev:devin/1790858689-watch-stop-clean-exitfrom
harshitgavita-07:fix/js-sdk-watch-stop-clean-exit

Conversation

@harshitgavita-07

@harshitgavita-07 harshitgavita-07 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #1895

WatchHandle.stop() is the documented way to end a directory watch, but every user-initiated stop fired onExit with a TimeoutError blaming requestTimeoutMs: stop() aborts the stream, the abort surfaces as a Connect cancellation, and the error mapping treats it as a request timeout.

This tracks the user-initiated stop in WatchHandle and treats the resulting stream end as clean: onExit now fires with no argument, matching the WatchOpts contract ("Callback to call when the watch operation stops") and the Python SDK behavior from #1480.

Changes

  • packages/js-sdk/src/sandbox/filesystem/watchHandle.ts: set a stopped flag in stop(); skip reporting the stream error to onExit when the end was user-initiated (13 lines incl. comments)
  • packages/js-sdk/tests/sandbox/files/watchHandle.test.ts: two new tests -
    • onExit fires with no argument when the watch is stopped by the user (fails before the fix with the reported TimeoutError, passes after)
    • stream errors are still reported to onExit when the watch was not user-stopped
  • Changeset: e2b patch

Verification

  • New stop test fails on unpatched code with the exact TimeoutError from the issue, passes with the fix
  • All 6 watchHandle tests pass
  • tsc --noEmit clean
  • Full unit suite: identical failure set before/after (pre-existing environment-dependent failures unrelated to this change)

Devin Review

A user-initiated stop aborts the stream, which surfaced as a Connect
cancellation mapped to a TimeoutError blaming requestTimeoutMs, so
onExit fired with a spurious error on every stop(). Track the
user-initiated stop and treat it as a clean end: onExit now fires with
no argument, matching the Python SDK (PR e2b-dev#1480) and the documented
WatchOpts contract.

Fixes e2b-dev#1895
@cla-bot

cla-bot Bot commented Sep 29, 2026

Copy link
Copy Markdown

We require contributors to sign our Contributor License Agreement, and we don't have @harshitgavita-07 on file. You can sign our CLA at https://e2b.dev/docs/cla . Once you've signed, post a comment here that says '@cla-bot check'

@changeset-bot

changeset-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3d6cd3f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
e2b Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration devin-ai-integration Bot 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.

TASTE.md review. Checked: T-1 (JS/Python parity), T-26/T-27/T-28/T-30 (watch handle, callbacks, and stop()), T-57–T-62 (error handling), T-69/T-70 (JSDoc).

1 violation (docs only). The behavior change is good. It fixes a T-1 parity gap: async Python AsyncWatchHandle.stop() already calls on_exit(None), and after this PR JS stop() leads to onExit() with no argument. Sync Python uses a polling handle with no on_exit, which T-28 allows. The public contract changed, but the JSDoc doesn't say so yet (inline comment on stop()).

Not on a changed line: WatchOpts.onExit in packages/js-sdk/src/sandbox/filesystem/index.ts ("Callback to call when the watch operation stops.") should also say that err is undefined after stop() or a clean stream end, and is set only when the watch failed (T-62: document when an error is surfaced).

@@ -91,6 +97,7 @@ export class WatchHandle {
* Stop watching the directory.

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.

T-69 (docstrings are part of the API; document defaults and failure modes) / T-70 (JSDoc tags). This PR makes stop() guarantee a clean end: onExit is called with no error, not a TimeoutError. Callers who branch on err depend on that guarantee, so the public JSDoc should state it instead of leaving it only in a private field comment.

Suggested change
* Stop watching the directory.
* Stop watching the directory.
*
* Stopping is a clean end: `onExit` is called with no error.

@mishushakov

Copy link
Copy Markdown
Member

looks good, sign the CLA and check the comment above

@harshitgavita-07

Copy link
Copy Markdown
Contributor Author

We require contributors to sign our Contributor License Agreement, and we don't have @harshitgavita-07 on file. You can sign our CLA at https://e2b.dev/docs/cla . Once you've signed, post a comment here that says '@cla-bot check'

@cla-bot check

@cla-bot cla-bot Bot added the cla-signed label Sep 30, 2026
@cla-bot

cla-bot Bot commented Sep 30, 2026

Copy link
Copy Markdown

The cla-bot has been summoned, and re-checked this pull request!

@harshitgavita-07

Copy link
Copy Markdown
Contributor Author

@mishushakov the CLA check is green, and I've addressed the JSDoc feedback for stop() and WatchOpts.onExit. All 6 watch-handle unit tests and TypeScript typechecking pass against the updated branch. Could you take another look when you have a moment?

@mishushakov

Copy link
Copy Markdown
Member

/accept

@devin-ai-integration
devin-ai-integration Bot changed the base branch from main to devin/1790858689-watch-stop-clean-exit October 1, 2026 12:44
@devin-ai-integration
devin-ai-integration Bot merged commit be2f23a into e2b-dev:devin/1790858689-watch-stop-clean-exit Oct 1, 2026
1 of 2 checks passed
@devin-ai-integration

Copy link
Copy Markdown
Contributor

CI is green on #1923. Lint and Generated files first failed on formatting in the new watchHandle test file, so I pushed a commit that reformats it with prettier.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

I fixed the valid review finding on #1923 and replied on the PR that it's mergeable: callback errors were being swallowed after stop(), and a regression test now covers it. The bug is JS-only; Python already reports a stop cleanly.

One check is red: JS SDK - cloudflare-deploy failed when its Cloudflare deploy step hit fetch failed, which looks like a runner network problem. It passed on the previous commit. I don't have permission to re-run it, so a maintainer needs to re-run that job before merging.

mishushakov pushed a commit that referenced this pull request Oct 1, 2026
## Summary

Squashed version of community PR
#1912 by @harshitgavita-07. Fixes
#1895.

`WatchHandle.stop()` aborts the watch stream. The abort surfaced as a
Connect cancellation, which was mapped to `TimeoutError` and passed to
`onExit`, so every user-initiated stop looked like a request timeout.
The handle now records the stop, and a stream error that arrives after
it ends the watch cleanly:

```ts
async stop() {
  this.stopped = true
  this.handleStop()
}

private async *iterateEvents() {
  try { for await (const event of this.events) ... }
  catch (err) {
    if (this.stopped) return
    throw await handleRpcErrorWithHealthCheck(err, this.checkHealth)
  }
}
```

The suppression covers only the stream iterator, so `onEvent` rejections
still reach `onExit`, including ones raised after `stop()`. After
`stop()`, `onExit` is called asynchronously with no argument, which
matches what async Python `AsyncWatchHandle.stop()` passes (#1480). This
is now documented in the JSDoc on `stop()` and `WatchOpts.onExit`.

Tests in `packages/js-sdk/tests/sandbox/files/watchHandle.test.ts` cover
three cases:
- a clean exit after `stop()`
- stream errors when the watch was not stopped
- an `onEvent` error thrown after `stop()`

Includes an `e2b` patch changeset.

Not covered: aborting through `opts.signal` still reports a
`TimeoutError`. That behaviour predates this PR and is left for a
follow-up.

Link to Devin session:
https://app.devin.ai/sessions/410788c120d144ed87242ce3b1eb22f7
Open in Devin Desktop:
https://app.devin.ai/desktop/session/410788c120d144ed87242ce3b1eb22f7?variant=devin
<!-- devin-review-badge-begin -->

---

<a href="https://app.devin.ai/review/e2b-dev/e2b/pull/1923"
target="_blank"><picture><source media="(prefers-color-scheme: dark)"
srcset="https://static.devin.ai/assets/gh-devin-review-dark.svg?v=4"><img
src="https://static.devin.ai/assets/gh-devin-review-light.svg?v=4"
alt="Devin Review"></picture></a>
<!-- devin-review-badge-end -->

---------

Co-authored-by: harshitgavita-07 <harshit.gavita@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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.

[Bug]: JS SDK: filesystem watch handle.stop() always fires onExit with a TimeoutError blaming 'requestTimeoutMs'

2 participants