22import threading
33import time
44
5- from app .resp_parser import bulk_array , bulk_int , bulk_stream_entries , bulk_string , decode_resp
5+ from app .resp_parser import bulk_array , bulk_int , bulk_stream_entries , bulk_string , bulk_xread_response , decode_resp
66
77HOST = "localhost"
88PORT = 6379
1616
1717# Each stream entry: (id, {field: value, ...})
1818stream_store : dict [str , list [tuple [str , dict [str , str ]]]] = {}
19+ stream_condition = threading .Condition ()
1920
2021
2122_XADD_ID_ERROR = b"-ERR The ID specified in XADD is equal or smaller than the target stream top item\r \n "
@@ -141,8 +142,67 @@ def handle_connection(conn: socket.socket) -> None:
141142 conn .sendall (err )
142143 else :
143144 fields = dict (zip (args [3 ::2 ], args [4 ::2 ]))
144- stream_store .setdefault (key , []).append ((entry_id , fields ))
145+ with stream_condition :
146+ stream_store .setdefault (key , []).append ((entry_id , fields ))
147+ stream_condition .notify_all ()
145148 conn .sendall (bulk_string (entry_id ))
149+ case "XREAD" :
150+ i = 1
151+ count = None
152+ if args [i ].upper () == "COUNT" :
153+ count = int (args [i + 1 ])
154+ i += 2
155+ block_ms = None
156+ if args [i ].upper () == "BLOCK" :
157+ block_ms = int (args [i + 1 ])
158+ i += 2
159+ i += 1 # skip STREAMS
160+ remaining = args [i :]
161+ mid = len (remaining ) // 2
162+ keys , start_ids = remaining [:mid ], remaining [mid :]
163+
164+ def _parse_exclusive_id (id_str : str , key : str ) -> tuple [int , int ]:
165+ if id_str == "$" :
166+ entries = stream_store .get (key , [])
167+ if entries :
168+ ms , seq = entries [- 1 ][0 ].split ("-" )
169+ return (int (ms ), int (seq ))
170+ return (0 , 0 )
171+ ms , seq = id_str .split ("-" )
172+ return (int (ms ), int (seq ))
173+
174+ def _read_streams () -> list | None :
175+ result = []
176+ for key , start_id_str in zip (keys , start_ids ):
177+ after = _parse_exclusive_id (start_id_str , key )
178+ entries = [
179+ e for e in stream_store .get (key , [])
180+ if (lambda p : (int (p [0 ]), int (p [1 ])))(e [0 ].split ("-" )) > after
181+ ]
182+ if count is not None :
183+ entries = entries [:count ]
184+ if entries :
185+ result .append ((key , entries ))
186+ return result if result else None
187+
188+ deadline = time .time () + block_ms / 1000 if block_ms is not None and block_ms > 0 else None
189+ with stream_condition :
190+ # Resolve $ IDs before blocking so they capture the current last ID
191+ resolved_ids = [_parse_exclusive_id (sid , k ) for k , sid in zip (keys , start_ids )]
192+ start_ids = [f"{ ms } -{ seq } " for ms , seq in resolved_ids ]
193+
194+ response = _read_streams ()
195+ while response is None and block_ms is not None :
196+ remaining_time = max (0.0 , deadline - time .time ()) if deadline else None
197+ if remaining_time == 0.0 :
198+ break
199+ stream_condition .wait (timeout = remaining_time )
200+ response = _read_streams ()
201+
202+ if response :
203+ conn .sendall (bulk_xread_response (response ))
204+ else :
205+ conn .sendall (b"$-1\r \n " )
146206 case "XRANGE" :
147207 def _parse_id (id_str : str , end : bool = False ) -> tuple [float , float ]:
148208 if id_str == "-" :
@@ -159,7 +219,7 @@ def _parse_id(id_str: str, end: bool = False) -> tuple[float, float]:
159219 count = int (args [5 ]) if len (args ) >= 6 and args [4 ].upper () == "COUNT" else None
160220 result = [
161221 e for e in entries
162- if start <= tuple ( int (x ) for x in e [0 ].split ("-" )) <= end
222+ if start <= ( lambda p : ( int (p [ 0 ]), int ( p [ 1 ])))( e [0 ].split ("-" )) <= end
163223 ]
164224 if count is not None :
165225 result = result [:count ]
0 commit comments