Skip to content

refactor(gateway): extract source plugin trait and resolve_common()#161

Merged
yordis merged 5 commits into
TrogonStack:mainfrom
enilsen16:plugin-system-dev
May 28, 2026
Merged

refactor(gateway): extract source plugin trait and resolve_common()#161
yordis merged 5 commits into
TrogonStack:mainfrom
enilsen16:plugin-system-dev

Conversation

@enilsen16

@enilsen16 enilsen16 commented May 14, 2026

Copy link
Copy Markdown
Contributor

refactor(gateway): introduce SourcePlugin trait for uniform source dispatch

Eliminates the per-source iteration repeated between http.rs::mount_sources and streams.rs::provision by introducing a gateway-internal SourcePlugin trait. Each webhook source has a unit-struct plugin (GithubPlugin, SlackPlugin, ..., MicrosoftGraphPlugin) that owns both its JetStream provisioning loop and its HTTP route mounting loop, including any per-source edge cases.

Two dispatch entry points — provision_webhook_sources and mount_webhook_sources — replace the repeated source-by-source dispatch blocks. Adding a new webhook source now touches one file (source_plugin.rs) instead of two.

Notes:

  • SlackPlugin::mount skips socket-mode-only integrations (no webhook config); the socket-mode runners are spawned in main.rs.
  • Discord is intentionally excluded from SourcePlugin — its primary path is a WebSocket gateway runner, not a webhook receiver. streams.rs still provisions it inline.
  • MicrosoftGraphPlugin covers the source added on main since this branch was originally opened.

All 579 gateway tests pass; full workspace tests pass.


Note: This branch was rebuilt on current main after main independently absorbed the original PR's resolve_common helper (now resolve_common_integration_fields in config.rs) and added the Vec<SourceIntegration<_>> model + mount_webhook_integrations helper. The remaining value over main is the uniform iteration via the SourcePlugin trait, which lets new sources land in one file. Original commit before rebuild: dc07fc2.

@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Introduces a SourcePlugin abstraction with ten concrete implementations for webhook sources, centralizes provisioning and HTTP mounting dispatch functions, and refactors gateway initialization to delegate source-specific logic to these dispatchers.

Changes

SourcePlugin abstraction and gateway integration

Layer / File(s) Summary
SourcePlugin trait and plugin structs
rsworkspace/crates/trogon-gateway/src/source_plugin.rs (lines 1–46)
Adds SourceId alias and SourcePlugin trait with id(), path_prefix(), provision(), and mount() methods; declares zero-sized plugin structs for GitHub, Slack, Telegram, Twitter, GitLab, Incident.io, Linear, Microsoft Graph, Notion, and Sentry. Module docs explicitly exclude Discord and note Slack's special webhook filtering.
Shared helpers for provisioning and mounting
rsworkspace/crates/trogon-gateway/src/source_plugin.rs (lines 47–88)
Introduces private provision_integrations() and mount_integrations() helpers that iterate configured integrations to provision JetStream streams and nest per-integration routers under computed paths while emitting per-integration tracing logs.
Per-source plugin implementations
rsworkspace/crates/trogon-gateway/src/source_plugin.rs (lines 90–361)
Implements SourcePlugin for all ten sources; GitHub, Telegram, Twitter, GitLab, Incident.io, Linear, Microsoft Graph, Notion, and Sentry delegate both provision() and mount() to the shared helpers. Slack implements special handling in mount() to skip integrations without webhook config while delegating provision().
Provision and mount dispatch entry points
rsworkspace/crates/trogon-gateway/src/source_plugin.rs (lines 363–401)
Adds provision_webhook_sources() to sequentially invoke all plugin provision() methods in fixed order (GitHub through Sentry), and mount_webhook_sources() to sequentially apply all plugin mount() methods to nest routers under their source IDs, cloning the publisher for each mount step.
Gateway initialization refactor
rsworkspace/crates/trogon-gateway/src/http.rs (lines 2–22), rsworkspace/crates/trogon-gateway/src/streams.rs (lines 5–13), rsworkspace/crates/trogon-gateway/src/main.rs (lines 14–15)
http.rs removes inline webhook mounting and the mount_webhook_integrations helper, delegating to mount_webhook_sources(); streams.rs removes per-source provisioning loops, provisioning only Discord directly then delegating to provision_webhook_sources(); main.rs adds mod source_plugin; declaration with coverage dead-code allowance.

Sequence Diagram(s)

sequenceDiagram
  participant GatewayInit
  participant ProvisionDispatch as provision_webhook_sources()
  participant Plugin as SourcePlugin
  participant JetStream
  participant MountDispatch as mount_webhook_sources()
  participant Router as axum::Router

  GatewayInit->>ProvisionDispatch: (client, config)
  loop each plugin in [GitHub...Sentry]
    ProvisionDispatch->>Plugin: provision(client, config)
    Plugin->>JetStream: provision_integrations(callback, config)
  end
  
  GatewayInit->>MountDispatch: (base_router, publisher, config)
  loop each plugin in [GitHub...Sentry]
    MountDispatch->>Plugin: mount(router, publisher, config)
    Plugin->>Router: mount_integrations(mount_fn, config)
    Router->>Router: nest("/sources/{id}/{integration}/webhook", ...)
  end
  MountDispatch-->>GatewayInit: fully_nested_router
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • TrogonStack/trogonai#124: Modifies mount_sources Sentry webhook routing; main PR refactors all per-source mounting through source_plugin dispatch instead of inlining.
  • TrogonStack/trogonai#156: Adds integration-ID-driven webhook routing under /sources/<source>/{integration}/webhook; main PR extracts that logic into the reusable SourcePlugin abstraction.
  • TrogonStack/trogonai#175: Conditionally skips Slack webhook route mounting based on socket_mode config; main PR refactors Slack mounting through source_plugin with its own skip logic for missing webhook configs.

Poem

🐰 Ten sources dance in plugins now,
No more inline, we show you how!
Provision, mount—each takes its place,
From gateway's heart to webhook's grace.
Abstraction blooms, the code runs clean! 🌱

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main refactoring work: introducing a SourcePlugin trait and centralizing source dispatch logic, making it clear and directly related to the primary changeset objective.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the SourcePlugin trait introduction, per-source plugin implementations, dispatch entry points, edge cases, and testing status.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@enilsen16 enilsen16 marked this pull request as ready for review May 14, 2026 20:58
@cursor

cursor Bot commented May 14, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Mechanical refactor that preserves existing routes, provisioning, and Slack/Discord edge cases; gateway tests cover mounting and stream creation.

Overview
Introduces a gateway-internal SourcePlugin trait so JetStream provisioning and HTTP mounting for each webhook source live in one place (source_plugin.rs) instead of duplicated loops in http.rs and streams.rs.

mount_sources now only registers health routes and delegates to mount_webhook_sources; the old mount_webhook_integrations helper and per-source blocks are removed. streams::provision still provisions Discord inline (WebSocket path, not a plugin) and calls provision_webhook_sources for the rest.

Per-source behavior is preserved: SlackPlugin::mount still skips integrations without webhook config; shared helpers provision_integrations / mount_integrations cover the standard sources including Microsoft Graph.

Reviewed by Cursor Bugbot for commit f1cb201. Bugbot is set up for automated code reviews on this repo. Configure here.

@enilsen16 enilsen16 force-pushed the plugin-system-dev branch from a9ebf3e to dc07fc2 Compare May 14, 2026 21:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
rsworkspace/crates/trogon-gateway/src/source_plugin.rs (1)

435-435: 💤 Low value

Remove unnecessary clone on the final mount_one call.

The last call to mount_one clones publisher, but since this is the final use, you can move it instead. This avoids an unnecessary clone operation.

♻️ Proposed fix
-    mount_one(&SentryPlugin, router, publisher.clone(), resolved)
+    mount_one(&SentryPlugin, router, publisher, resolved)
🤖 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 `@rsworkspace/crates/trogon-gateway/src/source_plugin.rs` at line 435, The
final call to mount_one(&SentryPlugin, router, publisher.clone(), resolved)
performs an unnecessary clone of publisher; change this call to move the
publisher (mount_one(&SentryPlugin, router, publisher, resolved)) since this is
the last use—ensure no subsequent code expects publisher afterwards, and if
earlier mounts needed clones keep those as-is (only remove .clone() on the final
mount_one invocation).
🤖 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.

Inline comments:
In `@rsworkspace/crates/trogon-gateway/src/config.rs`:
- Around line 481-531: The resolve_common function bundles four distinct
conversions (subject_prefix via NatsToken::new, stream_name via NatsToken::new,
nats_ack_timeout via NonZeroDuration::from_secs, and stream_max_age via
StreamMaxAge::from_secs) and should be split into small per-field resolver
functions (e.g., resolve_subject_prefix, resolve_stream_name,
resolve_nats_ack_timeout, resolve_stream_max_age) that each return Result<... ,
ConfigValidationError> or Option<...> and push errors into the shared errors
vec; then have resolve_common (or the callers of ResolvedCommon) compose those
per-field resolvers to construct ResolvedCommon so validation is isolated by
type and easier to test/maintain.

---

Nitpick comments:
In `@rsworkspace/crates/trogon-gateway/src/source_plugin.rs`:
- Line 435: The final call to mount_one(&SentryPlugin, router,
publisher.clone(), resolved) performs an unnecessary clone of publisher; change
this call to move the publisher (mount_one(&SentryPlugin, router, publisher,
resolved)) since this is the last use—ensure no subsequent code expects
publisher afterwards, and if earlier mounts needed clones keep those as-is (only
remove .clone() on the final mount_one invocation).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d13776bb-abb5-4e2f-b89b-a838d2651d70

📥 Commits

Reviewing files that changed from the base of the PR and between 0f13a50 and a9ebf3e.

📒 Files selected for processing (6)
  • PLUGIN_SYSTEM_PLAN.md
  • rsworkspace/crates/trogon-gateway/src/config.rs
  • rsworkspace/crates/trogon-gateway/src/http.rs
  • rsworkspace/crates/trogon-gateway/src/main.rs
  • rsworkspace/crates/trogon-gateway/src/source_plugin.rs
  • rsworkspace/crates/trogon-gateway/src/streams.rs

Comment thread rsworkspace/crates/trogon-gateway/src/config.rs Outdated
@yordis

yordis commented May 16, 2026

Copy link
Copy Markdown
Member

@enilsen16 you have some merge conflicts and unresolved comments 😢

@enilsen16 enilsen16 force-pushed the plugin-system-dev branch from dc07fc2 to 847d1e5 Compare May 26, 2026 23:17

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 847d1e5. Configure here.

Comment thread rsworkspace/crates/trogon-gateway/src/source_plugin.rs
@enilsen16 enilsen16 force-pushed the plugin-system-dev branch from 044e396 to 624bdac Compare May 27, 2026 19:52
enilsen16 added 3 commits May 27, 2026 12:52
…spatch

Eliminates the per-source iteration repeated between `http.rs::mount_sources`
and `streams.rs::provision` by introducing a gateway-internal `SourcePlugin`
trait. Each webhook source has a unit-struct plugin (GithubPlugin,
SlackPlugin, ..., MicrosoftGraphPlugin) that owns both its JetStream
provisioning loop and its HTTP route mounting loop, including any per-source
edge cases.

Two dispatch entry points -- `provision_webhook_sources` and
`mount_webhook_sources` -- replace the repeated source-by-source dispatch
blocks. Adding a new webhook source now touches one file (source_plugin.rs)
instead of two.

SlackPlugin's `mount` skips socket-mode-only integrations (no webhook
config) since their HTTP route would never receive traffic; the runner is
spawned in main.rs. Discord is intentionally excluded from `SourcePlugin`
because its primary path is a WebSocket gateway runner, not a webhook
receiver -- streams.rs still provisions it inline.

All 579 gateway tests pass; full workspace tests pass.

Signed-off-by: Erik Nilsen <enilsen16@live.com>
Collapses the per-source iteration that was repeated in every non-Slack
`SourcePlugin` impl into two shared helpers:

- `provision_integrations` — `AsyncFn`-bounded helper iterating
  `Vec<SourceIntegration<T>>` and calling each source's `provision` per
  integration.
- `mount_integrations` — closure helper iterating the same and nesting
  each integration's router at `{path_prefix}/{id}`.

Each non-Slack `provision` impl is now a single call to
`provision_integrations(...)`; each non-Slack `mount` impl is a single
call to `mount_integrations(...)`. The iteration / nesting / logging
pattern lives in one place instead of nine. `SlackPlugin::mount` stays
hand-written because its socket-mode webhook filter is a per-source
behavior that the helper deliberately doesn't generalize.

This addresses the Cursor Bugbot finding on the prior commit ("10
identical copies"). Restores the closure-helper pattern that existed on
main (`mount_webhook_integrations`) and adds the symmetric helper for
provisioning that main was missing.

All 579 gateway tests pass.

Signed-off-by: Erik Nilsen <enilsen16@live.com>
coverage-action with publish: true always tries to push to _xml_coverage_reports,
but fork PRs get a read-only GITHUB_TOKEN regardless of the permissions block.
Gate publish on push events so fork PRs skip the write step entirely.
pull-requests: write is kept for PR annotations.

Signed-off-by: Erik Nilsen <enilsen16@live.com>
@enilsen16 enilsen16 force-pushed the plugin-system-dev branch from 624bdac to 9bfac32 Compare May 27, 2026 19:53
Comment thread .github/workflows/ci-rust.yml Outdated
enilsen16 added 2 commits May 27, 2026 15:38
Reverts the conditional publish expression per yordis's review.

Signed-off-by: Erik Nilsen <enilsen16@live.com>
Signed-off-by: Erik Nilsen <enilsen16@live.com>
@enilsen16 enilsen16 force-pushed the plugin-system-dev branch from 933a0e9 to f1cb201 Compare May 27, 2026 22:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@rsworkspace/crates/trogon-gateway/src/source_plugin.rs`:
- Around line 47-62: The generic bound on provision_integrations is using the
unsupported `AsyncFn(...) -> ...` form; update the function signature so the
closure type F is declared with the stable async bound syntax: change the where
clause to `F: async Fn(&C, &T) -> Result<(), C::Error>` (keeping the other
bounds `C: JetStreamContext`) so the async callable is properly typed when
calling `provision_fn(client, &integration.config).await` inside
provision_integrations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 77a1f2cb-e8ed-4165-99e2-65bcc543a02a

📥 Commits

Reviewing files that changed from the base of the PR and between 044e396 and f1cb201.

📒 Files selected for processing (4)
  • rsworkspace/crates/trogon-gateway/src/http.rs
  • rsworkspace/crates/trogon-gateway/src/main.rs
  • rsworkspace/crates/trogon-gateway/src/source_plugin.rs
  • rsworkspace/crates/trogon-gateway/src/streams.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • rsworkspace/crates/trogon-gateway/src/streams.rs
  • rsworkspace/crates/trogon-gateway/src/main.rs

Comment on lines +47 to +62
async fn provision_integrations<C, T, F>(
integrations: &[SourceIntegration<T>],
source: SourceId,
client: &C,
provision_fn: F,
) -> Result<(), C::Error>
where
C: JetStreamContext,
F: AsyncFn(&C, &T) -> Result<(), C::Error>,
{
for integration in integrations {
provision_fn(client, &integration.config).await?;
info!(source, integration = integration.id.as_str(), "stream provisioned");
}
Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Rust AsyncFn trait bound syntax stabilized 1.85

💡 Result:

In Rust 1.85.0, the AsyncFn, AsyncFnMut, and AsyncFnOnce traits were stabilized as part of the support for async closures [1][2][3]. However, the specific "async fn" trait bound syntax—such as using where F: async Fn() as a shorthand in trait bounds—was not stabilized in Rust 1.85.0 [4][5]. Instead, these traits are used explicitly in bounds (e.g., where F: AsyncFn<()>) [2][3]. The "async" modifier in trait bounds remains gated behind the async_trait_bounds feature flag [4]. Key details regarding this stabilization include: - Async Closures: Rust 1.85.0 introduced async closures (e.g., async || {}) and the corresponding AsyncFn* trait hierarchy, which allows for futures that can borrow from the closure's environment [6][3]. - Trait Availability: The AsyncFn, AsyncFnMut, and AsyncFnOnce traits were added to the standard library prelude in Rust 1.85.0, making them available for use without explicit imports [1][5]. - Bound Syntax: While the traits themselves are stable, the shorthand async Fn() syntax for bounds was explicitly excluded from the 1.85.0 release to avoid blocking the stabilization of async closures while RFCs regarding the exact semantics of that bound modifier (such as whether it implies Future or IntoFuture) continued to be discussed [4][5][7].

Citations:


🌐 Web query:

Rust 1.85 std::ops::AsyncFn trait bound syntax F: AsyncFn(&C, &T) -> ... exact form

💡 Result:

In Rust 1.85, the correct syntax for using the AsyncFn trait as a bound is the async modifier applied to the standard Fn trait syntax, such as async Fn(&C, &T) -> R [1][2]. While the underlying standard library trait is named std::ops::AsyncFn [3][4], the language provides a special "async" bound modifier that allows you to write trait bounds as async Fn(...) [1]. This desugars to the AsyncFn trait hierarchy [1]. The exact form for the bound you requested is: F: async Fn(&C, &T) -> R Key details regarding this syntax: 1. Bound Modifier: The async keyword acts as a bound modifier on the parenthesized Fn trait bound [1]. 2. Desugaring: Writing F: async Fn(...) is the idiomatic way to bound a generic type to the AsyncFn family of traits [1][2]. The compiler handles the mapping to AsyncFn, AsyncFnMut, or AsyncFnOnce based on the usage [5][2]. 3. Why this syntax: This approach resolves limitations in previous versions of Rust where it was difficult to properly express higher-ranked function signatures (HRTBs) that return futures, especially when those futures need to borrow from the closure's captures [5][2]. The trait std::ops::AsyncFn is part of the standard library prelude as of Rust 1.85 [6][7], enabling first-class support for async closures [2].

Citations:


🌐 Web query:

std::ops::AsyncFn trait definition associated types how to write bounds in trait bound list

💡 Result:

The AsyncFn trait is an async-aware version of the standard Fn trait, defined in std::ops [1][2]. Its definition includes a method async_call, which returns a Generic Associated Type (GAT) named CallRefFuture [1][2]. Definition of AsyncFn: pub trait AsyncFn: AsyncFnMut where Args: Tuple { extern "rust-call" fn async_call(&self, args: Args) -> Self::CallRefFuture<'>; } Regarding Associated Types and Bounds: The associated types of the AsyncFn* traits (such as CallRefFuture, CallMutFuture, and CallOnceFuture) are considered implementation details and are generally not nameable by users in stable Rust [3]. Consequently, you cannot directly reference or place trait bounds on these associated types (e.g., you cannot write something like F: AsyncFn<CallRefFuture: Send>) [3]. Instead, the AsyncFn traits are intended to be used via the async bound modifier, which allows you to express trait bounds on functions and closures that return futures [3]. The syntax async Fn* -> T desugars to an AsyncFn* trait bound [3]. If you need to place specific bounds on the futures returned by an async function—for instance, requiring that the returned future is Send—the recommended approach in Rust is to use Return-Position Impl Trait in Trait (RPITIT) [4][5]. By defining a trait method that returns impl Future + Send + ', you can explicitly enforce such bounds [4][5]. Summary of Key Points: 1. Associated Type Privacy: The associated types of AsyncFn are not part of the stable public interface and cannot be named directly [3]. 2. Trait Bound Syntax: To bound functions that return futures, use the async Fn* syntax (e.g., F: async Fn -> Output) [3]. 3. Constraining Returned Futures: To add bounds (like Send) to the future returned by a trait method, define the method to return impl Future<Output = T> + Send + '_ [4][5].

Citations:


Fix AsyncFn bound syntax in provision_integrations

In Rust 1.85, the stable syntax for bounding an async callable is F: async Fn(&C, &T) -> R (it desugars to the std::ops::AsyncFn* traits). The current bound F: AsyncFn(&C, &T) -> Result<(), C::Error> doesn’t use the required async bound modifier and is not the supported stable form—switch it to F: async Fn(&C, &T) -> Result<(), C::Error>.

🤖 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 `@rsworkspace/crates/trogon-gateway/src/source_plugin.rs` around lines 47 - 62,
The generic bound on provision_integrations is using the unsupported
`AsyncFn(...) -> ...` form; update the function signature so the closure type F
is declared with the stable async bound syntax: change the where clause to `F:
async Fn(&C, &T) -> Result<(), C::Error>` (keeping the other bounds `C:
JetStreamContext`) so the async callable is properly typed when calling
`provision_fn(client, &integration.config).await` inside provision_integrations.

@enilsen16

Copy link
Copy Markdown
Contributor Author

Re: yordis comment on ci-rust.yml — that change was on a superseded commit (9bfac32). The branch was rebuilt onto current main and ci-rust.yml was dropped from the diff. The current PR contains no changes to that file, so there is nothing to revert.

@enilsen16

Copy link
Copy Markdown
Contributor Author

Re: CodeRabbit comment on provision_integrations AsyncFn syntax (comment 3314243483):

The current bound F: AsyncFn(&C, &T) -> Result<(), C::Error> is correct for stable Rust 1.85. AsyncFn was stabilized as a trait in std::ops in 1.85 and uses the same parenthesized sugar syntax as Fn/FnMut/FnOnce. The async Fn(...) bound-modifier syntax (i.e. F: async Fn(&C, &T)) is not stable — it is still behind #![feature(async_trait_bounds)] on nightly.

Re: unnecessary .clone() nitpick: already addressed — the dispatch loop in mount_webhook_sources uses publisher.clone() for the first nine plugins and passes publisher by move on the final SentryPlugin.mount call.

@yordis yordis merged commit 9863343 into TrogonStack:main May 28, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants