ts: MeshJob Stream subscription — subscribeEvents (TypeScript) - #1049
Conversation
napi binding + SDK async iterator + integration test. Mirrors the Python vertical (#1047). nextAfter watermark advances cursor on empty pages (server-side `types` filter friendliness). napi tuple shape: napi-rs's auto-serialisation does not emit a tuple on the JS side, so the binding returns a `#[napi(object)]`-wrapped `ListEventsResult { events, nextAfter }` instead of bare `(Vec<JobEvent>, i64)`. Struct name intentionally drops the `Js` prefix used by sibling types because napi-rs's codegen emits the Rust struct name verbatim for method return types (it honours `js_name` for top-level function returns but not for `#[napi] impl` method returns); keeping the Rust name aligned with the JS name avoids a dangling `JsListEventsResult` reference in the generated index.d.ts. Closes #1048
📝 WalkthroughWalkthroughThis PR implements TypeScript stream subscription mode for MeshJob: a non-destructive async iterator that allows clients to observe a job's event stream independently from the consumer. It adds an N-API binding, exposes subscribeEvents on mesh.jobs, validates iterator behavior with comprehensive unit tests, and verifies end-to-end observer patterns with concurrent producer/consumer integration. ChangesStream subscription mode for job events
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/integration/suites/uc22_meshjob_ts/tc27_subscribe_events_streams_observer_pattern_ts/test.yaml (1)
118-130: ⚡ Quick winStrengthen posted sequence assertions to target
posted_seqsdirectly.Lines 125-130 currently match
"seq"anywhere in the response, so they can pass even ifposted_seqsis wrong. Assert theposted_seqsarray content instead.Proposed assertion update
- - expr: "${captured.commission_response} contains '\\\"seq\\\":1'" - message: "first posted event must have seq=1" - - expr: "${captured.commission_response} contains '\\\"seq\\\":2'" - message: "second posted event must have seq=2" - - expr: "${captured.commission_response} contains '\\\"seq\\\":3'" - message: "third posted event must have seq=3" + - expr: "${captured.commission_response} contains '\\\"posted_seqs\\\":[1,2,3]'" + message: "posted_seqs must preserve ordered values [1,2,3] for a fresh job"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/suites/uc22_meshjob_ts/tc27_subscribe_events_streams_observer_pattern_ts/test.yaml` around lines 118 - 130, The current assertions check for '"seq":N' anywhere, which can match unrelated fields; update the assertions that reference captured.commission_response to explicitly assert the posted_seqs array and its entries (e.g., that captured.commission_response contains the "posted_seqs" key and that its array contains 1,2,3 in order or at least contains those values tied to that key). Modify the three seq checks to target posted_seqs (and keep the existing message strings) so they validate posted_seqs contains seq=1, seq=2 and seq=3 rather than any stray "seq" occurrences.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In
`@tests/integration/suites/uc22_meshjob_ts/tc27_subscribe_events_streams_observer_pattern_ts/test.yaml`:
- Around line 118-130: The current assertions check for '"seq":N' anywhere,
which can match unrelated fields; update the assertions that reference
captured.commission_response to explicitly assert the posted_seqs array and its
entries (e.g., that captured.commission_response contains the "posted_seqs" key
and that its array contains 1,2,3 in order or at least contains those values
tied to that key). Modify the three seq checks to target posted_seqs (and keep
the existing message strings) so they validate posted_seqs contains seq=1, seq=2
and seq=3 rather than any stray "seq" occurrences.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3369d217-111e-4a41-9582-ab8c370cb51c
⛔ Files ignored due to path filters (1)
src/runtime/core/typescript/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (7)
src/runtime/core/src/jobs_napi.rssrc/runtime/typescript/src/__tests__/jobs.spec.tssrc/runtime/typescript/src/index.tssrc/runtime/typescript/src/jobs.tstests/integration/suites/uc22_meshjob_ts/fixtures/long-task-consumer-ts/src/index.tstests/integration/suites/uc22_meshjob_ts/fixtures/long-task-provider-ts/src/index.tstests/integration/suites/uc22_meshjob_ts/tc27_subscribe_events_streams_observer_pattern_ts/test.yaml
## Summary - Adds **Stream subscription mode** for MeshJob (Java vertical of #1032 follow-up): `MeshJobs.subscribeEvents(jobId, options) -> EventSubscription` returning a blocking `Closeable` iterator over `Map<String, Object>` events. - Completes the polyglot trilogy started by #1047 (Python `subscribe_events`) and #1049 (TS `subscribeEvents`). Multiple subscribers can observe the same job's events independently via per-call cursors. - Four layers: FFI `mesh_job_proxy_list_events` returning a `{events, next_after}` JSON envelope; JNR binding in `MeshCore.java`; Java SDK `EventSubscription` + `SubscribeOptions` + `MeshJobs.subscribeEvents` overloads reusing the existing LinkedHashMap-based LRU `proxyCache` from #1045; integration test `tc27_java` extending the long-task fixtures with `run_until_done` (provider) and `commission_subscribe_observer` (consumer) capabilities. - **Java-specific value-add**: `EventSubscription implements Closeable`. Try-with-resources gives Java something Python's `asyncio.CancelledError` provides naturally and TS can't replicate without `AbortController` plumbing — clean iterator shutdown that stops issuing new FFI long-polls. ## Review Notes Independent review caught 1 BLOCKER and 5 substantive WARNINGs (all addressed in the amended commit): 1. **`JobProxy` lock serialized concurrent subscribers** (BLOCKER) — every `JobProxy` method was wrapped in a single `synchronized` block. Two `EventSubscription` instances sharing one cached `JobProxy` would serialize their 30-second long-polls. Replaced with `ReentrantReadWriteLock`: `close()` takes the write lock; `listEvents` / `sendEvent` / `status` / `await` / `cancel` take the read lock and can run concurrently. The Rust core's `JobProxy` is `Sync` (Arc + String, no internal mutex) so concurrent FFI calls are safe — empirically validated by the fact that Python/TS verticals have always worked without any client-side lock. 2. **`closed` field needed `volatile`** — without it, cross-thread `close()` calls from arbitrary threads may not be observed by the iterator's `while (!closed)` loop. Textbook JMM hole. Now `volatile`. 3. **`SubscribeOptions.Builder` lacked `after >= 0` validation** — fail-fast belongs at the builder. Now throws `IllegalArgumentException` for negative values. 4. **Integration fixture leaked `EventSubscription` on poster exception** — `Thread.sleep` / `postEvent` calls could throw without closing the subscription. Now wrapped in try-with-resources. 5. **Fixture silently swallowed subscriber `RuntimeException`** — `JobNotFoundException` / malformed payload errors produced empty `observed_events` with no diagnostic. Now logged via slf4j. 6. **`close()` doesn't interrupt an in-flight FFI long-poll** — the original JavaDoc overstated the close() contract as "the Java analog of Python's `asyncio.CancelledError` propagation". Rewrote honestly: close() flips a flag that stops *future* long-polls but does NOT interrupt the in-flight one (true interruption would require plumbing cancellation through FFI — out of scope). Documented the practical guidance: short `longPoll` for rapid shutdown. Other findings (4 INFOs + 2 cosmetic WARNINGs) skipped as not load-bearing. Closes #1050 ## Test plan - [x] Java unit tests: 80/80 pass (76 baseline + 4 new — 2 for `SubscribeOptions` builder validation, 2 for `ReentrantReadWriteLock` semantics — including a behavioral test that asserts two threads can enter the read lock simultaneously via `getReadLockCount() == 2`) - [x] Rust FFI tests: 23/23 pass (unchanged from initial implementation) - [x] `src-tests` 12/12 pass (image rebuilt with the new FFI symbol) - [x] uc23_meshjob_java integration suite: 27/27 pass (26 baseline + 1 new tc27_java; tc26 retry-flake unrelated, passed clean isolated) - [x] `parse_ffi_timeout_secs` reused at FFI boundary (NaN/Inf/negative-sentinel) - [x] LRU `JobProxy` cache reused from #1045 (no new cache) - [x] Server-side `types` filter forwarded end-to-end (no client-side post-filter) - [x] `next_after` watermark advances cursor on empty pages (parity with #1047 Python and #1049 TS) - [x] `Boolean`-`seq` and missing-`seq` rejection (parity with sibling verticals) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added event subscription API to long-poll and stream job events with optional event-type filtering. * Introduced `subscribeEvents()` method with configurable options for cursor positioning and timeout behavior. * New iterator-based event stream interface for consuming events in applications. * **Bug Fixes & Improvements** * Enhanced thread-safety for concurrent job operations. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/dhyansraj/mcp-mesh/pull/1051?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…1053) ## Summary Documentation-only PR covering the six commits shipped since v2.1.0 (MeshJob event-injection trilogy: #1041 / #1043 / #1045 for `recv_event` / `send_event` / `post_event`; #1047 / #1049 / #1051 for `subscribe_events`). - `docs/concepts/jobs.md` — adds two new sections after the existing v2.0 baseline material: **Event injection** (producer/consumer model + recv loop + post pattern + type filtering + synthetic cancel event with grace window + typed errors) and **Stream subscription** (per-call cursor + `next_after` watermark + no-automatic-terminal-detection + multiple-concurrent-subscribers semantics). Both sections carry cross-runtime Python/TS/Java tabbed examples respecting the three distinct iterator shapes (Python async generator, TS async generator, Java blocking `Closeable` iterator). - `docs/concepts/stateful-agents.md` — replaces the stale "Coming soon: mesh-managed event channel" block (which referenced #1032 as roadmap) with an accurate cross-link to the new sections. - `docs/concepts/index.md` — grid card teaser mentions event injection. - `docs/environment-variables.md` — documents `MCP_MESH_JOBPROXY_CACHE_MAX` (default 256) and `MCP_MESH_CANCEL_EVENT_GRACE_MS` (default 200, capped 10000). - `src/core/cli/man/content/` — same env-var content mirrored to `environment.md`; new **Event injection** + **Stream subscription** sections added to `jobs.md`, `jobs_typescript.md`, `jobs_java.md` (each variant carries idiomatic native-runtime code). - `RELEASE_NOTES.md` — `## v2.2.0 (UNRELEASED)` stub at the top, grouping the six commits by feature rather than PR number, mirroring the v2.1.0 entry's voice and structure. `meshctl man` content is **hand-maintained markdown**, not generated from `docs/` — both surfaces were updated to keep them in sync. ## Review Notes Independent review caught 3 substantive WARNINGs (all addressed) and surfaced one pre-existing adjacent inaccuracy (also folded in): 1. **Java `Map.of` foot-gun** — the cancel-event handling example used `Map.of("reason", payloadMap.get("reason"))`, which NPEs if the synthetic cancel event has no `reason` field (the API allows null reasons). Replaced with a null-tolerant pattern using `Objects.toString(payload.get("reason"), "")`. 2. **Misstated Java cancel mechanism** — `jobs_java.md` claimed handlers observe cancel via `InterruptedException`. Per `JobController.java:239-249`, Java's `Thread#sleep` cannot be interrupted by the Tokio cancel token firing — handlers must poll `isCancelled()` between work units OR park on `recvEvent(["cancelled", ...])`. Rewrote the section accurately. 3. **TS double-bang + `as any` cast** — the canonical TS doc example used `job!.recvEvent!(...)` and `(event.payload as any)?.reason`. Replaced with idiomatic early-return narrowing + `Record<string, unknown>` payload typing so the example doesn't teach non-idiomatic TS. 4. **Adjacent pre-existing inaccuracy** — `jobs_java.md:308-310` (outside the diff) carried the same wrong `Thread.sleep` interrupt claim in the Cancellation section. Folded a one-paragraph fix in since the same surface area was already being edited. All API signatures, env-var defaults (256, 200ms, 10000ms cap), tab indentation, sequence-diagram syntax, anchor links, and framing verified accurate against source files. Closes #1052 A follow-up PR (tracked separately) will add a runnable example agent under `examples/` demonstrating the same APIs. ## Test plan - [x] `mkdocs build` clean (no new errors/warnings introduced; pre-existing orphan-page warnings unrelated) - [x] `mkdocs serve` renders new sections cleanly (grid cards, tabbed code, mermaid `sequenceDiagram` blocks) - [x] `meshctl man environment --raw | grep -E 'MCP_MESH_(JOBPROXY|CANCEL_EVENT)'` matches both env vars - [x] `meshctl man jobs [--typescript|--java] --raw` matches new Event injection + Stream subscription sections per runtime - [x] `meshctl man jobs --java --raw | grep InterruptedException` returns only an unrelated `throws` clause in a code sample — the wrong cancel-mechanism claim is gone - [x] `meshctl man jobs --typescript --raw | grep -E '!\.|\bas any\b'` zero matches — non-idiomatic TS scrubbed - [x] `go build ./...` clean (man content compiles via `//go:embed`) - [x] No-deprecated-in-docs rule observed; public-artifact framing observed (no naming of downstream consumers, no behavioral framing) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * MeshJob event injection: post and receive job events across all supported runtimes with typed error handling and configurable cancel-event grace window. * MeshJob stream subscription: non-destructive event observation with independent per-call cursors and watermark filtering. * **Documentation** * Added comprehensive event injection and stream subscription guidance across Python, TypeScript, and Java documentation. * Documented new environment variables for event channel tuning. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/dhyansraj/mcp-mesh/pull/1053?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary Release v2.2.0 — the MeshJob event-injection wave. Rolls up six feature PRs (cross-runtime point-to-point event injection + stream subscription) plus three docs/examples PRs into a stable release: - **MeshJob event injection** — `recv_event` / `send_event` / `post_event` across Python (#1041), TypeScript (#1043), Java (#1045). - **MeshJob stream subscription** — `subscribe_events` async generator (Python #1047, TS #1049) and `EventSubscription` Closeable iterator (Java #1051). - **Docs + examples** — concepts + env vars + meshctl man + release notes (#1053), signature-drift follow-up (#1056), runnable event-injection example agents across all three runtimes (#1058). See `RELEASE_NOTES.md` for the per-feature narrative grouping all six feature PRs. ## Mechanical changes in this PR - `scripts/bump_version.py 2.1.0 2.2.0` — 417 files updated across 36 categories (Cargo manifests, pyproject.toml, package.json, helm charts, scaffold templates, man content, test config, release workflow versions). - `helm dependency update helm/mcp-mesh-core` — Chart.lock regenerated. - `cargo generate-lockfile` (src/runtime/core) — Cargo.lock refreshed. - `RELEASE_NOTES.md` — `## v2.2.0 (UNRELEASED)` → `## v2.2.0 (2026-05-19)`, top-of-file Full Changelog link bumped to `v2.2.0...HEAD`, new `v2.1.0...v2.2.0` link added above the v2.2.0 heading. Net diff: 398 files changed, +613 / -611. Mostly one-line version bumps. Closes #1059 ## Test plan - [x] Dry-run `bump_version.py` matched expected scope (537 files / 36 categories) - [x] No stray `2.1.0` mcp-mesh-internal references left after bump (sanity grep — only transitive npm-dep matches remain) - [x] `helm dependency update` + `cargo generate-lockfile` reminders followed - [x] All upstream feature PRs (#1041 → #1058) merged and validated end-to-end before this release was cut - [ ] **After merge**: tag `v2.2.0` pushed to origin (HOLD for user inspection of merged main) - [ ] **After tag**: publish workflow fired (HOLD for explicit user go-ahead) Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Summary
mesh.jobs.subscribeEvents(jobId, { types, after, longPollSecs }) -> AsyncGenerator<JobEvent>.recvEvent/postEventsurface shipped in ts: MeshJob event injection parity — recvEvent/sendEvent/postEvent #1043. Multiple subscribers can observe the same job's events independently via per-call cursors.JsJobProxy::list_eventsreturningListEventsResult { events, next_after }(napi-rs v3 doesn't auto-serialize bare tuples), TS SDKsubscribeEventsasync generator reusing the existing_getOrCreateProxyLRU cache, integration test tc27 inuc22_meshjob_tsextending the long-task fixtures withrun_until_done(producer) andcommission_subscribe_observer(consumer).jobs.ts:187(${registryUrl}\x00${jobId}→${registryUrl} ${jobId}) inherited from an earlier PR that made git classify the file as binary in diffs. This also explains theBin -> Binline in the file stat —git diff -ashows the actual textual diff.Review Notes
Independent review caught 4 substantive WARNINGs (all addressed in the amended commit):
bigint→numbercursor truncation: TSDoc acceptedbigintfor i64-sourced cursors, butNumber(after)silently truncated values above2^53. Now guarded with an explicitRangeErrorwhenafter > BigInt(Number.MAX_SAFE_INTEGER).seqkey" malformed-payload test: Python sibling had this test; TS now has parity (rejects event without seq key)..catch(() => {})only suppresses the unhandled rejection — it does NOT cancel the underlying long-poll (JS has no cancellation primitive withoutAbortControllerplumbed through napi). Comment rewritten to be honest. PlumbingAbortControllerthrough napi is a known limitation left out of scope.seq=1independently — the actual headline contract.Other findings (3 INFOs) skipped as cosmetic or already-intentional patterns.
Closes #1048
Sibling Java vertical of subscription mode will follow as a separate PR, mirroring the #1043 → #1045 cadence.
Test plan
jobs.spec.ts: 23 tests, +7 new in this PR — 6 from initial implementation + 1 from review fix)src-tests12/12 passparse_timeout_secsguard reused at napi boundary (NaN/Inf/negative reject)JobProxycache reused from ts: MeshJob event injection parity — recvEvent/sendEvent/postEvent #1043 (no duplicate cache)typesfilter forwarded all the way to the registry (no client-side post-filter)nextAfterwatermark advances cursor on empty pages (parity with Python MeshJob: Stream subscription — subscribe_events (Python) #1047)seqand missing-seqrejection (parity with PythonRuntimeErrorshape)Summary by CodeRabbit
Release Notes
New Features
subscribeEvents()function to the jobs namespace for real-time event subscription with optional event type filtering and long-polling supportTests