1515import json # JSON encoding and decoding
1616import secrets # Generate secure random numbers
1717import logging # Logging library
18- from typing import Callable # Type hinting support
19- from websockets import connect , exceptions
20-
21-
22- class EventEmitter :
23- """
24- A simple event emitter class that allows registering and emitting events.
25- Supports wildcard events using '*' and '**' with '.' as the separator.
26- """
27-
28- def __init__ (self ):
29- """
30- Initialize an `EventEmitter` instance with an empty events dictionary.
31- """
32- self ._events = {}
33-
34- def on (self , event : str , listener : Callable ):
35- """
36- Register a listener for a specific event, with support for wildcards.
37-
38- Args:
39- event (str): The name of the event, can include wildcards ('*' or '**').
40- listener (Callable): The function to call when the event is emitted.
41- """
42- if event not in self ._events :
43- self ._events [event ] = []
44- self ._events [event ].append (listener )
45-
46- def off (self , event : str , listener : Callable ):
47- """
48- Remove a listener for a specific event.
49-
50- Args:
51- event (str): The name of the event.
52- listener (Callable): The function to remove from the event listeners.
53- """
54- if event in self ._events :
55- try :
56- self ._events [event ].remove (listener )
57- if not self ._events [event ]:
58- del self ._events [event ]
59- except ValueError :
60- # Listener not found in the list
61- pass
62-
63- def emit (self , event : str , * args , ** kwargs ):
64- """
65- Trigger all listeners associated with an event, supporting wildcards.
66-
67- Args:
68- event (str): The name of the event to emit.
69- *args: Positional arguments to pass to the event listeners.
70- **kwargs: Keyword arguments to pass to the event listeners.
71- """
72- listeners = []
73- for event_pattern , event_listeners in self ._events .items ():
74- if self .event_matches (event_pattern , event ):
75- listeners .extend (event_listeners )
76- for listener in listeners :
77- if asyncio .iscoroutinefunction (listener ):
78- asyncio .create_task (listener (* args , ** kwargs ))
79- else :
80- listener (* args , ** kwargs )
81-
82- def once (self , event : str , listener : Callable ):
83- """
84- Register a listener for a specific event that will be called at most once.
85-
86- Args:
87- event (str): The name of the event.
88- listener (Callable): The function to call when the event is emitted.
89- """
90-
91- def _once_listener (* args , ** kwargs ):
92- listener (* args , ** kwargs )
93- self .off (event , _once_listener )
94-
95- self .on (event , _once_listener )
96-
97- @staticmethod
98- def event_matches (pattern : str , event_name : str ) -> bool :
99- """
100- Check if an event pattern matches an event name, supporting wildcards '*' and '**'.
101-
102- Args:
103- pattern (str): The event pattern, may include wildcards '*' and '**'.
104- event_name (str): The event name to match against.
10518
106- Returns:
107- bool: True if the pattern matches the event name, False otherwise.
108- """
109-
110- def match_segments (pattern_segments , event_segments ):
111- i = j = 0
112- while i < len (pattern_segments ) and j < len (event_segments ):
113- if pattern_segments [i ] == '**' :
114- # '**' matches any number of segments, including zero
115- if i == len (pattern_segments ) - 1 :
116- # '**' at the end matches all remaining segments
117- return True
118- else :
119- # Try to match remaining pattern with any position in event_segments
120- for k in range (j , len (event_segments ) + 1 ):
121- if match_segments (pattern_segments [i + 1 :], event_segments [k :]):
122- return True
123- return False
124- elif pattern_segments [i ] == '*' :
125- # '*' matches exactly one segment
126- i += 1
127- j += 1
128- elif pattern_segments [i ] == event_segments [j ]:
129- # Exact match
130- i += 1
131- j += 1
132- else :
133- return False
134- while i < len (pattern_segments ) and pattern_segments [i ] == '**' :
135- i += 1
136- return i == len (pattern_segments ) and j == len (event_segments )
19+ from websockets import connect , exceptions
13720
138- pattern_segments = pattern .split ('.' )
139- event_segments = event_name .split ('.' )
140- return match_segments (pattern_segments , event_segments )
21+ from realtime_pubsub_client .event_emitter import EventEmitter
22+ from realtime_pubsub_client .wait_for import WaitFor
14123
14224
14325def reply (client , message ):
@@ -158,14 +40,14 @@ def reply(client, message):
15840 ValueError: If the connection ID is not available in the incoming message.
15941 """
16042
161- def reply_function (data , status = 'ok' , options = None ):
43+ def reply_function (data , status = 'ok' , compress = False ):
16244 """
16345 Sends a reply message back to the sender of the original message.
16446
16547 Args:
16648 data: The payload data to send in the reply.
16749 status (str, optional): The status of the reply. Defaults to 'ok'.
168- options (dict , optional): Additional message options . Defaults to None .
50+ compress (bool , optional): Whether to compress the reply payload . Defaults to False .
16951
17052 Returns:
17153 WaitFor: An instance to wait for acknowledgments or replies.
@@ -181,69 +63,13 @@ def reply_function(data, status='ok', options=None):
18163 'data' : data ,
18264 'status' : status ,
18365 'id' : message ['data' ].get ('id' ),
184- },
185- {
186- 'messageType' : 'response' ,
187- 'compress' : options .get ('compress' , False ) if options else False ,
188- },
189- ))
66+ }, message_type = 'response' , compress = compress , ))
19067 else :
19168 raise ValueError ('Connection ID is not available in the message' )
19269
19370 return reply_function
19471
19572
196- class WaitFor :
197- """
198- Class representing a factory for waiting on acknowledgments or replies.
199-
200- The `WaitFor` class provides methods to wait for acknowledgments from the Messaging Gateway
201- or replies from other subscribers or backend services. It is used in conjunction with
202- message publishing and sending methods to ensure reliable communication.
203- """
204-
205- def __init__ (self , client , options ):
206- """
207- Initialize a new instance of the `WaitFor` class.
208-
209- Args:
210- client (RealtimeClient): The `RealtimeClient` instance associated with this factory.
211- options (dict): The message options used for publishing or sending messages.
212- """
213- self .client = client
214- self .options = options
215-
216- async def wait_for_ack (self , timeout = 5 ):
217- """
218- Wait for an acknowledgment event with a timeout.
219-
220- Args:
221- timeout (int, optional): The maximum time to wait for the acknowledgment in seconds. Defaults to 5.
222-
223- Returns:
224- Any: The acknowledgment data received.
225-
226- Raises:
227- TimeoutError: If the acknowledgment is not received within the timeout period.
228- """
229- return await self .client .wait_for (f"ack.{ self .options ['id' ]} " , timeout )
230-
231- async def wait_for_reply (self , timeout = 5 ):
232- """
233- Wait for a reply event with a timeout.
234-
235- Args:
236- timeout (int, optional): The maximum time to wait for the reply in seconds. Defaults to 5.
237-
238- Returns:
239- Any: The reply data received.
240-
241- Raises:
242- TimeoutError: If the reply is not received within the timeout period.
243- """
244- return await self .client .wait_for (f"response.{ self .options ['id' ]} " , timeout )
245-
246-
24773async def wait (ms ):
24874 """
24975 Wait for a specified duration before proceeding.
@@ -367,8 +193,7 @@ async def connect(self):
367193 raise ValueError ('WebSocket URL is not provided' )
368194
369195 try :
370- max_float = float ('inf' )
371- self .ws = await connect (ws_url , max_size = None , ping_interval = None , ping_timeout = None )
196+ self .ws = await connect (ws_url , max_size = None , ping_interval = None , ping_timeout = None )
372197 self .logger .info (f'Connected to WebSocket URL: { ws_url [:80 ]} ...' ) # Masking the URL for security
373198 asyncio .ensure_future (self ._receive_messages ())
374199
@@ -393,7 +218,7 @@ async def disconnect(self):
393218 self .ws = None
394219 self .logger .info ('WebSocket connection closed.' )
395220
396- async def publish (self , topic , payload , options = None ):
221+ async def publish (self , topic , payload , message_type = "broadcast" , compress = False , message_id = None ):
397222 """
398223 Publish a message to a specified topic.
399224
@@ -403,7 +228,9 @@ async def publish(self, topic, payload, options=None):
403228 Args:
404229 topic (str): The topic to publish the message to.
405230 payload (str or dict): The message payload.
406- options (dict, optional): Optional message options, including `id`, `messageType`, and `compress`.
231+ message_type (str, optional): The type of message being published. Defaults to "broadcast".
232+ compress (bool, optional): Whether to compress the message payload. Defaults to False.
233+ message_id (str, optional): The unique identifier for the message. Defaults to auto-generated value.
407234
408235 Returns:
409236 WaitFor: An instance to wait for acknowledgments or replies.
@@ -415,26 +242,26 @@ async def publish(self, topic, payload, options=None):
415242 self .logger .error ('Attempted to publish without an active WebSocket connection.' )
416243 raise Exception ('WebSocket connection is not established' )
417244
418- options = options or {}
419- options [ 'id' ] = options . get ( 'id' , get_random_id () )
245+ if message_id is None :
246+ message_id = get_random_id ()
420247
421248 message = json .dumps ({
422249 'type' : 'publish' ,
423250 'data' : {
424251 'topic' : topic ,
425- 'messageType' : options . get ( 'messageType' ) ,
426- 'compress' : options . get ( ' compress' , False ),
252+ 'messageType' : message_type ,
253+ 'compress' : bool ( compress ),
427254 'payload' : payload ,
428- 'id' : options [ 'id' ] ,
255+ 'id' : message_id ,
429256 },
430257 })
431258
432259 self .logger .debug (f'Publishing message to topic { topic } : { payload } ' )
433260 await self .ws .send (message )
434261
435- return WaitFor (self , options )
262+ return WaitFor (self , message_id )
436263
437- async def send (self , payload , options = None ):
264+ async def send (self , payload , message_type = "broadcast" , compress = False , message_id = None ):
438265 """
439266 Send a message directly to the server.
440267
@@ -443,7 +270,9 @@ async def send(self, payload, options=None):
443270
444271 Args:
445272 payload (str or dict): The message payload.
446- options (dict, optional): Optional message options, including `id`, `messageType`, and `compress`.
273+ message_type (str, optional): The type of message being sent. Defaults to "broadcast".
274+ compress (bool, optional): Whether to compress the message payload. Defaults to False.
275+ message_id (str, optional): The unique identifier for the message. Defaults to auto-generated value.
447276
448277 Returns:
449278 WaitFor: An instance to wait for acknowledgments or replies.
@@ -455,23 +284,23 @@ async def send(self, payload, options=None):
455284 self .logger .error ('Attempted to send without an active WebSocket connection.' )
456285 raise Exception ('WebSocket connection is not established' )
457286
458- options = options or {}
459- options [ 'id' ] = options . get ( 'id' , get_random_id () )
287+ if message_id is None :
288+ message_id = get_random_id ()
460289
461290 message = json .dumps ({
462291 'type' : 'message' ,
463292 'data' : {
464- 'messageType' : options . get ( 'messageType' ) ,
465- 'compress' : options . get ( ' compress' , False ),
293+ 'messageType' : message_type ,
294+ 'compress' : bool ( compress ),
466295 'payload' : payload ,
467- 'id' : options [ 'id' ] ,
296+ 'id' : message_id ,
468297 },
469298 })
470299
471300 self .logger .debug (f'Sending message: { payload } ' )
472301 await self .ws .send (message )
473302
474- return WaitFor (self , options )
303+ return WaitFor (self , message_id )
475304
476305 async def subscribe_remote_topic (self , topic ):
477306 """
0 commit comments