Skip to main content

phoenix_channel_runtime_native/
managed.rs

1use std::{
2    any::Any,
3    collections::{HashMap, HashSet, VecDeque},
4    future::Future,
5    panic::{AssertUnwindSafe, catch_unwind},
6    rc::Rc,
7    sync::{
8        Arc, Mutex,
9        atomic::{AtomicU64, Ordering},
10    },
11    thread::JoinHandle,
12    time::Duration,
13};
14
15use futures::{FutureExt, future::BoxFuture};
16use phoenix_channel_client::{
17    Channel, ChannelEvent, ChannelStatus, ClientError, ConnectContext, ConnectionConfig, Endpoint,
18    EndpointError, JoinContext, Options, PresenceEvent, ReconnectAction, ReconnectContext, Reply,
19    Socket, SocketEvent, SocketStatus, SubscriptionEvent, TelemetryEvent,
20};
21use phoenix_channel_runtime::{
22    Payload, PresenceError, PresenceState, PresenceTracker, PresenceUpdate, ProtocolEvent,
23};
24use serde::{Serialize, de::DeserializeOwned};
25use serde_json::Value;
26use thiserror::Error;
27use tokio::sync::{broadcast, mpsc, oneshot, watch};
28
29use crate::{NativeConnector, NativeTimer, NativeTransportOptions};
30
31/// Thread-safe async callback that refreshes connection configuration.
32pub type NativeConnectionConfigLoader = Arc<
33    dyn Fn(ConnectContext) -> BoxFuture<'static, Result<ConnectionConfig, String>> + Send + Sync,
34>;
35
36/// Thread-safe async callback that refreshes a channel join payload.
37pub type NativeJoinPayloadLoader =
38    Arc<dyn Fn(JoinContext) -> BoxFuture<'static, Result<Value, String>> + Send + Sync>;
39
40/// Thread-safe callback for managed-client telemetry.
41pub type NativeTelemetryHook = Arc<dyn Fn(&TelemetryEvent) + Send + Sync>;
42/// Thread-safe callback that decides whether and when to reconnect.
43pub type NativeReconnectPolicy = Arc<dyn Fn(ReconnectContext) -> ReconnectAction + Send + Sync>;
44
45/// Creates a native loader that clones the same connection configuration.
46pub fn native_static_connection_config(config: ConnectionConfig) -> NativeConnectionConfigLoader {
47    Arc::new(move |_| {
48        let config = config.clone();
49        async move { Ok(config) }.boxed()
50    })
51}
52
53/// Creates a native loader that clones the same join payload.
54pub fn native_static_join_payload(payload: Value) -> NativeJoinPayloadLoader {
55    Arc::new(move |_| {
56        let payload = payload.clone();
57        async move { Ok(payload) }.boxed()
58    })
59}
60
61/// Settings for a dedicated native client worker.
62#[derive(Clone)]
63pub struct NativeOptions {
64    heartbeat_interval: Duration,
65    heartbeat_timeout: Duration,
66    connect_timeout: Duration,
67    join_timeout: Duration,
68    call_timeout: Duration,
69    leave_timeout: Duration,
70    reconnect_delays: Vec<Duration>,
71    rejoin_delays: Vec<Duration>,
72    command_capacity: usize,
73    push_buffer_capacity: usize,
74    event_capacity: usize,
75    transport: NativeTransportOptions,
76    telemetry: Option<NativeTelemetryHook>,
77    reconnect_policy: Option<NativeReconnectPolicy>,
78}
79
80impl Default for NativeOptions {
81    fn default() -> Self {
82        let retry_delays = vec![
83            Duration::ZERO,
84            Duration::from_secs(1),
85            Duration::from_secs(2),
86            Duration::from_secs(5),
87            Duration::from_secs(10),
88        ];
89        Self {
90            heartbeat_interval: Duration::from_secs(30),
91            heartbeat_timeout: Duration::from_secs(30),
92            connect_timeout: Duration::from_secs(10),
93            join_timeout: Duration::from_secs(10),
94            call_timeout: Duration::from_secs(10),
95            leave_timeout: Duration::from_secs(10),
96            reconnect_delays: retry_delays.clone(),
97            rejoin_delays: retry_delays,
98            command_capacity: 64,
99            push_buffer_capacity: 64,
100            event_capacity: 256,
101            transport: NativeTransportOptions::default(),
102            telemetry: None,
103            reconnect_policy: None,
104        }
105    }
106}
107
108impl NativeOptions {
109    /// Sets the automatic heartbeat interval.
110    pub fn heartbeat_interval(mut self, value: Duration) -> Self {
111        self.heartbeat_interval = value;
112        self
113    }
114
115    /// Sets the automatic heartbeat acknowledgement timeout.
116    pub fn heartbeat_timeout(mut self, value: Duration) -> Self {
117        self.heartbeat_timeout = value;
118        self
119    }
120
121    /// Sets the maximum duration of a transport connection attempt.
122    pub fn connect_timeout(mut self, value: Duration) -> Self {
123        self.connect_timeout = value;
124        self
125    }
126
127    /// Sets join, call, and leave timeouts to one common value.
128    pub fn request_timeout(mut self, value: Duration) -> Self {
129        self.join_timeout = value;
130        self.call_timeout = value;
131        self.leave_timeout = value;
132        self
133    }
134
135    /// Sets the default join timeout.
136    pub fn join_timeout(mut self, value: Duration) -> Self {
137        self.join_timeout = value;
138        self
139    }
140
141    /// Sets the default call timeout.
142    pub fn call_timeout(mut self, value: Duration) -> Self {
143        self.call_timeout = value;
144        self
145    }
146
147    /// Sets the default leave timeout.
148    pub fn leave_timeout(mut self, value: Duration) -> Self {
149        self.leave_timeout = value;
150        self
151    }
152
153    /// Sets reconnect delays by attempt, retaining the final delay thereafter.
154    pub fn reconnect_delays(mut self, values: impl IntoIterator<Item = Duration>) -> Self {
155        self.reconnect_delays = normalized_delays(values);
156        self
157    }
158
159    /// Sets rejoin delays by attempt, retaining the final delay thereafter.
160    pub fn rejoin_delays(mut self, values: impl IntoIterator<Item = Duration>) -> Self {
161        self.rejoin_delays = normalized_delays(values);
162        self
163    }
164
165    /// Sets the bounded host-to-worker command capacity.
166    pub fn command_capacity(mut self, value: usize) -> Self {
167        self.command_capacity = value.max(1);
168        self
169    }
170
171    /// Sets how many calls may wait for a connection or join.
172    pub fn push_buffer_capacity(mut self, value: usize) -> Self {
173        self.push_buffer_capacity = value;
174        self
175    }
176
177    /// Sets each broadcast event channel's capacity.
178    pub fn event_capacity(mut self, value: usize) -> Self {
179        self.event_capacity = value.max(1);
180        self
181    }
182
183    /// Installs a thread-safe telemetry callback.
184    pub fn telemetry(mut self, hook: NativeTelemetryHook) -> Self {
185        self.telemetry = Some(hook);
186        self
187    }
188
189    /// Installs a thread-safe reconnect policy.
190    pub fn reconnect_policy(mut self, policy: NativeReconnectPolicy) -> Self {
191        self.reconnect_policy = Some(policy);
192        self
193    }
194
195    /// Replaces native WebSocket transport settings.
196    pub fn transport(mut self, options: NativeTransportOptions) -> Self {
197        self.transport = options;
198        self
199    }
200
201    fn client_options(&self) -> Options {
202        let reconnect_delays = self.reconnect_delays.clone();
203        let rejoin_delays = self.rejoin_delays.clone();
204        let mut options = Options::default()
205            .connect_on_start(false)
206            .heartbeat_interval(self.heartbeat_interval)
207            .heartbeat_timeout(self.heartbeat_timeout)
208            .connect_timeout(self.connect_timeout)
209            .join_timeout(self.join_timeout)
210            .call_timeout(self.call_timeout)
211            .leave_timeout(self.leave_timeout)
212            .event_capacity(self.event_capacity)
213            .reconnect_delay(move |attempt| retry_delay(&reconnect_delays, attempt))
214            .rejoin_delay(move |attempt| retry_delay(&rejoin_delays, attempt))
215            .command_capacity(self.command_capacity)
216            .push_buffer_capacity(self.push_buffer_capacity);
217        if let Some(hook) = &self.telemetry {
218            let hook = hook.clone();
219            options = options.telemetry(Rc::new(move |event| hook(event)));
220        }
221        if let Some(policy) = &self.reconnect_policy {
222            let policy = policy.clone();
223            options = options.reconnect_policy(move |context| policy(context));
224        }
225        options
226    }
227}
228
229fn normalized_delays(values: impl IntoIterator<Item = Duration>) -> Vec<Duration> {
230    let values = values.into_iter().collect::<Vec<_>>();
231    if values.is_empty() {
232        vec![Duration::ZERO]
233    } else {
234        values
235    }
236}
237
238fn retry_delay(delays: &[Duration], attempt: u32) -> Duration {
239    delays
240        .get(attempt as usize)
241        .or_else(|| delays.last())
242        .copied()
243        .unwrap_or(Duration::ZERO)
244}
245
246/// Failure from the native worker or managed client.
247#[derive(Debug, Error)]
248pub enum NativeRuntimeError {
249    /// The Phoenix endpoint was invalid.
250    #[error(transparent)]
251    Endpoint(#[from] EndpointError),
252    /// The managed client rejected or failed an operation.
253    #[error(transparent)]
254    Client(#[from] ClientError),
255    /// The dedicated worker thread or runtime could not start.
256    #[error("failed to start native client worker: {0}")]
257    WorkerStart(String),
258    /// The worker stopped before completing a host request.
259    #[error("native client worker stopped")]
260    WorkerStopped,
261    /// The worker terminated with the supplied failure message.
262    #[error("native client worker failed: {0}")]
263    WorkerFailed(String),
264    /// Joining the worker thread failed.
265    #[error("failed to join native client worker: {0}")]
266    WorkerJoin(String),
267}
268
269/// Failure from [`NativeChannel::call_json`].
270#[derive(Debug, Error)]
271pub enum NativeCallJsonError {
272    /// The native worker or managed call failed.
273    #[error(transparent)]
274    Runtime(#[from] NativeRuntimeError),
275    /// The request could not be serialized as JSON.
276    #[error("failed to encode request payload: {0}")]
277    Encode(serde_json::Error),
278    /// Phoenix rejected the call or its response could not be decoded.
279    #[error(transparent)]
280    Reply(#[from] phoenix_channel_client::ReplyError),
281}
282
283/// Lifecycle state of the dedicated native worker thread.
284#[derive(Clone, Debug, Eq, PartialEq)]
285pub enum NativeWorkerStatus {
286    /// The worker is accepting and processing commands.
287    Running,
288    /// The worker shut down normally.
289    Stopped,
290    /// The worker terminated with an error or panic message.
291    Failed(String),
292}
293
294/// Failure while consuming a native broadcast or status stream.
295#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
296pub enum NativeEventError {
297    /// The bounded broadcast receiver missed this many messages.
298    #[error("event receiver lagged by {0} messages")]
299    Lagged(u64),
300    /// All senders for the event or status stream were dropped.
301    #[error("event stream closed")]
302    Closed,
303}
304
305struct WorkerOwner {
306    commands: mpsc::Sender<HostCommand>,
307    control: mpsc::UnboundedSender<ControlCommand>,
308    next_request: AtomicU64,
309    worker: Mutex<Option<JoinHandle<()>>>,
310    status: watch::Receiver<NativeWorkerStatus>,
311}
312
313impl WorkerOwner {
314    fn next_request_id(&self) -> u64 {
315        let id = self.next_request.fetch_add(1, Ordering::Relaxed);
316        if id == 0 {
317            self.next_request.store(2, Ordering::Relaxed);
318            1
319        } else {
320            id
321        }
322    }
323
324    fn join(&self) -> Result<(), NativeRuntimeError> {
325        let Some(worker) = self
326            .worker
327            .lock()
328            .map_err(|error| NativeRuntimeError::WorkerJoin(error.to_string()))?
329            .take()
330        else {
331            return self.worker_result();
332        };
333        worker
334            .join()
335            .map_err(|panic| NativeRuntimeError::WorkerJoin(panic_message(panic.as_ref())))?;
336        self.worker_result()
337    }
338
339    fn worker_result(&self) -> Result<(), NativeRuntimeError> {
340        match self.status.borrow().clone() {
341            NativeWorkerStatus::Failed(message) => Err(NativeRuntimeError::WorkerFailed(message)),
342            NativeWorkerStatus::Running | NativeWorkerStatus::Stopped => Ok(()),
343        }
344    }
345}
346
347impl Drop for WorkerOwner {
348    fn drop(&mut self) {
349        let _ = self.control.send(ControlCommand::Shutdown);
350        if let Ok(worker) = self.worker.get_mut() {
351            if let Some(worker) = worker.take() {
352                let _ = worker.join();
353            }
354        }
355    }
356}
357
358/// `Send + Sync` handle for a managed client running on a dedicated thread.
359#[derive(Clone)]
360pub struct NativeSocket {
361    owner: Arc<WorkerOwner>,
362    events: broadcast::Sender<SocketEvent>,
363    status: watch::Receiver<SocketStatus>,
364}
365
366impl NativeSocket {
367    /// Starts a worker with static connection configuration and default options.
368    pub fn spawn(
369        endpoint: impl Into<String>,
370        config: ConnectionConfig,
371    ) -> Result<Self, NativeRuntimeError> {
372        Self::spawn_with_loader(
373            endpoint,
374            native_static_connection_config(config),
375            NativeOptions::default(),
376        )
377    }
378
379    /// Starts a worker with static connection configuration and custom options.
380    pub fn spawn_with_options(
381        endpoint: impl Into<String>,
382        config: ConnectionConfig,
383        options: NativeOptions,
384    ) -> Result<Self, NativeRuntimeError> {
385        Self::spawn_with_loader(endpoint, native_static_connection_config(config), options)
386    }
387
388    /// Starts a worker with per-attempt connection configuration.
389    pub fn spawn_with_loader(
390        endpoint: impl Into<String>,
391        config_loader: NativeConnectionConfigLoader,
392        options: NativeOptions,
393    ) -> Result<Self, NativeRuntimeError> {
394        let endpoint = endpoint.into();
395        Endpoint::new(&endpoint)?;
396        let (commands, command_rx) = mpsc::channel(options.command_capacity);
397        let (control, control_rx) = mpsc::unbounded_channel();
398        let worker_control = control.clone();
399        let (events, _) = broadcast::channel(options.event_capacity);
400        let (status_tx, status) = watch::channel(SocketStatus::Disconnected);
401        let (worker_status_tx, worker_status) = watch::channel(NativeWorkerStatus::Running);
402        let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1);
403        let worker_events = events.clone();
404        let worker = std::thread::Builder::new()
405            .name("phoenix-channel-runtime".into())
406            .spawn(move || {
407                let fallback_ready = ready_tx.clone();
408                let result = catch_unwind(AssertUnwindSafe(|| {
409                    run_worker(WorkerBootstrap {
410                        endpoint_url: endpoint,
411                        config_loader,
412                        options,
413                        commands: command_rx,
414                        control: control_rx,
415                        control_tx: worker_control,
416                        events: worker_events,
417                        status: status_tx,
418                        ready: ready_tx,
419                    })
420                }));
421                let final_status = match result {
422                    Ok(Ok(())) => NativeWorkerStatus::Stopped,
423                    Ok(Err(message)) => {
424                        let _ = fallback_ready.send(Err(message.clone()));
425                        NativeWorkerStatus::Failed(message)
426                    }
427                    Err(panic) => {
428                        let message = panic_message(panic.as_ref());
429                        let _ = fallback_ready.send(Err(message.clone()));
430                        NativeWorkerStatus::Failed(message)
431                    }
432                };
433                worker_status_tx.send_replace(final_status);
434            })
435            .map_err(|error| NativeRuntimeError::WorkerStart(error.to_string()))?;
436        match ready_rx.recv() {
437            Ok(Ok(())) => {}
438            Ok(Err(error)) => {
439                let _ = worker.join();
440                return Err(NativeRuntimeError::WorkerStart(error));
441            }
442            Err(error) => {
443                let _ = worker.join();
444                return Err(NativeRuntimeError::WorkerStart(error.to_string()));
445            }
446        }
447        Ok(Self {
448            owner: Arc::new(WorkerOwner {
449                commands,
450                control,
451                next_request: AtomicU64::new(1),
452                worker: Mutex::new(Some(worker)),
453                status: worker_status,
454            }),
455            events,
456            status,
457        })
458    }
459
460    /// Returns the latest managed socket status.
461    pub fn status(&self) -> SocketStatus {
462        *self.status.borrow()
463    }
464
465    /// Creates an independent bounded socket event receiver.
466    pub fn events(&self) -> NativeSocketEvents {
467        NativeSocketEvents {
468            receiver: self.events.subscribe(),
469        }
470    }
471
472    /// Creates a receiver for subsequent socket status changes.
473    pub fn status_changes(&self) -> NativeSocketStatusChanges {
474        NativeSocketStatusChanges {
475            receiver: self.status.clone(),
476        }
477    }
478
479    /// Returns the latest worker-thread status.
480    pub fn worker_status(&self) -> NativeWorkerStatus {
481        self.owner.status.borrow().clone()
482    }
483
484    /// Creates a receiver for subsequent worker-thread status changes.
485    pub fn worker_status_changes(&self) -> NativeWorkerStatusChanges {
486        NativeWorkerStatusChanges {
487            receiver: self.owner.status.clone(),
488        }
489    }
490
491    /// Enables connection attempts.
492    pub async fn connect(&self) -> Result<(), NativeRuntimeError> {
493        self.unit_request(|request_id, response| HostCommand::Connect {
494            request_id,
495            response,
496        })
497        .await
498    }
499
500    /// Sends a heartbeat with the configured call timeout and returns its RTT.
501    pub async fn ping(&self) -> Result<Duration, NativeRuntimeError> {
502        self.request(|request_id, response| HostCommand::Ping {
503            request_id,
504            timeout: None,
505            response,
506        })
507        .await
508    }
509
510    /// Sends a heartbeat with an explicit timeout and returns its RTT.
511    pub async fn ping_with_timeout(
512        &self,
513        timeout: Duration,
514    ) -> Result<Duration, NativeRuntimeError> {
515        self.request(|request_id, response| HostCommand::Ping {
516            request_id,
517            timeout: Some(timeout),
518            response,
519        })
520        .await
521    }
522
523    /// Disables automatic reconnection and closes the active transport.
524    pub async fn disconnect(&self) -> Result<(), NativeRuntimeError> {
525        self.unit_request(|request_id, response| HostCommand::Disconnect {
526            request_id,
527            response,
528        })
529        .await
530    }
531
532    /// Disconnects with an explicit WebSocket close code and reason.
533    pub async fn disconnect_with(
534        &self,
535        code: u16,
536        reason: impl Into<String>,
537    ) -> Result<(), NativeRuntimeError> {
538        let reason = reason.into();
539        self.unit_request(|request_id, response| HostCommand::DisconnectWith {
540            request_id,
541            code,
542            reason,
543            response,
544        })
545        .await
546    }
547
548    /// Registers a channel with a static join payload.
549    pub async fn channel(
550        &self,
551        topic: impl Into<String>,
552        payload: Value,
553    ) -> Result<NativeChannel, NativeRuntimeError> {
554        self.channel_with_loader(topic, native_static_join_payload(payload))
555            .await
556    }
557
558    /// Registers a channel with a payload loader evaluated for every join.
559    pub async fn channel_with_loader(
560        &self,
561        topic: impl Into<String>,
562        payload_loader: NativeJoinPayloadLoader,
563    ) -> Result<NativeChannel, NativeRuntimeError> {
564        let registration = self
565            .request(|request_id, response| HostCommand::Channel {
566                request_id,
567                topic: topic.into(),
568                payload_loader,
569                response,
570            })
571            .await?;
572        Ok(NativeChannel {
573            inner: Arc::new(NativeChannelInner {
574                id: registration.id,
575                topic: registration.topic,
576                owner: self.owner.clone(),
577                events: registration.events,
578                status: registration.status,
579            }),
580        })
581    }
582
583    /// Permanently stops and joins the dedicated worker.
584    pub async fn shutdown(&self) -> Result<(), NativeRuntimeError> {
585        let command_result = self
586            .unit_command(|response| HostCommand::Shutdown { response })
587            .await;
588        match self.owner.join() {
589            Err(error) => Err(error),
590            Ok(()) => command_result,
591        }
592    }
593
594    /// Blocks the current thread until the worker exits.
595    ///
596    /// Async callers should normally use [`NativeSocket::shutdown`] instead.
597    pub fn join_worker(&self) -> Result<(), NativeRuntimeError> {
598        self.owner.join()
599    }
600
601    async fn unit_request(
602        &self,
603        build: impl FnOnce(u64, oneshot::Sender<Result<(), ClientError>>) -> HostCommand,
604    ) -> Result<(), NativeRuntimeError> {
605        self.request(build).await
606    }
607
608    async fn unit_command(
609        &self,
610        build: impl FnOnce(oneshot::Sender<Result<(), ClientError>>) -> HostCommand,
611    ) -> Result<(), NativeRuntimeError> {
612        let (response, receiver) = oneshot::channel();
613        if self.owner.commands.send(build(response)).await.is_err() {
614            return Err(wait_for_worker_error(&self.owner).await);
615        }
616        let result = match receiver.await {
617            Ok(result) => result,
618            Err(_) => return Err(wait_for_worker_error(&self.owner).await),
619        };
620        result?;
621        Ok(())
622    }
623
624    async fn request<T: Send + 'static>(
625        &self,
626        build: impl FnOnce(u64, oneshot::Sender<Result<T, ClientError>>) -> HostCommand,
627    ) -> Result<T, NativeRuntimeError> {
628        request_worker(&self.owner, build).await
629    }
630}
631
632/// Independent bounded native socket event receiver.
633pub struct NativeSocketEvents {
634    receiver: broadcast::Receiver<SocketEvent>,
635}
636
637impl NativeSocketEvents {
638    /// Waits for the next socket event.
639    pub async fn next(&mut self) -> Result<SocketEvent, NativeEventError> {
640        receive_event(&mut self.receiver).await
641    }
642}
643
644/// Receiver for managed socket status changes.
645pub struct NativeSocketStatusChanges {
646    receiver: watch::Receiver<SocketStatus>,
647}
648
649/// Receiver for dedicated worker-thread status changes.
650pub struct NativeWorkerStatusChanges {
651    receiver: watch::Receiver<NativeWorkerStatus>,
652}
653
654impl NativeWorkerStatusChanges {
655    /// Returns the latest worker status without waiting.
656    pub fn current(&self) -> NativeWorkerStatus {
657        self.receiver.borrow().clone()
658    }
659
660    /// Waits for and returns the next worker status.
661    pub async fn changed(&mut self) -> Result<NativeWorkerStatus, NativeEventError> {
662        self.receiver
663            .changed()
664            .await
665            .map_err(|_| NativeEventError::Closed)?;
666        Ok(self.receiver.borrow_and_update().clone())
667    }
668}
669
670impl NativeSocketStatusChanges {
671    /// Returns the latest socket status without waiting.
672    pub fn current(&self) -> SocketStatus {
673        *self.receiver.borrow()
674    }
675
676    /// Waits for and returns the next socket status.
677    pub async fn changed(&mut self) -> Result<SocketStatus, NativeEventError> {
678        self.receiver
679            .changed()
680            .await
681            .map_err(|_| NativeEventError::Closed)?;
682        Ok(*self.receiver.borrow_and_update())
683    }
684}
685
686/// `Send + Sync` handle for one Phoenix channel topic.
687#[derive(Clone)]
688pub struct NativeChannel {
689    inner: Arc<NativeChannelInner>,
690}
691
692struct NativeChannelInner {
693    id: u64,
694    topic: String,
695    owner: Arc<WorkerOwner>,
696    events: broadcast::Sender<ChannelEvent>,
697    status: watch::Receiver<ChannelStatus>,
698}
699
700impl Drop for NativeChannelInner {
701    fn drop(&mut self) {
702        let _ = self
703            .owner
704            .control
705            .send(ControlCommand::RemoveChannel(self.id));
706    }
707}
708
709impl NativeChannel {
710    /// Returns the registered topic.
711    pub fn topic(&self) -> &str {
712        &self.inner.topic
713    }
714
715    /// Returns the latest channel status.
716    pub fn status(&self) -> ChannelStatus {
717        *self.inner.status.borrow()
718    }
719
720    /// Creates an independent bounded channel event receiver.
721    pub fn events(&self) -> NativeChannelEvents {
722        NativeChannelEvents {
723            receiver: self.inner.events.subscribe(),
724        }
725    }
726
727    /// Creates a receiver for subsequent channel status changes.
728    pub fn status_changes(&self) -> NativeChannelStatusChanges {
729        NativeChannelStatusChanges {
730            receiver: self.inner.status.clone(),
731        }
732    }
733
734    /// Creates a synchronized Presence consumer for this channel.
735    pub fn presence(&self) -> NativeChannelPresence {
736        NativeChannelPresence {
737            channel: self.clone(),
738            events: self.events(),
739            tracker: PresenceTracker::new(),
740            pending: VecDeque::new(),
741            desynchronized: false,
742        }
743    }
744
745    /// Subscribes to application messages with one event name.
746    pub fn subscribe(&self, event: impl Into<String>) -> NativeEventSubscription {
747        NativeEventSubscription {
748            event: event.into(),
749            events: self.events(),
750        }
751    }
752
753    /// Joins with the configured default timeout.
754    pub async fn join(&self) -> Result<Payload, NativeRuntimeError> {
755        self.request(|request_id, response| HostCommand::Join {
756            request_id,
757            id: self.inner.id,
758            timeout: None,
759            response,
760        })
761        .await
762    }
763
764    /// Joins with an explicit timeout.
765    pub async fn join_with_timeout(
766        &self,
767        timeout: Duration,
768    ) -> Result<Payload, NativeRuntimeError> {
769        self.request(|request_id, response| HostCommand::Join {
770            request_id,
771            id: self.inner.id,
772            timeout: Some(timeout),
773            response,
774        })
775        .await
776    }
777
778    /// Sends an event and waits for its correlated reply.
779    pub async fn call(
780        &self,
781        event: impl Into<String>,
782        payload: impl Into<Payload>,
783    ) -> Result<Reply, NativeRuntimeError> {
784        self.request(|request_id, response| HostCommand::Call {
785            request_id,
786            id: self.inner.id,
787            event: event.into(),
788            payload: payload.into(),
789            timeout: None,
790            response,
791        })
792        .await
793    }
794
795    /// Sends an event and waits up to `timeout` for its reply.
796    pub async fn call_with_timeout(
797        &self,
798        event: impl Into<String>,
799        payload: impl Into<Payload>,
800        timeout: Duration,
801    ) -> Result<Reply, NativeRuntimeError> {
802        self.request(|request_id, response| HostCommand::Call {
803            request_id,
804            id: self.inner.id,
805            event: event.into(),
806            payload: payload.into(),
807            timeout: Some(timeout),
808            response,
809        })
810        .await
811    }
812
813    /// Serializes a JSON request, requires an `ok` reply, and decodes it.
814    pub async fn call_json<Request, Response>(
815        &self,
816        event: impl Into<String>,
817        request: &Request,
818    ) -> Result<Response, NativeCallJsonError>
819    where
820        Request: Serialize + ?Sized,
821        Response: DeserializeOwned,
822    {
823        let payload = serde_json::to_value(request).map_err(NativeCallJsonError::Encode)?;
824        self.call(event, payload)
825            .await?
826            .deserialize_ok()
827            .map_err(Into::into)
828    }
829
830    /// Sends an event without asking Phoenix for a reply.
831    pub async fn cast(
832        &self,
833        event: impl Into<String>,
834        payload: impl Into<Payload>,
835    ) -> Result<(), NativeRuntimeError> {
836        self.request(|request_id, response| HostCommand::Cast {
837            request_id,
838            id: self.inner.id,
839            event: event.into(),
840            payload: payload.into(),
841            timeout: None,
842            response,
843        })
844        .await
845    }
846
847    /// Sends a cast, waiting at most `timeout` for transport acceptance.
848    pub async fn cast_with_timeout(
849        &self,
850        event: impl Into<String>,
851        payload: impl Into<Payload>,
852        timeout: Duration,
853    ) -> Result<(), NativeRuntimeError> {
854        self.request(|request_id, response| HostCommand::Cast {
855            request_id,
856            id: self.inner.id,
857            event: event.into(),
858            payload: payload.into(),
859            timeout: Some(timeout),
860            response,
861        })
862        .await
863    }
864
865    /// Leaves with the configured default timeout.
866    pub async fn leave(&self) -> Result<Payload, NativeRuntimeError> {
867        self.request(|request_id, response| HostCommand::Leave {
868            request_id,
869            id: self.inner.id,
870            timeout: None,
871            response,
872        })
873        .await
874    }
875
876    /// Leaves and waits up to `timeout` for the server reply.
877    pub async fn leave_with_timeout(
878        &self,
879        timeout: Duration,
880    ) -> Result<Payload, NativeRuntimeError> {
881        self.request(|request_id, response| HostCommand::Leave {
882            request_id,
883            id: self.inner.id,
884            timeout: Some(timeout),
885            response,
886        })
887        .await
888    }
889
890    async fn request<T: Send + 'static>(
891        &self,
892        build: impl FnOnce(u64, oneshot::Sender<Result<T, ClientError>>) -> HostCommand,
893    ) -> Result<T, NativeRuntimeError> {
894        request_worker(&self.inner.owner, build).await
895    }
896}
897
898async fn request_worker<T: Send + 'static>(
899    owner: &Arc<WorkerOwner>,
900    build: impl FnOnce(u64, oneshot::Sender<Result<T, ClientError>>) -> HostCommand,
901) -> Result<T, NativeRuntimeError> {
902    let permit = match owner.commands.reserve().await {
903        Ok(permit) => permit,
904        Err(_) => return Err(wait_for_worker_error(owner).await),
905    };
906    let request_id = owner.next_request_id();
907    let (response, receiver) = oneshot::channel();
908    if owner
909        .control
910        .send(ControlCommand::Register(request_id))
911        .is_err()
912    {
913        return Err(wait_for_worker_error(owner).await);
914    }
915    let mut guard = HostRequestGuard {
916        request_id,
917        control: owner.control.clone(),
918        armed: true,
919    };
920    permit.send(build(request_id, response));
921    let result = match receiver.await {
922        Ok(result) => result?,
923        Err(_) => return Err(wait_for_worker_error(owner).await),
924    };
925    guard.armed = false;
926    Ok(result)
927}
928
929fn worker_error(owner: &WorkerOwner) -> NativeRuntimeError {
930    match owner.status.borrow().clone() {
931        NativeWorkerStatus::Failed(message) => NativeRuntimeError::WorkerFailed(message),
932        NativeWorkerStatus::Running | NativeWorkerStatus::Stopped => {
933            NativeRuntimeError::WorkerStopped
934        }
935    }
936}
937
938async fn wait_for_worker_error(owner: &WorkerOwner) -> NativeRuntimeError {
939    let mut status = owner.status.clone();
940    if matches!(*status.borrow(), NativeWorkerStatus::Running) {
941        let _ = status.changed().await;
942    }
943    worker_error(owner)
944}
945
946struct HostRequestGuard {
947    request_id: u64,
948    control: mpsc::UnboundedSender<ControlCommand>,
949    armed: bool,
950}
951
952impl Drop for HostRequestGuard {
953    fn drop(&mut self) {
954        if self.armed {
955            let _ = self.control.send(ControlCommand::Cancel(self.request_id));
956        }
957    }
958}
959
960/// Independent bounded native channel event receiver.
961pub struct NativeChannelEvents {
962    receiver: broadcast::Receiver<ChannelEvent>,
963}
964
965/// Current Presence state and changes for a native channel.
966pub struct NativeChannelPresence {
967    channel: NativeChannel,
968    events: NativeChannelEvents,
969    tracker: PresenceTracker,
970    pending: VecDeque<PresenceEvent>,
971    desynchronized: bool,
972}
973
974/// Filtered native subscription for one application event name.
975pub struct NativeEventSubscription {
976    event: String,
977    events: NativeChannelEvents,
978}
979
980impl NativeEventSubscription {
981    /// Returns the event name matched by this subscription.
982    pub fn event(&self) -> &str {
983        &self.event
984    }
985
986    /// Waits for the next matching message or channel lifecycle event.
987    pub async fn next(&mut self) -> Result<SubscriptionEvent, NativeEventError> {
988        loop {
989            match self.events.next().await? {
990                ChannelEvent::Protocol(ProtocolEvent::Message(frame))
991                    if frame.event == self.event =>
992                {
993                    return Ok(SubscriptionEvent::Message(frame.payload));
994                }
995                ChannelEvent::Protocol(ProtocolEvent::ChannelError { payload, .. }) => {
996                    return Ok(SubscriptionEvent::ChannelError(payload));
997                }
998                ChannelEvent::Protocol(ProtocolEvent::ChannelClosed { payload, .. }) => {
999                    return Ok(SubscriptionEvent::ChannelClosed(payload));
1000                }
1001                ChannelEvent::Disconnected => return Ok(SubscriptionEvent::Disconnected),
1002                ChannelEvent::Lagged { dropped } => {
1003                    return Ok(SubscriptionEvent::Lagged { dropped });
1004                }
1005                ChannelEvent::Protocol(_) | ChannelEvent::JoinPayloadError(_) => {}
1006            }
1007        }
1008    }
1009}
1010
1011/// Failure while consuming native Presence state and events.
1012#[derive(Debug, Error)]
1013pub enum NativePresenceError {
1014    /// The underlying channel event receiver lagged or closed.
1015    #[error(transparent)]
1016    Events(#[from] NativeEventError),
1017    /// A Presence state or diff payload was invalid.
1018    #[error(transparent)]
1019    Decode(#[from] PresenceError),
1020    /// The bounded event stream dropped events and invalidated local state.
1021    #[error("presence event stream dropped {dropped} events and must be resynchronized")]
1022    Desynchronized {
1023        /// Number of dropped events.
1024        dropped: u64,
1025    },
1026    /// [`NativeChannelPresence::resync`] is required before consuming events.
1027    #[error("presence state must be resynchronized before consuming more events")]
1028    ResyncRequired,
1029}
1030
1031impl NativeChannelPresence {
1032    /// Returns the current synchronized Presence state.
1033    pub fn state(&self) -> &PresenceState {
1034        self.tracker.state()
1035    }
1036
1037    /// Returns whether event lag invalidated local state.
1038    pub fn requires_resync(&self) -> bool {
1039        self.desynchronized
1040    }
1041
1042    /// Clears local state, leaves, and rejoins to request a fresh state.
1043    pub async fn resync(&mut self) -> Result<(), NativeRuntimeError> {
1044        self.tracker.reset();
1045        self.pending.clear();
1046        self.channel.leave().await?;
1047        self.events = self.channel.events();
1048        self.channel.join().await?;
1049        self.desynchronized = false;
1050        Ok(())
1051    }
1052
1053    /// Waits for the next Presence change or channel lifecycle event.
1054    pub async fn next(&mut self) -> Result<PresenceEvent, NativePresenceError> {
1055        if self.desynchronized {
1056            return Err(NativePresenceError::ResyncRequired);
1057        }
1058        loop {
1059            if let Some(event) = self.pending.pop_front() {
1060                return Ok(event);
1061            }
1062            match self.events.next().await {
1063                Ok(ChannelEvent::Protocol(ProtocolEvent::Message(frame))) => {
1064                    let previous = self.tracker.state().clone();
1065                    match self.tracker.apply(&frame)? {
1066                        PresenceUpdate::Synced(diff) => {
1067                            for (key, joined) in diff.joins.0 {
1068                                self.pending.push_back(PresenceEvent::Joined {
1069                                    current: previous.get(&key).cloned(),
1070                                    key,
1071                                    joined,
1072                                });
1073                            }
1074                            for (key, left) in diff.leaves.0 {
1075                                if let Some(current) = previous.get(&key).cloned() {
1076                                    self.pending.push_back(PresenceEvent::Left {
1077                                        key,
1078                                        current,
1079                                        left,
1080                                    });
1081                                }
1082                            }
1083                            self.pending.push_back(PresenceEvent::Synced);
1084                        }
1085                        PresenceUpdate::Ignored | PresenceUpdate::Pending => {}
1086                    }
1087                }
1088                Ok(ChannelEvent::Disconnected) => {
1089                    self.tracker.reset();
1090                    return Ok(PresenceEvent::Disconnected);
1091                }
1092                Ok(ChannelEvent::Lagged { dropped }) | Err(NativeEventError::Lagged(dropped)) => {
1093                    self.tracker.reset();
1094                    self.pending.clear();
1095                    self.desynchronized = true;
1096                    return Err(NativePresenceError::Desynchronized { dropped });
1097                }
1098                Ok(ChannelEvent::Protocol(ProtocolEvent::Left { .. })) => {
1099                    self.tracker.reset();
1100                    return Ok(PresenceEvent::ChannelLeft);
1101                }
1102                Ok(ChannelEvent::Protocol(ProtocolEvent::ChannelClosed { .. })) => {
1103                    self.tracker.reset();
1104                    return Ok(PresenceEvent::ChannelClosed);
1105                }
1106                Ok(ChannelEvent::Protocol(ProtocolEvent::ChannelError { .. })) => {
1107                    self.tracker.reset();
1108                    return Ok(PresenceEvent::ChannelError);
1109                }
1110                Ok(ChannelEvent::Protocol(_) | ChannelEvent::JoinPayloadError(_)) => {}
1111                Err(error) => return Err(error.into()),
1112            }
1113        }
1114    }
1115}
1116
1117impl NativeChannelEvents {
1118    /// Waits for the next channel event.
1119    pub async fn next(&mut self) -> Result<ChannelEvent, NativeEventError> {
1120        receive_event(&mut self.receiver).await
1121    }
1122}
1123
1124/// Receiver for native channel status changes.
1125pub struct NativeChannelStatusChanges {
1126    receiver: watch::Receiver<ChannelStatus>,
1127}
1128
1129impl NativeChannelStatusChanges {
1130    /// Returns the latest channel status without waiting.
1131    pub fn current(&self) -> ChannelStatus {
1132        *self.receiver.borrow()
1133    }
1134
1135    /// Waits for and returns the next channel status.
1136    pub async fn changed(&mut self) -> Result<ChannelStatus, NativeEventError> {
1137        self.receiver
1138            .changed()
1139            .await
1140            .map_err(|_| NativeEventError::Closed)?;
1141        Ok(*self.receiver.borrow_and_update())
1142    }
1143}
1144
1145async fn receive_event<T: Clone>(
1146    receiver: &mut broadcast::Receiver<T>,
1147) -> Result<T, NativeEventError> {
1148    receiver.recv().await.map_err(|error| match error {
1149        broadcast::error::RecvError::Closed => NativeEventError::Closed,
1150        broadcast::error::RecvError::Lagged(count) => NativeEventError::Lagged(count),
1151    })
1152}
1153
1154struct ChannelRegistration {
1155    id: u64,
1156    topic: String,
1157    events: broadcast::Sender<ChannelEvent>,
1158    status: watch::Receiver<ChannelStatus>,
1159}
1160
1161enum HostCommand {
1162    Connect {
1163        request_id: u64,
1164        response: oneshot::Sender<Result<(), ClientError>>,
1165    },
1166    Disconnect {
1167        request_id: u64,
1168        response: oneshot::Sender<Result<(), ClientError>>,
1169    },
1170    DisconnectWith {
1171        request_id: u64,
1172        code: u16,
1173        reason: String,
1174        response: oneshot::Sender<Result<(), ClientError>>,
1175    },
1176    Ping {
1177        request_id: u64,
1178        timeout: Option<Duration>,
1179        response: oneshot::Sender<Result<Duration, ClientError>>,
1180    },
1181    Shutdown {
1182        response: oneshot::Sender<Result<(), ClientError>>,
1183    },
1184    Channel {
1185        request_id: u64,
1186        topic: String,
1187        payload_loader: NativeJoinPayloadLoader,
1188        response: oneshot::Sender<Result<ChannelRegistration, ClientError>>,
1189    },
1190    Join {
1191        request_id: u64,
1192        id: u64,
1193        timeout: Option<Duration>,
1194        response: oneshot::Sender<Result<Payload, ClientError>>,
1195    },
1196    Call {
1197        request_id: u64,
1198        id: u64,
1199        event: String,
1200        payload: Payload,
1201        timeout: Option<Duration>,
1202        response: oneshot::Sender<Result<Reply, ClientError>>,
1203    },
1204    Cast {
1205        request_id: u64,
1206        id: u64,
1207        event: String,
1208        payload: Payload,
1209        timeout: Option<Duration>,
1210        response: oneshot::Sender<Result<(), ClientError>>,
1211    },
1212    Leave {
1213        request_id: u64,
1214        id: u64,
1215        timeout: Option<Duration>,
1216        response: oneshot::Sender<Result<Payload, ClientError>>,
1217    },
1218}
1219
1220enum ControlCommand {
1221    Register(u64),
1222    Cancel(u64),
1223    Finished(u64),
1224    RemoveChannel(u64),
1225    Shutdown,
1226}
1227
1228struct WorkerBootstrap {
1229    endpoint_url: String,
1230    config_loader: NativeConnectionConfigLoader,
1231    options: NativeOptions,
1232    commands: mpsc::Receiver<HostCommand>,
1233    control: mpsc::UnboundedReceiver<ControlCommand>,
1234    control_tx: mpsc::UnboundedSender<ControlCommand>,
1235    events: broadcast::Sender<SocketEvent>,
1236    status: watch::Sender<SocketStatus>,
1237    ready: std::sync::mpsc::SyncSender<Result<(), String>>,
1238}
1239
1240fn run_worker(bootstrap: WorkerBootstrap) -> Result<(), String> {
1241    let runtime = tokio::runtime::Builder::new_current_thread()
1242        .enable_all()
1243        .build()
1244        .map_err(|error| error.to_string())?;
1245    tokio::task::LocalSet::new().block_on(&runtime, worker_main(bootstrap))
1246}
1247
1248async fn worker_main(bootstrap: WorkerBootstrap) -> Result<(), String> {
1249    let WorkerBootstrap {
1250        endpoint_url,
1251        config_loader,
1252        options,
1253        mut commands,
1254        mut control,
1255        control_tx,
1256        events,
1257        status,
1258        ready,
1259    } = bootstrap;
1260    let local_loader = Rc::new(move |context| {
1261        let config_loader = config_loader.clone();
1262        async move { config_loader(context).await }.boxed_local()
1263    });
1264    let endpoint = Endpoint::new(endpoint_url)
1265        .map_err(|error| error.to_string())?
1266        .connection_config_loader(local_loader);
1267    let event_capacity = options.event_capacity;
1268    let transport_options = options.transport.clone();
1269    let (socket, driver) = Socket::new(
1270        NativeConnector::from_endpoint(endpoint).options(transport_options),
1271        NativeTimer,
1272        options.client_options(),
1273    );
1274    let mut socket_events = socket.events().map_err(|error| error.to_string())?;
1275    let mut socket_statuses = socket.status_changes();
1276    let mut driver_task = tokio::task::spawn_local(driver);
1277    let next_id = AtomicU64::new(1);
1278    let mut channels: HashMap<u64, Rc<Channel>> = HashMap::new();
1279    let mut operations: HashMap<u64, tokio::task::JoinHandle<()>> = HashMap::new();
1280    let mut known_requests = HashSet::new();
1281    let mut cancelled = HashSet::new();
1282    let _ = ready.send(Ok(()));
1283
1284    loop {
1285        tokio::select! {
1286            driver_result = &mut driver_task => match driver_result {
1287                Ok(()) => break,
1288                Err(error) => return Err(format!("client driver task failed: {error}")),
1289            },
1290            control_command = control.recv() => match control_command {
1291                Some(ControlCommand::Register(request_id)) => {
1292                    known_requests.insert(request_id);
1293                }
1294                Some(ControlCommand::Cancel(request_id)) => {
1295                    if let Some(operation) = operations.remove(&request_id) {
1296                        operation.abort();
1297                    } else if known_requests.contains(&request_id) {
1298                        cancelled.insert(request_id);
1299                    }
1300                }
1301                Some(ControlCommand::Finished(request_id)) => {
1302                    operations.remove(&request_id);
1303                    known_requests.remove(&request_id);
1304                    cancelled.remove(&request_id);
1305                }
1306                Some(ControlCommand::RemoveChannel(id)) => {
1307                    channels.remove(&id);
1308                }
1309                Some(ControlCommand::Shutdown) | None => {
1310                    let _ = socket.shutdown().await;
1311                    break;
1312                }
1313            },
1314            event = socket_events.next() => match event {
1315                Some(event) => {
1316                    let _ = events.send(event);
1317                }
1318                None => break,
1319            },
1320            changed = socket_statuses.changed() => match changed {
1321                Some(changed) => {
1322                    status.send_replace(changed);
1323                }
1324                None => break,
1325            },
1326            command = commands.recv() => {
1327                let Some(command) = command else {
1328                    let _ = socket.shutdown().await;
1329                    break;
1330                };
1331                let mut host = HostState {
1332                    channels: &mut channels,
1333                    next_id: &next_id,
1334                    event_capacity,
1335                    control: &control_tx,
1336                    operations: &mut operations,
1337                    known_requests: &mut known_requests,
1338                    cancelled: &mut cancelled,
1339                };
1340                if handle_host_command(command, &socket, &mut host).await {
1341                    break;
1342                }
1343            }
1344        }
1345    }
1346    status.send_replace(SocketStatus::Closed);
1347    for (_, operation) in operations {
1348        operation.abort();
1349    }
1350    if driver_task.is_finished() {
1351        driver_task
1352            .await
1353            .map_err(|error| format!("client driver task failed: {error}"))?;
1354    } else {
1355        driver_task.abort();
1356    }
1357    Ok(())
1358}
1359
1360struct HostState<'a> {
1361    channels: &'a mut HashMap<u64, Rc<Channel>>,
1362    next_id: &'a AtomicU64,
1363    event_capacity: usize,
1364    control: &'a mpsc::UnboundedSender<ControlCommand>,
1365    operations: &'a mut HashMap<u64, tokio::task::JoinHandle<()>>,
1366    known_requests: &'a mut HashSet<u64>,
1367    cancelled: &'a mut HashSet<u64>,
1368}
1369
1370async fn handle_host_command(
1371    command: HostCommand,
1372    socket: &Socket,
1373    state: &mut HostState<'_>,
1374) -> bool {
1375    let HostState {
1376        channels,
1377        next_id,
1378        event_capacity,
1379        control,
1380        operations,
1381        known_requests,
1382        cancelled,
1383    } = state;
1384    match command {
1385        HostCommand::Connect {
1386            request_id,
1387            response,
1388        } => {
1389            known_requests.remove(&request_id);
1390            if cancelled.remove(&request_id) {
1391                return false;
1392            }
1393            let socket = socket.clone();
1394            track_operation(request_id, control, operations, async move {
1395                let _ = response.send(socket.connect().await);
1396            });
1397        }
1398        HostCommand::Disconnect {
1399            request_id,
1400            response,
1401        } => {
1402            known_requests.remove(&request_id);
1403            if cancelled.remove(&request_id) {
1404                return false;
1405            }
1406            let socket = socket.clone();
1407            track_operation(request_id, control, operations, async move {
1408                let _ = response.send(socket.disconnect().await);
1409            });
1410        }
1411        HostCommand::DisconnectWith {
1412            request_id,
1413            code,
1414            reason,
1415            response,
1416        } => {
1417            known_requests.remove(&request_id);
1418            if cancelled.remove(&request_id) {
1419                return false;
1420            }
1421            let socket = socket.clone();
1422            track_operation(request_id, control, operations, async move {
1423                let _ = response.send(socket.disconnect_with(code, reason).await);
1424            });
1425        }
1426        HostCommand::Ping {
1427            request_id,
1428            timeout,
1429            response,
1430        } => {
1431            known_requests.remove(&request_id);
1432            if cancelled.remove(&request_id) {
1433                return false;
1434            }
1435            let socket = socket.clone();
1436            track_operation(request_id, control, operations, async move {
1437                let result = match timeout {
1438                    Some(timeout) => socket.ping_with_timeout(timeout).await,
1439                    None => socket.ping().await,
1440                };
1441                let _ = response.send(result);
1442            });
1443        }
1444        HostCommand::Shutdown { response } => {
1445            let result = socket.shutdown().await;
1446            let _ = response.send(result);
1447            return true;
1448        }
1449        HostCommand::Channel {
1450            request_id,
1451            topic,
1452            payload_loader,
1453            response,
1454        } => {
1455            known_requests.remove(&request_id);
1456            if cancelled.remove(&request_id) {
1457                return false;
1458            }
1459            let registration_topic = topic.clone();
1460            let local_loader = Rc::new(move |context| {
1461                let payload_loader = payload_loader.clone();
1462                async move { payload_loader(context).await }.boxed_local()
1463            });
1464            match socket.channel(topic, local_loader) {
1465                Ok(channel) => {
1466                    let id = next_id.fetch_add(1, Ordering::Relaxed);
1467                    let channel = Rc::new(channel);
1468                    let (event_tx, _) = broadcast::channel(*event_capacity);
1469                    let (status_tx, status_rx) = watch::channel(channel.status());
1470                    let mut status_changes = channel.status_changes();
1471                    let mut event_rx = match channel.events() {
1472                        Ok(events) => events,
1473                        Err(error) => {
1474                            let _ = response.send(Err(error));
1475                            return false;
1476                        }
1477                    };
1478                    let pump_tx = event_tx.clone();
1479                    tokio::task::spawn_local(async move {
1480                        while let Some(event) = event_rx.next().await {
1481                            let _ = pump_tx.send(event);
1482                        }
1483                    });
1484                    tokio::task::spawn_local(async move {
1485                        while let Some(changed) = status_changes.changed().await {
1486                            status_tx.send_replace(changed);
1487                        }
1488                    });
1489                    channels.insert(id, channel);
1490                    let _ = response.send(Ok(ChannelRegistration {
1491                        id,
1492                        topic: registration_topic,
1493                        events: event_tx,
1494                        status: status_rx,
1495                    }));
1496                }
1497                Err(error) => {
1498                    let _ = response.send(Err(error));
1499                }
1500            }
1501        }
1502        HostCommand::Join {
1503            request_id,
1504            id,
1505            timeout,
1506            response,
1507        } => {
1508            known_requests.remove(&request_id);
1509            if cancelled.remove(&request_id) {
1510                return false;
1511            }
1512            let channel = channels.get(&id).cloned();
1513            track_operation(request_id, control, operations, async move {
1514                send_channel_response(channel, response, move |channel| async move {
1515                    match timeout {
1516                        Some(timeout) => channel.join_with_timeout(timeout).await,
1517                        None => channel.join().await,
1518                    }
1519                })
1520                .await;
1521            });
1522        }
1523        HostCommand::Call {
1524            request_id,
1525            id,
1526            event,
1527            payload,
1528            timeout,
1529            response,
1530        } => {
1531            known_requests.remove(&request_id);
1532            if cancelled.remove(&request_id) {
1533                return false;
1534            }
1535            let channel = channels.get(&id).cloned();
1536            track_operation(request_id, control, operations, async move {
1537                send_channel_response(channel, response, move |channel| async move {
1538                    match timeout {
1539                        Some(timeout) => channel.call_with_timeout(event, payload, timeout).await,
1540                        None => channel.call(event, payload).await,
1541                    }
1542                })
1543                .await;
1544            });
1545        }
1546        HostCommand::Cast {
1547            request_id,
1548            id,
1549            event,
1550            payload,
1551            timeout,
1552            response,
1553        } => {
1554            known_requests.remove(&request_id);
1555            if cancelled.remove(&request_id) {
1556                return false;
1557            }
1558            let channel = channels.get(&id).cloned();
1559            track_operation(request_id, control, operations, async move {
1560                send_channel_response(channel, response, move |channel| async move {
1561                    match timeout {
1562                        Some(timeout) => channel.cast_with_timeout(event, payload, timeout).await,
1563                        None => channel.cast(event, payload).await,
1564                    }
1565                })
1566                .await;
1567            });
1568        }
1569        HostCommand::Leave {
1570            request_id,
1571            id,
1572            timeout,
1573            response,
1574        } => {
1575            known_requests.remove(&request_id);
1576            if cancelled.remove(&request_id) {
1577                return false;
1578            }
1579            let channel = channels.get(&id).cloned();
1580            track_operation(request_id, control, operations, async move {
1581                send_channel_response(channel, response, move |channel| async move {
1582                    match timeout {
1583                        Some(timeout) => channel.leave_with_timeout(timeout).await,
1584                        None => channel.leave().await,
1585                    }
1586                })
1587                .await;
1588            });
1589        }
1590    }
1591    false
1592}
1593
1594async fn send_channel_response<T, F, Fut>(
1595    channel: Option<Rc<Channel>>,
1596    response: oneshot::Sender<Result<T, ClientError>>,
1597    call: F,
1598) where
1599    T: 'static,
1600    F: FnOnce(Rc<Channel>) -> Fut + 'static,
1601    Fut: Future<Output = Result<T, ClientError>> + 'static,
1602{
1603    let Some(channel) = channel else {
1604        let _ = response.send(Err(ClientError::DriverStopped));
1605        return;
1606    };
1607    let result = call(channel).await;
1608    let _ = response.send(result);
1609}
1610
1611fn track_operation(
1612    request_id: u64,
1613    control: &mpsc::UnboundedSender<ControlCommand>,
1614    operations: &mut HashMap<u64, tokio::task::JoinHandle<()>>,
1615    operation: impl Future<Output = ()> + 'static,
1616) {
1617    let control = control.clone();
1618    let handle = tokio::task::spawn_local(async move {
1619        operation.await;
1620        let _ = control.send(ControlCommand::Finished(request_id));
1621    });
1622    operations.insert(request_id, handle);
1623}
1624
1625fn panic_message(panic: &(dyn Any + Send)) -> String {
1626    panic
1627        .downcast_ref::<&str>()
1628        .map(|message| (*message).to_owned())
1629        .or_else(|| panic.downcast_ref::<String>().cloned())
1630        .unwrap_or_else(|| "worker panicked without a string payload".to_owned())
1631}
1632
1633#[cfg(test)]
1634mod tests {
1635    use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
1636
1637    use futures::StreamExt;
1638
1639    use super::*;
1640
1641    fn assert_send_sync<T: Send + Sync>() {}
1642
1643    #[test]
1644    fn native_handles_are_send_and_sync() {
1645        assert_send_sync::<NativeSocket>();
1646        assert_send_sync::<NativeChannel>();
1647        assert_send_sync::<NativeChannelPresence>();
1648        assert_send_sync::<NativeEventSubscription>();
1649        assert_send_sync::<NativeOptions>();
1650    }
1651
1652    #[test]
1653    fn retry_schedules_hold_the_last_delay() {
1654        let delays = [Duration::from_secs(1), Duration::from_secs(3)];
1655        assert_eq!(retry_delay(&delays, 0), Duration::from_secs(1));
1656        assert_eq!(retry_delay(&delays, 8), Duration::from_secs(3));
1657    }
1658
1659    #[tokio::test(flavor = "current_thread")]
1660    async fn event_subscriptions_report_broadcast_lag() {
1661        let (events, receiver) = broadcast::channel(1);
1662        let mut subscription = NativeEventSubscription {
1663            event: "notice".into(),
1664            events: NativeChannelEvents { receiver },
1665        };
1666        events.send(ChannelEvent::Disconnected).unwrap();
1667        events.send(ChannelEvent::Disconnected).unwrap();
1668        assert!(matches!(
1669            subscription.next().await,
1670            Err(NativeEventError::Lagged(1))
1671        ));
1672    }
1673
1674    #[tokio::test(flavor = "current_thread")]
1675    async fn reconnect_policy_runs_on_the_native_worker() {
1676        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1677        let port = listener.local_addr().unwrap().port();
1678        let server = tokio::spawn(async move {
1679            let (stream, _) = listener.accept().await.unwrap();
1680            drop(stream);
1681        });
1682        let attempts = Arc::new(AtomicUsize::new(0));
1683        let options = NativeOptions::default().reconnect_policy({
1684            let attempts = attempts.clone();
1685            Arc::new(move |_| {
1686                attempts.fetch_add(1, AtomicOrdering::SeqCst);
1687                ReconnectAction::Stop
1688            })
1689        });
1690        let socket = NativeSocket::spawn_with_options(
1691            format!("ws://127.0.0.1:{port}/socket"),
1692            ConnectionConfig::default(),
1693            options,
1694        )
1695        .unwrap();
1696        socket.connect().await.unwrap();
1697
1698        tokio::time::timeout(Duration::from_secs(5), async {
1699            while attempts.load(AtomicOrdering::SeqCst) == 0 {
1700                tokio::task::yield_now().await;
1701            }
1702        })
1703        .await
1704        .expect("native reconnect policy was not invoked");
1705        server.await.unwrap();
1706        socket.shutdown().await.unwrap();
1707    }
1708
1709    #[tokio::test(flavor = "current_thread")]
1710    async fn native_ping_timeout_cancels_the_host_request() {
1711        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1712        let port = listener.local_addr().unwrap().port();
1713        let server = tokio::spawn(async move {
1714            let (stream, _) = listener.accept().await.unwrap();
1715            let mut websocket = tokio_tungstenite::accept_async(stream).await.unwrap();
1716            while websocket.next().await.is_some() {}
1717        });
1718        let socket = NativeSocket::spawn(
1719            format!("ws://127.0.0.1:{port}/socket"),
1720            ConnectionConfig::default(),
1721        )
1722        .unwrap();
1723        socket.connect().await.unwrap();
1724        let mut statuses = socket.status_changes();
1725        tokio::time::timeout(Duration::from_secs(2), async {
1726            while socket.status() != SocketStatus::Connected {
1727                statuses.changed().await.unwrap();
1728            }
1729        })
1730        .await
1731        .expect("native test socket did not connect");
1732
1733        assert!(matches!(
1734            socket.ping_with_timeout(Duration::from_millis(20)).await,
1735            Err(NativeRuntimeError::Client(ClientError::Timeout {
1736                operation: phoenix_channel_client::ClientOperation::Ping,
1737                ..
1738            }))
1739        ));
1740        socket.shutdown().await.unwrap();
1741        server.abort();
1742        let _ = server.await;
1743    }
1744
1745    #[tokio::test(flavor = "current_thread")]
1746    async fn cancelled_host_requests_do_not_prevent_shutdown() {
1747        let socket =
1748            NativeSocket::spawn("ws://127.0.0.1:9/socket", ConnectionConfig::default()).unwrap();
1749        let channel = socket
1750            .channel("room:lobby", serde_json::json!({}))
1751            .await
1752            .unwrap();
1753        assert_eq!(channel.topic(), "room:lobby");
1754
1755        let pending = tokio::spawn({
1756            let channel = channel.clone();
1757            async move { channel.call("queued", serde_json::json!({})).await }
1758        });
1759        tokio::task::yield_now().await;
1760        pending.abort();
1761        let _ = pending.await;
1762
1763        socket.shutdown().await.unwrap();
1764        assert_eq!(socket.worker_status(), NativeWorkerStatus::Stopped);
1765    }
1766
1767    #[tokio::test(flavor = "current_thread")]
1768    async fn reports_driver_panics_as_worker_failures() {
1769        let options = NativeOptions::default().telemetry(Arc::new(|_| {
1770            panic!("telemetry failure");
1771        }));
1772        let socket = NativeSocket::spawn_with_options(
1773            "ws://127.0.0.1:9/socket",
1774            ConnectionConfig::default(),
1775            options,
1776        )
1777        .unwrap();
1778        let _ = socket.connect().await;
1779        assert!(matches!(
1780            socket.shutdown().await,
1781            Err(NativeRuntimeError::WorkerFailed(_))
1782        ));
1783        assert!(matches!(
1784            socket.join_worker(),
1785            Err(NativeRuntimeError::WorkerFailed(message)) if message.contains("client driver task failed")
1786        ));
1787        assert!(matches!(
1788            socket.worker_status(),
1789            NativeWorkerStatus::Failed(_)
1790        ));
1791    }
1792}