feat(rawcan): callback-style Subscribe(onNext, predicate) - #15
Conversation
…ence API A colleague asked for a way to subscribe to incoming CAN frames with a callback instead of driving the ISubscription.Frames async-enumerable manually. Rather than a bigger cross-protocol unification (the protocol layers already have their own event-based APIs -- J1939's MessageReceived, ISO-TP's DatagramReceived, CANopen's PDO/SDO events -- and unifying those under one generic signature would either duplicate them for one line of savings or flatten their typed payloads to a lowest common denominator), this adds a small, additive-only extension method scoped to the raw-CAN layer, where the underlying complexity (per-subscription bounded buffering, backpressure) is real enough to be worth hiding. CanBusServiceExtensions.Subscribe(onNext, predicate, bufferCapacity) is built entirely on the existing ISubscription pull API, so it inherits the same FR-RAW-011 guarantee for free: a slow onNext only falls behind and drops its own subscription's oldest frames -- it can never delay delivery to other subscriptions or to the bus's own FrameObserved event, because the dispatch hot path never waits on a subscriber's consumer. A throwing onNext is isolated per frame and routed through the existing BackgroundExceptionOccurred fault channel via a new small internal CanBusService.RaiseBackgroundException helper (also used to de-duplicate the identical inline try/catch OnFrameObserved already had). Four new tests cover: filtering, the slow-handler-does-not-block invariant (mirroring the existing raw-subscription FR-RAW-011 test), Dispose stopping delivery, and handler-exception routing + delivery continuing. Public API surface changed (new type in CanKit.Pro.RawCan) -- approval baseline updated in the same commit. Verified: full build + 414/414 tests green on net10.0.
PR SummaryLow Risk Overview Dispose completes the underlying subscription, uses an Public API approval baseline is updated; tests cover filtering, non-blocking slow handlers, dispose semantics (including in-handler dispose), exception surfacing, and continued delivery. Reviewed by Cursor Bugbot for commit fa54874. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Dispose from callback hangs two seconds
- CallbackSubscription.Dispose now skips the pump-task join when invoked from onNext, so unsubscribe-from-handler no longer deadlocks until the two-second timeout.
You can send follow-ups to the cloud agent here.
…Next CallbackSubscription.Dispose waited on the pump task that invokes onNext, so disposing the handle from inside the callback deadlocked until the two-second timeout. Skip that join when Dispose runs on the pump, matching ProtocolActor.
CodeQL flagged it (cs/local-not-disposed) on the PR. Genuine gap, not a by-design pattern like the rest of this session's CodeQL triage.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Self-dispose still delivers buffered frames
- The callback pump now checks the disposed flag around onNext so a self-dispose no longer drains remaining buffered frames after Dispose returns.
You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit e44f3cc. Configure here.
Completing the subscription channel does not drop already-buffered frames, so disposing from onNext skipped the pump join and still delivered the remainder after Dispose returned. Stop the pump on the disposed flag instead.
10.4.0 (merged via the routine dependency-bump PR #2) requires conventional-changelog-writer@9+, but @semantic-release/commit-analyzer, release-notes-generator and changelog (all pinned to older majors here) resolve conventional-changelog-writer@^8.0.0 -- so npm installs 8.4.0. The mismatch doesn't surface at plugin/preset resolution time (which is all eng/verify-release-config.mjs checks, and all a PR build can check without push-rights), only when generateNotes actually renders a changelog template against a real commit history -- which only happens on a genuine release attempt on main. That's exactly what just failed after merging PR #15 (a feat: commit, the first release-triggering commit since #2 landed): "Missing helper: conventional-changelog-writer requires @9 or newer" (Handlebars helper missing from the older writer). No release side effects had happened yet (generateNotes runs before prepare/publish/tag) -- the run failed clean, nothing to unwind. Reverting to the last known-good 9.3.1 is the safe immediate fix. Re-attempting the writer@9+ upgrade later needs @semantic-release/ commit-analyzer, release-notes-generator and changelog bumped together with it, and should be verified with a real `GITHUB_TOKEN=$(gh auth token) npx semantic-release --dry-run --no-ci` locally (not just eng/verify-release-config.mjs, which only checks resolvability, not template rendering) -- that's exactly what confirmed this fix: generateNotes now completes and prints the real release notes.
## [1.1.0](v1.0.0...v1.1.0) (2026-09-09) ### Features * **rawcan:** add callback-style Subscribe(onNext, predicate) convenience API ([f3bb3e8](f3bb3e8)) ### Bug Fixes * **ci:** resolve release-config presets via import.meta.resolve ([e30423d](e30423d)) * **ci:** use a bypass-eligible PAT for the release push ([03e0cd9](03e0cd9)) * **rawcan:** skip pump join when disposing callback Subscribe from onNext ([be23113](be23113)) * **rawcan:** stop callback pump after self-dispose ([fa54874](fa54874)) * **release:** revert conventional-changelog-conventionalcommits to 9.3.1 ([1910026](1910026)), closes [#2](#2) [#15](#15) [#2](#2) ### Build & Dependencies * **deps:** Bump conventional-changelog-conventionalcommits ([#2](#2)) ([41af4e6](41af4e6)) * **deps:** Bump coverlet.collector and 2 others ([#8](#8)) ([b05ae9c](b05ae9c)) * **deps:** Bump Microsoft.Bcl.AsyncInterfaces from 10.0.11 to 10.0.12 ([#9](#9)) ([67aee2c](67aee2c)) * **deps:** Bump System.Memory from 4.5.4 to 4.6.3 ([#10](#10)) ([c15225a](c15225a)) * **deps:** Bump System.Threading.Channels from 10.0.11 to 10.0.12 ([#11](#11)) ([50db23c](50db23c)) * move to .NET 10 LTS ahead of net8.0 EOL ([cad2962](cad2962))

Summary
CanBusServiceExtensions.Subscribe(onNext, predicate, bufferCapacity): a small, additive-only extension method for subscribers who want a callback instead of drivingISubscription.Framesthemselves.onNextonly falls behind and drops its own subscription's oldest frames, never delaying other subscriptions or the bus's ownFrameObservedevent.onNextis isolated per frame and routed through the existingBackgroundExceptionOccurredfault channel (small internalCanBusService.RaiseBackgroundExceptionhelper added, also de-duplicates the identical inline try/catchOnFrameObservedalready had).Test plan
CanKit.Pro.RawCan) -- approval baseline updated in the same commitdotnet build+dotnet test-- 414/414 passing