webpatser/resonate
Resonate is a Laravel package for adding real-time event broadcasting and pub/sub messaging to your app. It offers a simple, expressive API for publishing and subscribing to events with minimal configuration, helping you build interactive features faster.
Architecture fit — This is a plugin surface on a Pusher-protocol websocket server. v0.4.0 completes a state machine that was previously one-sided: onSubscribe had no onUnsubscribe, and PluginContext::connectionsOn() (read) had no write counterpart. The practical consequence is that the plugin layer can now express the full connection↔channel lifecycle rather than approximating it with socket-level events. The most valuable addition is PluginContext::unsubscribe(Connection, string $channel): per-channel eviction without terminating the socket. That is the primitive most real plugin use cases need — entitlement revocation, moderation, channel-level access changes — and previously the only workaround was force-disconnect plus client reconnect, which is a sledgehammer.
Integration feasibility — Straightforward but breaking. ConnectionLifecycle goes from three methods to four; any existing implementer fails to compile until it adds onUnsubscribe. Blast radius is small because the plugin system is one release old, but "small" is an assumption to verify, not a fact.
Technical risk — Three concerns, in order:
PluginContext::unsubscribe() is explicitly a direct action that does not emit onUnsubscribe. This is defensible (avoids reentrancy and self-triggered loops), but it means "connection left a channel" has two notification paths with different guarantees. Any plugin that treats onUnsubscribe as a complete audit log of channel departures will silently miss plugin-initiated evictions. This will be the single most common integration bug.onClose does not fan out unsubscribes. Documented and correct — a close reports once via onClose, not N times. But a plugin maintaining a channel-membership index must treat onClose as an implicit bulk unsubscribe. Whether that is possible depends on whether Connection still exposes its channel list at onClose time; if channels are cleared before the hook fires, plugins cannot reconcile and will leak stale membership. This is the key question to resolve before committing.EventHandler::unsubscribe(). The hook fires after removal, so the hook observes post-state. Good for consistency, but combined with (1) it means a plugin reacting to onUnsubscribe sees the same state as a plugin that called unsubscribe() itself — while only one of them gets told. Confirm the ordering is contractual, not incidental.Exception isolation on notifyUnsubscribe() matches the existing fan-out contract, which is the right call: one bad plugin must not break the event loop. The residual risk is silent state drift rather than an outage, which is harder to detect — worth a log line per swallowed exception.
Key questions
ConnectionLifecycle ship with an abstract base class or trait providing a no-op onUnsubscribe, or must every implementer write the method? (If not, request one — it turns this from a break into a non-event.)Connection when onClose fires?unsubscribe() a no-op if the connection is not in the channel, or does it throw?PluginContext::unsubscribe() from within onSubscribe, and what happens if it targets the channel currently joining?onClose-vs-onUnsubscribe exclusivity, and exception isolation?Stack fit — Laravel package, so registration is via service provider; plugins implement the lifecycle interface and receive a PluginContext. Fits naturally where the broadcasting stack already uses the pusher driver and the team wants self-hosted, in-process extension points (audit, presence enrichment, authorization, moderation) rather than a separate sidecar.
Migration path — Pin to 0.4.x explicitly; do not float on ^0.3. Audit for implements ConnectionLifecycle across the app, in-house plugins, and any vendored forks. Each implementer needs a fourth method; in the absence of a shipped no-op default, add an empty onUnsubscribe as the minimal upgrade, then adopt real behavior deliberately.
Compatibility — Interface-level break only; runtime behavior for the existing three hooks is unchanged except that unsubscribe() now emits. Critically: do not migrate per-channel cleanup out of onClose into onUnsubscribe. Because close does not fan out unsubscribes, moving that logic would silently drop cleanup on disconnect. The correct pattern is "handle both paths, idempotently." Version skew is a hard failure, not a soft one — a plugin class compiled against the 0.3 interface will fatal at load, so server and plugins must deploy together.
Sequencing
onUnsubscribe to all implementers; deploy. This is the safe, behavior-neutral step and can ship independently.onClose channel-visibility question empirically with a throwaway plugin.PluginContext::unsubscribe() for one concrete, reversible use case (e.g. revoking a single channel on role change) and observe.onUnsubscribe consumers, and make them idempotent against onClose.Do not build a first-class product feature on unsubscribe() until the reentrancy and authorization questions are answered.
Maintenance — This is a 0.x package with an explicitly unsettled contract ("the contract is completed before it settles"). Expect further interface churn before 1.0; budget a review-and-adapt cycle per minor release. Pin exact versions and gate upgrades behind a changelog read. The good news is that the author is closing design gaps early rather than accumulating them, and is flagging the break loudly.
Support — Two documentation gaps will generate nearly all tickets: (a) "my plugin didn't get an onUnsubscribe when another plugin evicted the connection," and (b) "my per-channel state is stale after a disconnect." Both are consequences of intentional, correct design decisions that are non-obvious from the method signatures alone. Publish a one-page lifecycle contract before onboarding any plugin author.
Scaling — Net positive. Replacing "force disconnect + client resubscribe" with an in-process channel removal eliminates reconnect storms and resubscribe churn, which were the failure mode that made per-channel revocation expensive. The write path itself is a channel-membership removal, not a socket operation, so cost is negligible relative to the connection lifecycle it replaces.
Failure modes
onUnsubscribe → isolated, but the plugin's own membership view drifts. Needs a counter/log to be observable.unsubscribe() inside onSubscribe → possible loop or unexpected post-join state; validate before allowing.onUnsubscribe → leaks entries on every disconnect.onUnsubscribe and onClose for the same cleanup without idempotency → double-decrement.Ramp-up — Low to add the interface method (under an hour). Moderate to build correct membership tracking (a few days including tests), because correctness depends on the two asymmetry rules above. Recommend a written runbook for the "revoke one channel without dropping the socket" flow, since that is the capability this release exists to enable and the first team to exercise it will be defining the pattern for everyone else.
How can I help you explore Laravel packages today?