15
15
import json # JSON encoding and decoding
16
16
import secrets # Generate secure random numbers
17
17
import 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.
105
18
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
137
20
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
141
23
142
24
143
25
def reply (client , message ):
@@ -158,14 +40,14 @@ def reply(client, message):
158
40
ValueError: If the connection ID is not available in the incoming message.
159
41
"""
160
42
161
- def reply_function (data , status = 'ok' , options = None ):
43
+ def reply_function (data , status = 'ok' , compress = False ):
162
44
"""
163
45
Sends a reply message back to the sender of the original message.
164
46
165
47
Args:
166
48
data: The payload data to send in the reply.
167
49
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 .
169
51
170
52
Returns:
171
53
WaitFor: An instance to wait for acknowledgments or replies.
@@ -181,69 +63,13 @@ def reply_function(data, status='ok', options=None):
181
63
'data' : data ,
182
64
'status' : status ,
183
65
'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 , ))
190
67
else :
191
68
raise ValueError ('Connection ID is not available in the message' )
192
69
193
70
return reply_function
194
71
195
72
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
-
247
73
async def wait (ms ):
248
74
"""
249
75
Wait for a specified duration before proceeding.
@@ -367,8 +193,7 @@ async def connect(self):
367
193
raise ValueError ('WebSocket URL is not provided' )
368
194
369
195
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 )
372
197
self .logger .info (f'Connected to WebSocket URL: { ws_url [:80 ]} ...' ) # Masking the URL for security
373
198
asyncio .ensure_future (self ._receive_messages ())
374
199
@@ -393,7 +218,7 @@ async def disconnect(self):
393
218
self .ws = None
394
219
self .logger .info ('WebSocket connection closed.' )
395
220
396
- async def publish (self , topic , payload , options = None ):
221
+ async def publish (self , topic , payload , message_type = "broadcast" , compress = False , message_id = None ):
397
222
"""
398
223
Publish a message to a specified topic.
399
224
@@ -403,7 +228,9 @@ async def publish(self, topic, payload, options=None):
403
228
Args:
404
229
topic (str): The topic to publish the message to.
405
230
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.
407
234
408
235
Returns:
409
236
WaitFor: An instance to wait for acknowledgments or replies.
@@ -415,26 +242,26 @@ async def publish(self, topic, payload, options=None):
415
242
self .logger .error ('Attempted to publish without an active WebSocket connection.' )
416
243
raise Exception ('WebSocket connection is not established' )
417
244
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 ()
420
247
421
248
message = json .dumps ({
422
249
'type' : 'publish' ,
423
250
'data' : {
424
251
'topic' : topic ,
425
- 'messageType' : options . get ( 'messageType' ) ,
426
- 'compress' : options . get ( ' compress' , False ),
252
+ 'messageType' : message_type ,
253
+ 'compress' : bool ( compress ),
427
254
'payload' : payload ,
428
- 'id' : options [ 'id' ] ,
255
+ 'id' : message_id ,
429
256
},
430
257
})
431
258
432
259
self .logger .debug (f'Publishing message to topic { topic } : { payload } ' )
433
260
await self .ws .send (message )
434
261
435
- return WaitFor (self , options )
262
+ return WaitFor (self , message_id )
436
263
437
- async def send (self , payload , options = None ):
264
+ async def send (self , payload , message_type = "broadcast" , compress = False , message_id = None ):
438
265
"""
439
266
Send a message directly to the server.
440
267
@@ -443,7 +270,9 @@ async def send(self, payload, options=None):
443
270
444
271
Args:
445
272
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.
447
276
448
277
Returns:
449
278
WaitFor: An instance to wait for acknowledgments or replies.
@@ -455,23 +284,23 @@ async def send(self, payload, options=None):
455
284
self .logger .error ('Attempted to send without an active WebSocket connection.' )
456
285
raise Exception ('WebSocket connection is not established' )
457
286
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 ()
460
289
461
290
message = json .dumps ({
462
291
'type' : 'message' ,
463
292
'data' : {
464
- 'messageType' : options . get ( 'messageType' ) ,
465
- 'compress' : options . get ( ' compress' , False ),
293
+ 'messageType' : message_type ,
294
+ 'compress' : bool ( compress ),
466
295
'payload' : payload ,
467
- 'id' : options [ 'id' ] ,
296
+ 'id' : message_id ,
468
297
},
469
298
})
470
299
471
300
self .logger .debug (f'Sending message: { payload } ' )
472
301
await self .ws .send (message )
473
302
474
- return WaitFor (self , options )
303
+ return WaitFor (self , message_id )
475
304
476
305
async def subscribe_remote_topic (self , topic ):
477
306
"""
0 commit comments