Skip to content

Commit 0e12e61

Browse files
committed
implement realtime presence publish/subscribe functionality
1 parent d94e127 commit 0e12e61

3 files changed

Lines changed: 57 additions & 3 deletions

File tree

‎ably/realtime/presencemap.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def __init__(
8989
self._member_key_fn = member_key_fn
9090
self._is_newer_fn = is_newer_fn or _is_newer
9191
self._logger = logger_instance or logger
92+
self._sync_complete_callbacks: List[Callable[[], None]] = []
9293

9394
@property
9495
def sync_in_progress(self) -> bool:
@@ -303,8 +304,30 @@ def end_sync(self) -> Tuple[List[PresenceMessage], List[PresenceMessage]]:
303304
f"{len(residual_list)} residual members"
304305
)
305306

307+
# Notify callbacks that sync is complete
308+
for callback in self._sync_complete_callbacks:
309+
try:
310+
callback()
311+
except Exception as e:
312+
self._logger.error(f"Error in sync complete callback: {e}")
313+
self._sync_complete_callbacks.clear()
314+
306315
return residual_list, absent_list
307316

317+
def wait_sync(self, callback: Callable[[], None]) -> None:
318+
"""
319+
Wait for SYNC to complete, calling callback when done.
320+
321+
If sync is not in progress, callback is called immediately.
322+
323+
Args:
324+
callback: Function to call when sync completes
325+
"""
326+
if not self._sync_in_progress:
327+
callback()
328+
else:
329+
self._sync_complete_callbacks.append(callback)
330+
308331
def clear(self) -> None:
309332
"""
310333
Clear all members and reset sync state.
@@ -314,4 +337,5 @@ def clear(self) -> None:
314337
self._map.clear()
315338
self._residual_members = None
316339
self._sync_in_progress = False
340+
self._sync_complete_callbacks.clear()
317341
self._logger.debug("PresenceMap.clear: cleared all members")

‎ably/realtime/realtime_channel.py‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ def __init__(self, realtime: AblyRealtime, name: str, channel_options: ChannelOp
136136
# will be disrupted if the user called .off() to remove all listeners
137137
self.__internal_state_emitter = EventEmitter()
138138

139+
# Initialize presence for this channel
140+
from ably.realtime.realtimepresence import RealtimePresence
141+
self.__presence = RealtimePresence(self)
142+
139143
# Pass channel options as dictionary to parent Channel class
140144
Channel.__init__(self, realtime, name, self.__channel_options.to_dict())
141145

@@ -528,6 +532,7 @@ def _on_message(self, proto_msg: dict) -> None:
528532
error = proto_msg.get("error")
529533
exception = None
530534
resumed = False
535+
has_presence = False
531536

532537
self.__attach_serial = channel_serial
533538
self.__channel_serial = channel_serial
@@ -538,14 +543,16 @@ def _on_message(self, proto_msg: dict) -> None:
538543

539544
if flags:
540545
resumed = has_flag(flags, Flag.RESUMED)
546+
# RTP1: Check for HAS_PRESENCE flag
547+
has_presence = has_flag(flags, Flag.HAS_PRESENCE)
541548

542549
# RTL12
543550
if self.state == ChannelState.ATTACHED:
544551
if not resumed:
545552
state_change = ChannelStateChange(self.state, ChannelState.ATTACHED, resumed, exception)
546553
self._emit("update", state_change)
547554
elif self.state == ChannelState.ATTACHING:
548-
self._notify_state(ChannelState.ATTACHED, resumed=resumed)
555+
self._notify_state(ChannelState.ATTACHED, resumed=resumed, has_presence=has_presence)
549556
else:
550557
log.warn("RealtimeChannel._on_message(): ATTACHED received while not attaching")
551558
elif action == ProtocolMessageAction.DETACHED:
@@ -568,6 +575,19 @@ def _on_message(self, proto_msg: dict) -> None:
568575
log.error(f"Message processing error {e}. Skip messages {proto_msg.get('messages')}")
569576
for message in messages:
570577
self.__message_emitter._emit(message.name, message)
578+
elif action == ProtocolMessageAction.PRESENCE:
579+
# Handle PRESENCE messages
580+
from ably.types.presence import PresenceMessage
581+
presence_messages = proto_msg.get('presence', [])
582+
decoded_presence = PresenceMessage.from_encoded_array(presence_messages, cipher=self.cipher)
583+
self.__presence.set_presence(decoded_presence, is_sync=False)
584+
elif action == ProtocolMessageAction.SYNC:
585+
# Handle SYNC messages (RTP18)
586+
from ably.types.presence import PresenceMessage
587+
presence_messages = proto_msg.get('presence', [])
588+
decoded_presence = PresenceMessage.from_encoded_array(presence_messages, cipher=self.cipher)
589+
sync_channel_serial = proto_msg.get('channelSerial')
590+
self.__presence.set_presence(decoded_presence, is_sync=True, sync_channel_serial=sync_channel_serial)
571591
elif action == ProtocolMessageAction.ERROR:
572592
error = AblyException.from_dict(proto_msg.get('error'))
573593
self._notify_state(ChannelState.FAILED, reason=error)
@@ -578,7 +598,7 @@ def _request_state(self, state: ChannelState) -> None:
578598
self._check_pending_state()
579599

580600
def _notify_state(self, state: ChannelState, reason: AblyException | None = None,
581-
resumed: bool = False) -> None:
601+
resumed: bool = False, has_presence: bool = False) -> None:
582602
log.debug(f'RealtimeChannel._notify_state(): state = {state}')
583603

584604
self.__clear_state_timer()
@@ -616,6 +636,9 @@ def _notify_state(self, state: ChannelState, reason: AblyException | None = None
616636
self._emit(state, state_change)
617637
self.__internal_state_emitter._emit(state, state_change)
618638

639+
# RTP5: Notify presence of channel state change
640+
self.__presence.act_on_channel_state(state, has_presence=has_presence, error=reason)
641+
619642
def _send_message(self, msg: dict) -> None:
620643
asyncio.create_task(self.__realtime.connection.connection_manager.send_protocol_message(msg))
621644

@@ -706,6 +729,11 @@ def params(self) -> dict[str, str]:
706729
"""Get channel parameters"""
707730
return self.__params
708731

732+
@property
733+
def presence(self):
734+
"""Get the RealtimePresence object for this channel"""
735+
return self.__presence
736+
709737
def _start_decode_failure_recovery(self, error: AblyException) -> None:
710738
"""Start RTL18 decode failure recovery procedure"""
711739

‎ably/transport/websockettransport.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,9 @@ async def on_protocol_message(self, msg):
180180
elif action in (
181181
ProtocolMessageAction.ATTACHED,
182182
ProtocolMessageAction.DETACHED,
183-
ProtocolMessageAction.MESSAGE
183+
ProtocolMessageAction.MESSAGE,
184+
ProtocolMessageAction.PRESENCE,
185+
ProtocolMessageAction.SYNC
184186
):
185187
self.connection_manager.on_channel_message(msg)
186188

0 commit comments

Comments
 (0)