openart-realtime-sdk 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -86,77 +86,87 @@ function RealtimeProvider({
86
86
  connect();
87
87
  }, PING_TIMEOUT_MS);
88
88
  }, []);
89
- const connect = react.useCallback((opts) => {
90
- const { replayEventsSince } = opts ?? { replayEventsSince: Date.now() };
91
- const channels = Array.from(getAllNeededChannels());
92
- if (channels.length === 0) return;
93
- if (reconnectAttemptsRef.current >= maxReconnectAttempts) {
94
- console.log("Max reconnection attempts reached.");
95
- setStatus("error");
96
- return;
97
- }
98
- if (visibilityTimeoutRef.current) {
99
- clearTimeout(visibilityTimeoutRef.current);
100
- visibilityTimeoutRef.current = null;
101
- }
102
- if (eventSourceRef.current) {
103
- eventSourceRef.current.close();
104
- }
105
- setStatus("connecting");
106
- try {
107
- const channelsParam = channels.map((ch) => `channel=${encodeURIComponent(ch)}`).join("&");
108
- const lastAckParam = channels.map((c) => {
109
- const lastAck = lastAckRef.current.get(c) ?? String(replayEventsSince);
110
- return `last_ack_${encodeURIComponent(c)}=${encodeURIComponent(lastAck)}`;
111
- }).join("&");
112
- const url = api.url + "?" + channelsParam + "&" + lastAckParam;
113
- const eventSource = new EventSource(url, {
114
- withCredentials: api.withCredentials ?? false
115
- });
116
- eventSourceRef.current = eventSource;
117
- eventSource.onopen = () => {
118
- reconnectAttemptsRef.current = 0;
119
- setStatus("connected");
120
- resetPingTimeout();
121
- };
122
- eventSource.onmessage = (evt) => {
123
- try {
124
- const payload = JSON.parse(evt.data);
89
+ const connect = react.useCallback(
90
+ (opts) => {
91
+ const { replayEventsSince } = opts ?? { replayEventsSince: Date.now() };
92
+ const channels = Array.from(getAllNeededChannels());
93
+ if (channels.length === 0) return;
94
+ if (reconnectAttemptsRef.current >= maxReconnectAttempts) {
95
+ console.log("Max reconnection attempts reached.");
96
+ setStatus("error");
97
+ return;
98
+ }
99
+ if (visibilityTimeoutRef.current) {
100
+ clearTimeout(visibilityTimeoutRef.current);
101
+ visibilityTimeoutRef.current = null;
102
+ }
103
+ if (eventSourceRef.current) {
104
+ eventSourceRef.current.close();
105
+ }
106
+ setStatus("connecting");
107
+ try {
108
+ const channelsParam = channels.map((ch) => `channel=${encodeURIComponent(ch)}`).join("&");
109
+ const lastAckParam = channels.map((c) => {
110
+ const lastAck = lastAckRef.current.get(c) ?? String(replayEventsSince);
111
+ return `last_ack_${encodeURIComponent(c)}=${encodeURIComponent(
112
+ lastAck
113
+ )}`;
114
+ }).join("&");
115
+ const url = api.url + "?" + channelsParam + "&" + lastAckParam;
116
+ const eventSource = new EventSource(url, {
117
+ withCredentials: api.withCredentials ?? false
118
+ });
119
+ eventSourceRef.current = eventSource;
120
+ eventSource.onopen = () => {
121
+ reconnectAttemptsRef.current = 0;
122
+ setStatus("connected");
125
123
  resetPingTimeout();
126
- handleMessage(payload);
127
- const systemResult = systemEvent.safeParse(payload);
128
- if (systemResult.success) {
129
- if (systemResult.data.type === "reconnect") {
130
- connect({ replayEventsSince: systemResult.data.timestamp });
124
+ };
125
+ eventSource.onmessage = (evt) => {
126
+ try {
127
+ const payload = JSON.parse(evt.data);
128
+ resetPingTimeout();
129
+ handleMessage(payload);
130
+ const systemResult = systemEvent.safeParse(payload);
131
+ if (systemResult.success) {
132
+ if (systemResult.data.type === "reconnect") {
133
+ connect({ replayEventsSince: systemResult.data.timestamp });
134
+ }
131
135
  }
136
+ } catch (error) {
137
+ console.warn("Error parsing message:", error);
132
138
  }
133
- } catch (error) {
134
- console.warn("Error parsing message:", error);
135
- }
136
- };
137
- eventSource.onerror = () => {
138
- if (eventSource !== eventSourceRef.current) return;
139
- const readyState = eventSourceRef.current?.readyState;
140
- if (readyState === EventSource.CONNECTING) return;
141
- if (readyState === EventSource.CLOSED) {
142
- console.log("Connection closed, reconnecting...");
143
- }
144
- setStatus("disconnected");
145
- if (reconnectAttemptsRef.current < maxReconnectAttempts) {
146
- reconnectAttemptsRef.current++;
147
- const timeoutMs = Math.min(1e3 * Math.pow(2, reconnectAttemptsRef.current), 3e4);
148
- console.log(`Reconnecting in ${timeoutMs}ms... (Attempt ${reconnectAttemptsRef.current})`);
149
- reconnectTimeoutRef.current = setTimeout(() => {
150
- connect();
151
- }, timeoutMs);
152
- } else {
153
- setStatus("error");
154
- }
155
- };
156
- } catch (error) {
157
- setStatus("error");
158
- }
159
- }, [getAllNeededChannels, maxReconnectAttempts, api.url, api.withCredentials]);
139
+ };
140
+ eventSource.onerror = () => {
141
+ if (eventSource !== eventSourceRef.current) return;
142
+ const readyState = eventSourceRef.current?.readyState;
143
+ if (readyState === EventSource.CONNECTING) return;
144
+ if (readyState === EventSource.CLOSED) {
145
+ console.log("Connection closed, reconnecting...");
146
+ }
147
+ setStatus("disconnected");
148
+ if (reconnectAttemptsRef.current < maxReconnectAttempts) {
149
+ reconnectAttemptsRef.current++;
150
+ const timeoutMs = Math.min(
151
+ 1e3 * Math.pow(2, reconnectAttemptsRef.current),
152
+ 3e4
153
+ );
154
+ console.log(
155
+ `Reconnecting in ${timeoutMs}ms... (Attempt ${reconnectAttemptsRef.current})`
156
+ );
157
+ reconnectTimeoutRef.current = setTimeout(() => {
158
+ connect();
159
+ }, timeoutMs);
160
+ } else {
161
+ setStatus("error");
162
+ }
163
+ };
164
+ } catch (error) {
165
+ setStatus("error");
166
+ }
167
+ },
168
+ [getAllNeededChannels, maxReconnectAttempts, api.url, api.withCredentials]
169
+ );
160
170
  const debouncedConnect = react.useCallback(() => {
161
171
  if (debounceTimeoutRef.current) {
162
172
  clearTimeout(debounceTimeoutRef.current);
@@ -173,7 +183,8 @@ function RealtimeProvider({
173
183
  if (!disconnectOnWindowHidden || typeof document === "undefined") return;
174
184
  const handleVisibilityChange = () => {
175
185
  if (document.hidden) {
176
- if (visibilityTimeoutRef.current) clearTimeout(visibilityTimeoutRef.current);
186
+ if (visibilityTimeoutRef.current)
187
+ clearTimeout(visibilityTimeoutRef.current);
177
188
  visibilityTimeoutRef.current = setTimeout(() => {
178
189
  console.log("Window hidden for too long, disconnecting...");
179
190
  cleanup();
@@ -192,7 +203,8 @@ function RealtimeProvider({
192
203
  document.addEventListener("visibilitychange", handleVisibilityChange);
193
204
  return () => {
194
205
  document.removeEventListener("visibilitychange", handleVisibilityChange);
195
- if (visibilityTimeoutRef.current) clearTimeout(visibilityTimeoutRef.current);
206
+ if (visibilityTimeoutRef.current)
207
+ clearTimeout(visibilityTimeoutRef.current);
196
208
  };
197
209
  }, [disconnectOnWindowHidden, disconnectDelay, connect, status]);
198
210
  const handleMessage = (payload) => {
@@ -262,7 +274,9 @@ function RealtimeProvider({
262
274
  function useRealtimeContext() {
263
275
  const context = react.useContext(RealtimeContext);
264
276
  if (!context) {
265
- throw new Error("useRealtimeContext must be used within a RealtimeProvider");
277
+ throw new Error(
278
+ "useRealtimeContext must be used within a RealtimeProvider"
279
+ );
266
280
  }
267
281
  return context;
268
282
  }
@@ -279,7 +293,9 @@ function useRealtime(opts) {
279
293
  "useRealtime: No RealtimeProvider found. Wrap your app in <RealtimeProvider> to use Realtime."
280
294
  );
281
295
  }
282
- const registrationId = react.useRef(Math.random().toString(36).substring(2)).current;
296
+ const registrationId = react.useRef(
297
+ Math.random().toString(36).substring(2)
298
+ ).current;
283
299
  const onDataRef = react.useRef(onData);
284
300
  onDataRef.current = onData;
285
301
  react.useEffect(() => {
@@ -296,7 +312,11 @@ function useRealtime(opts) {
296
312
  if (events && events.length > 0 && !events.includes(event)) {
297
313
  return;
298
314
  }
299
- const payload = { event, data, channel };
315
+ const payload = {
316
+ event,
317
+ data,
318
+ channel
319
+ };
300
320
  onDataRef.current?.(payload);
301
321
  }
302
322
  });
@@ -39,7 +39,7 @@ interface RealtimeProviderProps {
39
39
  */
40
40
  disconnectDelay?: number;
41
41
  }
42
- declare function RealtimeProvider({ children, api, maxReconnectAttempts, disconnectOnWindowHidden, disconnectDelay }: RealtimeProviderProps): react_jsx_runtime.JSX.Element;
42
+ declare function RealtimeProvider({ children, api, maxReconnectAttempts, disconnectOnWindowHidden, disconnectDelay, }: RealtimeProviderProps): react_jsx_runtime.JSX.Element;
43
43
  declare function useRealtimeContext(): RealtimeContextValue;
44
44
  declare const createRealtime: <T extends Record<string, unknown>>() => {
45
45
  useRealtime: <const E extends EventPaths<T>>(opts: UseRealtimeOpts<T, E>) => {
@@ -39,7 +39,7 @@ interface RealtimeProviderProps {
39
39
  */
40
40
  disconnectDelay?: number;
41
41
  }
42
- declare function RealtimeProvider({ children, api, maxReconnectAttempts, disconnectOnWindowHidden, disconnectDelay }: RealtimeProviderProps): react_jsx_runtime.JSX.Element;
42
+ declare function RealtimeProvider({ children, api, maxReconnectAttempts, disconnectOnWindowHidden, disconnectDelay, }: RealtimeProviderProps): react_jsx_runtime.JSX.Element;
43
43
  declare function useRealtimeContext(): RealtimeContextValue;
44
44
  declare const createRealtime: <T extends Record<string, unknown>>() => {
45
45
  useRealtime: <const E extends EventPaths<T>>(opts: UseRealtimeOpts<T, E>) => {
@@ -80,77 +80,87 @@ function RealtimeProvider({
80
80
  connect();
81
81
  }, PING_TIMEOUT_MS);
82
82
  }, []);
83
- const connect = useCallback((opts) => {
84
- const { replayEventsSince } = opts ?? { replayEventsSince: Date.now() };
85
- const channels = Array.from(getAllNeededChannels());
86
- if (channels.length === 0) return;
87
- if (reconnectAttemptsRef.current >= maxReconnectAttempts) {
88
- console.log("Max reconnection attempts reached.");
89
- setStatus("error");
90
- return;
91
- }
92
- if (visibilityTimeoutRef.current) {
93
- clearTimeout(visibilityTimeoutRef.current);
94
- visibilityTimeoutRef.current = null;
95
- }
96
- if (eventSourceRef.current) {
97
- eventSourceRef.current.close();
98
- }
99
- setStatus("connecting");
100
- try {
101
- const channelsParam = channels.map((ch) => `channel=${encodeURIComponent(ch)}`).join("&");
102
- const lastAckParam = channels.map((c) => {
103
- const lastAck = lastAckRef.current.get(c) ?? String(replayEventsSince);
104
- return `last_ack_${encodeURIComponent(c)}=${encodeURIComponent(lastAck)}`;
105
- }).join("&");
106
- const url = api.url + "?" + channelsParam + "&" + lastAckParam;
107
- const eventSource = new EventSource(url, {
108
- withCredentials: api.withCredentials ?? false
109
- });
110
- eventSourceRef.current = eventSource;
111
- eventSource.onopen = () => {
112
- reconnectAttemptsRef.current = 0;
113
- setStatus("connected");
114
- resetPingTimeout();
115
- };
116
- eventSource.onmessage = (evt) => {
117
- try {
118
- const payload = JSON.parse(evt.data);
83
+ const connect = useCallback(
84
+ (opts) => {
85
+ const { replayEventsSince } = opts ?? { replayEventsSince: Date.now() };
86
+ const channels = Array.from(getAllNeededChannels());
87
+ if (channels.length === 0) return;
88
+ if (reconnectAttemptsRef.current >= maxReconnectAttempts) {
89
+ console.log("Max reconnection attempts reached.");
90
+ setStatus("error");
91
+ return;
92
+ }
93
+ if (visibilityTimeoutRef.current) {
94
+ clearTimeout(visibilityTimeoutRef.current);
95
+ visibilityTimeoutRef.current = null;
96
+ }
97
+ if (eventSourceRef.current) {
98
+ eventSourceRef.current.close();
99
+ }
100
+ setStatus("connecting");
101
+ try {
102
+ const channelsParam = channels.map((ch) => `channel=${encodeURIComponent(ch)}`).join("&");
103
+ const lastAckParam = channels.map((c) => {
104
+ const lastAck = lastAckRef.current.get(c) ?? String(replayEventsSince);
105
+ return `last_ack_${encodeURIComponent(c)}=${encodeURIComponent(
106
+ lastAck
107
+ )}`;
108
+ }).join("&");
109
+ const url = api.url + "?" + channelsParam + "&" + lastAckParam;
110
+ const eventSource = new EventSource(url, {
111
+ withCredentials: api.withCredentials ?? false
112
+ });
113
+ eventSourceRef.current = eventSource;
114
+ eventSource.onopen = () => {
115
+ reconnectAttemptsRef.current = 0;
116
+ setStatus("connected");
119
117
  resetPingTimeout();
120
- handleMessage(payload);
121
- const systemResult = systemEvent.safeParse(payload);
122
- if (systemResult.success) {
123
- if (systemResult.data.type === "reconnect") {
124
- connect({ replayEventsSince: systemResult.data.timestamp });
118
+ };
119
+ eventSource.onmessage = (evt) => {
120
+ try {
121
+ const payload = JSON.parse(evt.data);
122
+ resetPingTimeout();
123
+ handleMessage(payload);
124
+ const systemResult = systemEvent.safeParse(payload);
125
+ if (systemResult.success) {
126
+ if (systemResult.data.type === "reconnect") {
127
+ connect({ replayEventsSince: systemResult.data.timestamp });
128
+ }
125
129
  }
130
+ } catch (error) {
131
+ console.warn("Error parsing message:", error);
126
132
  }
127
- } catch (error) {
128
- console.warn("Error parsing message:", error);
129
- }
130
- };
131
- eventSource.onerror = () => {
132
- if (eventSource !== eventSourceRef.current) return;
133
- const readyState = eventSourceRef.current?.readyState;
134
- if (readyState === EventSource.CONNECTING) return;
135
- if (readyState === EventSource.CLOSED) {
136
- console.log("Connection closed, reconnecting...");
137
- }
138
- setStatus("disconnected");
139
- if (reconnectAttemptsRef.current < maxReconnectAttempts) {
140
- reconnectAttemptsRef.current++;
141
- const timeoutMs = Math.min(1e3 * Math.pow(2, reconnectAttemptsRef.current), 3e4);
142
- console.log(`Reconnecting in ${timeoutMs}ms... (Attempt ${reconnectAttemptsRef.current})`);
143
- reconnectTimeoutRef.current = setTimeout(() => {
144
- connect();
145
- }, timeoutMs);
146
- } else {
147
- setStatus("error");
148
- }
149
- };
150
- } catch (error) {
151
- setStatus("error");
152
- }
153
- }, [getAllNeededChannels, maxReconnectAttempts, api.url, api.withCredentials]);
133
+ };
134
+ eventSource.onerror = () => {
135
+ if (eventSource !== eventSourceRef.current) return;
136
+ const readyState = eventSourceRef.current?.readyState;
137
+ if (readyState === EventSource.CONNECTING) return;
138
+ if (readyState === EventSource.CLOSED) {
139
+ console.log("Connection closed, reconnecting...");
140
+ }
141
+ setStatus("disconnected");
142
+ if (reconnectAttemptsRef.current < maxReconnectAttempts) {
143
+ reconnectAttemptsRef.current++;
144
+ const timeoutMs = Math.min(
145
+ 1e3 * Math.pow(2, reconnectAttemptsRef.current),
146
+ 3e4
147
+ );
148
+ console.log(
149
+ `Reconnecting in ${timeoutMs}ms... (Attempt ${reconnectAttemptsRef.current})`
150
+ );
151
+ reconnectTimeoutRef.current = setTimeout(() => {
152
+ connect();
153
+ }, timeoutMs);
154
+ } else {
155
+ setStatus("error");
156
+ }
157
+ };
158
+ } catch (error) {
159
+ setStatus("error");
160
+ }
161
+ },
162
+ [getAllNeededChannels, maxReconnectAttempts, api.url, api.withCredentials]
163
+ );
154
164
  const debouncedConnect = useCallback(() => {
155
165
  if (debounceTimeoutRef.current) {
156
166
  clearTimeout(debounceTimeoutRef.current);
@@ -167,7 +177,8 @@ function RealtimeProvider({
167
177
  if (!disconnectOnWindowHidden || typeof document === "undefined") return;
168
178
  const handleVisibilityChange = () => {
169
179
  if (document.hidden) {
170
- if (visibilityTimeoutRef.current) clearTimeout(visibilityTimeoutRef.current);
180
+ if (visibilityTimeoutRef.current)
181
+ clearTimeout(visibilityTimeoutRef.current);
171
182
  visibilityTimeoutRef.current = setTimeout(() => {
172
183
  console.log("Window hidden for too long, disconnecting...");
173
184
  cleanup();
@@ -186,7 +197,8 @@ function RealtimeProvider({
186
197
  document.addEventListener("visibilitychange", handleVisibilityChange);
187
198
  return () => {
188
199
  document.removeEventListener("visibilitychange", handleVisibilityChange);
189
- if (visibilityTimeoutRef.current) clearTimeout(visibilityTimeoutRef.current);
200
+ if (visibilityTimeoutRef.current)
201
+ clearTimeout(visibilityTimeoutRef.current);
190
202
  };
191
203
  }, [disconnectOnWindowHidden, disconnectDelay, connect, status]);
192
204
  const handleMessage = (payload) => {
@@ -256,7 +268,9 @@ function RealtimeProvider({
256
268
  function useRealtimeContext() {
257
269
  const context = useContext(RealtimeContext);
258
270
  if (!context) {
259
- throw new Error("useRealtimeContext must be used within a RealtimeProvider");
271
+ throw new Error(
272
+ "useRealtimeContext must be used within a RealtimeProvider"
273
+ );
260
274
  }
261
275
  return context;
262
276
  }
@@ -273,7 +287,9 @@ function useRealtime(opts) {
273
287
  "useRealtime: No RealtimeProvider found. Wrap your app in <RealtimeProvider> to use Realtime."
274
288
  );
275
289
  }
276
- const registrationId = useRef(Math.random().toString(36).substring(2)).current;
290
+ const registrationId = useRef(
291
+ Math.random().toString(36).substring(2)
292
+ ).current;
277
293
  const onDataRef = useRef(onData);
278
294
  onDataRef.current = onData;
279
295
  useEffect(() => {
@@ -290,7 +306,11 @@ function useRealtime(opts) {
290
306
  if (events && events.length > 0 && !events.includes(event)) {
291
307
  return;
292
308
  }
293
- const payload = { event, data, channel };
309
+ const payload = {
310
+ event,
311
+ data,
312
+ channel
313
+ };
294
314
  onDataRef.current?.(payload);
295
315
  }
296
316
  });
@@ -132,6 +132,10 @@ function handle(config) {
132
132
  request.signal.addEventListener("abort", handleAbort);
133
133
  const safeEnqueue = (data) => {
134
134
  if (isClosed) return;
135
+ if (controller.desiredSize && controller.desiredSize <= 0) {
136
+ logger.warn?.("\u26A0\uFE0F Client too slow, dropping message to prevent OOM.");
137
+ return;
138
+ }
135
139
  try {
136
140
  controller.enqueue(data);
137
141
  } catch (err) {
@@ -152,15 +156,15 @@ function handle(config) {
152
156
  let buffer = [];
153
157
  let isHistoryReplayed = false;
154
158
  const lastHistoryIds = /* @__PURE__ */ new Map();
155
- const onManagerMessage = (message) => {
159
+ const onManagerMessage = (message, encodedMessage) => {
156
160
  logger.debug?.("\u2B07\uFE0F Received event:", message);
157
161
  if (!isHistoryReplayed) {
158
162
  buffer.push(message);
159
163
  } else {
160
- safeEnqueue(json(message));
164
+ safeEnqueue(encodedMessage);
161
165
  }
162
166
  };
163
- const fetchHistory = async () => {
167
+ const executeHistoryPipeline = async () => {
164
168
  const pipeline = redis.pipeline();
165
169
  const channelAcks = /* @__PURE__ */ new Map();
166
170
  for (const channel of channels) {
@@ -171,36 +175,53 @@ function handle(config) {
171
175
  safeEnqueue(json(connectedEvent));
172
176
  const lastAck = searchParams.get(`last_ack_${channel}`) ?? String(Date.now());
173
177
  channelAcks.set(channel, lastAck);
174
- pipeline.xrange(channel, `(${lastAck}`, "+", "COUNT", maxRecoveryLimit);
178
+ pipeline.xrange(
179
+ channel,
180
+ `(${lastAck}`,
181
+ "+",
182
+ "COUNT",
183
+ maxRecoveryLimit
184
+ );
175
185
  }
176
186
  try {
177
- const results = await pipeline.exec();
178
- if (results) {
179
- results.forEach((result, index) => {
180
- const [err, rawMissing] = result;
181
- const channel = channels[index];
182
- if (!channel) return;
183
- if (err) {
184
- logger.error(`Error fetching history for channel ${channel}:`, err);
185
- return;
186
- }
187
- const missingMessages = parseStreamResponse(rawMissing);
188
- if (missingMessages.length > 0) {
189
- missingMessages.forEach((value) => {
190
- const eventWithId = value;
191
- const event = userEvent.safeParse(eventWithId);
192
- if (event.success) safeEnqueue(json(event.data));
193
- });
194
- lastHistoryIds.set(channel, missingMessages[missingMessages.length - 1]?.id ?? "");
195
- }
196
- });
197
- }
187
+ return await pipeline.exec();
198
188
  } catch (error) {
199
189
  logger.error("Error executing history pipeline:", error);
190
+ return null;
200
191
  }
192
+ };
193
+ const processHistoryResults = (results) => {
194
+ if (!results) return;
195
+ results.forEach((result, index) => {
196
+ const [err, rawMissing] = result;
197
+ const channel = channels[index];
198
+ if (!channel) return;
199
+ if (err) {
200
+ logger.error(
201
+ `Error fetching history for channel ${channel}:`,
202
+ err
203
+ );
204
+ return;
205
+ }
206
+ const missingMessages = parseStreamResponse(rawMissing);
207
+ if (missingMessages.length > 0) {
208
+ missingMessages.forEach((value) => {
209
+ const eventWithId = value;
210
+ const event = userEvent.safeParse(eventWithId);
211
+ if (event.success) safeEnqueue(json(event.data));
212
+ });
213
+ lastHistoryIds.set(
214
+ channel,
215
+ missingMessages[missingMessages.length - 1]?.id ?? ""
216
+ );
217
+ }
218
+ });
219
+ };
220
+ const flushBuffer = () => {
201
221
  for (const msg of buffer) {
202
222
  const channelLastId = lastHistoryIds.get(msg.channel);
203
- if (channelLastId && compareStreamIds(msg.id, channelLastId) <= 0) continue;
223
+ if (channelLastId && compareStreamIds(msg.id, channelLastId) <= 0)
224
+ continue;
204
225
  safeEnqueue(json(msg));
205
226
  }
206
227
  buffer = [];
@@ -208,11 +229,20 @@ function handle(config) {
208
229
  logger.info("\u2705 Subscription established:", { channels });
209
230
  };
210
231
  try {
211
- await Promise.all(channels.map(async (channel) => {
212
- const unsub = await subscriptionManager.subscribe(channel, onManagerMessage);
213
- unsubs.push(unsub);
214
- }));
215
- await fetchHistory();
232
+ const [_, historyResults] = await Promise.all([
233
+ Promise.all(
234
+ channels.map(async (channel) => {
235
+ const unsub = await subscriptionManager.subscribe(
236
+ channel,
237
+ onManagerMessage
238
+ );
239
+ unsubs.push(unsub);
240
+ })
241
+ ),
242
+ executeHistoryPipeline()
243
+ ]);
244
+ processHistoryResults(historyResults);
245
+ flushBuffer();
216
246
  } catch (err) {
217
247
  const errorMessage = err instanceof Error ? err.message : "Unknown error";
218
248
  logger.error("\u26A0\uFE0F Redis subscriber error:", errorMessage);
@@ -238,8 +268,9 @@ function handle(config) {
238
268
  return new StreamingResponse(stream);
239
269
  };
240
270
  }
271
+ var encoder = new TextEncoder();
241
272
  function json(data) {
242
- return new TextEncoder().encode(`data: ${JSON.stringify(data)}
273
+ return encoder.encode(`data: ${JSON.stringify(data)}
243
274
 
244
275
  `);
245
276
  }
@@ -262,17 +293,18 @@ var StreamingResponse = class extends Response {
262
293
  };
263
294
 
264
295
  // src/server/subscription-manager.ts
296
+ var encoder2 = new TextEncoder();
265
297
  var SubscriptionManager = class {
266
298
  redis;
267
299
  subRedis;
268
300
  // Map<ChannelName, Set<Listener>>
269
301
  listeners = /* @__PURE__ */ new Map();
270
302
  unsubscribeTimers = /* @__PURE__ */ new Map();
271
- verbose;
272
- constructor(redis, verbose = false) {
303
+ logger;
304
+ constructor(redis, logger) {
273
305
  this.redis = redis;
274
306
  this.subRedis = redis.duplicate();
275
- this.verbose = verbose;
307
+ this.logger = logger;
276
308
  this.setupMessageListener();
277
309
  }
278
310
  setupMessageListener() {
@@ -292,34 +324,49 @@ var SubscriptionManager = class {
292
324
  }
293
325
  const result = userEvent.safeParse(payload);
294
326
  if (result.success) {
295
- if (this.verbose) {
296
- console.log(`[SubscriptionManager] Dispatching message to ${handlers.size} listeners on ${channel}`);
297
- }
327
+ const encodedMessage = encoder2.encode(
328
+ `data: ${JSON.stringify(result.data)}
329
+
330
+ `
331
+ );
332
+ this.logger.debug?.(
333
+ `[SubscriptionManager] Dispatching message to ${handlers.size} listeners on ${channel}`
334
+ );
298
335
  handlers.forEach((listener) => {
299
336
  try {
300
- listener(result.data);
337
+ listener(result.data, encodedMessage);
301
338
  } catch (listenerErr) {
302
- console.error(`[SubscriptionManager] Error in listener for ${channel}:`, listenerErr);
339
+ this.logger.error(
340
+ `[SubscriptionManager] Error in listener for ${channel}:`,
341
+ listenerErr
342
+ );
303
343
  }
304
344
  });
305
345
  }
306
346
  } catch (err) {
307
- console.error(`[SubscriptionManager] Error processing message on ${channel}:`, err);
347
+ this.logger.error(
348
+ `[SubscriptionManager] Error processing message on ${channel}:`,
349
+ err
350
+ );
308
351
  }
309
352
  });
310
353
  this.subRedis.on("error", (err) => {
311
- console.error("[SubscriptionManager] Redis subscription error:", err);
354
+ this.logger.error("[SubscriptionManager] Redis subscription error:", err);
312
355
  });
313
356
  }
314
357
  async subscribe(channel, listener) {
315
358
  if (this.unsubscribeTimers.has(channel)) {
316
359
  clearTimeout(this.unsubscribeTimers.get(channel));
317
360
  this.unsubscribeTimers.delete(channel);
318
- if (this.verbose) console.log(`[SubscriptionManager] Cancelled pending unsubscribe for: ${channel}`);
361
+ this.logger.debug?.(
362
+ `[SubscriptionManager] Cancelled pending unsubscribe for: ${channel}`
363
+ );
319
364
  }
320
365
  if (!this.listeners.has(channel)) {
321
366
  this.listeners.set(channel, /* @__PURE__ */ new Set());
322
- if (this.verbose) console.log(`[SubscriptionManager] Subscribing to Redis channel: ${channel}`);
367
+ this.logger.debug?.(
368
+ `[SubscriptionManager] Subscribing to Redis channel: ${channel}`
369
+ );
323
370
  await this.subRedis.subscribe(channel);
324
371
  }
325
372
  const channelListeners = this.listeners.get(channel);
@@ -335,9 +382,14 @@ var SubscriptionManager = class {
335
382
  const timer = setTimeout(() => {
336
383
  this.listeners.delete(channel);
337
384
  this.unsubscribeTimers.delete(channel);
338
- if (this.verbose) console.log(`[SubscriptionManager] Unsubscribing from Redis channel: ${channel}`);
385
+ this.logger.debug?.(
386
+ `[SubscriptionManager] Unsubscribing from Redis channel: ${channel}`
387
+ );
339
388
  this.subRedis.unsubscribe(channel).catch((err) => {
340
- console.error(`[SubscriptionManager] Error unsubscribing from ${channel}:`, err);
389
+ this.logger.error(
390
+ `[SubscriptionManager] Error unsubscribing from ${channel}:`,
391
+ err
392
+ );
341
393
  });
342
394
  }, 2e3);
343
395
  this.unsubscribeTimers.set(channel, timer);
@@ -387,7 +439,10 @@ var RealtimeBase = class {
387
439
  }
388
440
  };
389
441
  if (this._redis) {
390
- this._subscriptionManager = new SubscriptionManager(this._redis, this._verbose);
442
+ this._subscriptionManager = new SubscriptionManager(
443
+ this._redis,
444
+ this._logger
445
+ );
391
446
  }
392
447
  Object.assign(this, this.createEventHandlers("default"));
393
448
  }
@@ -400,7 +455,10 @@ var RealtimeBase = class {
400
455
  let pingInterval = void 0;
401
456
  const startPingInterval = () => {
402
457
  pingInterval = setInterval(() => {
403
- this._redis?.publish(channel, JSON.stringify({ type: "ping", timestamp: Date.now() }));
458
+ this._redis?.publish(
459
+ channel,
460
+ JSON.stringify({ type: "ping", timestamp: Date.now() })
461
+ );
404
462
  }, 6e4);
405
463
  };
406
464
  const stopPingInterval = () => {
@@ -412,7 +470,13 @@ var RealtimeBase = class {
412
470
  const start = args?.start ? String(args.start) : "-";
413
471
  const end = args?.end ? String(args.end) : "+";
414
472
  const limit = args?.limit ?? 1e3;
415
- const rawHistory = await redis.xrange(channel, start, end, "COUNT", limit);
473
+ const rawHistory = await redis.xrange(
474
+ channel,
475
+ start,
476
+ end,
477
+ "COUNT",
478
+ limit
479
+ );
416
480
  const historyMessages = parseStreamResponse(rawHistory);
417
481
  return historyMessages.map((value) => {
418
482
  if (typeof value === "object" && value !== null) {
@@ -456,17 +520,26 @@ var RealtimeBase = class {
456
520
  const limit = typeof history === "object" ? history.limit : void 0;
457
521
  let rawMessages = [];
458
522
  if (limit) {
459
- rawMessages = await redis.xrange(channel, start, end, "COUNT", limit);
523
+ rawMessages = await redis.xrange(
524
+ channel,
525
+ start,
526
+ end,
527
+ "COUNT",
528
+ limit
529
+ );
460
530
  } else {
461
531
  rawMessages = await redis.xrange(channel, start, end);
462
532
  }
463
533
  const messages = parseStreamResponse(rawMessages);
464
534
  for (const message of messages) {
465
535
  const typedMessage = message;
466
- if (!typedMessage.event || events && !events.includes(typedMessage.event)) continue;
536
+ if (!typedMessage.event || events && !events.includes(typedMessage.event))
537
+ continue;
467
538
  const result = userEvent.safeParse(message);
468
539
  if (result.success) {
469
- onData(result.data);
540
+ onData(
541
+ result.data
542
+ );
470
543
  }
471
544
  }
472
545
  if (messages.length > 0) {
@@ -107,6 +107,10 @@ function handle(config) {
107
107
  request.signal.addEventListener("abort", handleAbort);
108
108
  const safeEnqueue = (data) => {
109
109
  if (isClosed) return;
110
+ if (controller.desiredSize && controller.desiredSize <= 0) {
111
+ logger.warn?.("\u26A0\uFE0F Client too slow, dropping message to prevent OOM.");
112
+ return;
113
+ }
110
114
  try {
111
115
  controller.enqueue(data);
112
116
  } catch (err) {
@@ -127,15 +131,15 @@ function handle(config) {
127
131
  let buffer = [];
128
132
  let isHistoryReplayed = false;
129
133
  const lastHistoryIds = /* @__PURE__ */ new Map();
130
- const onManagerMessage = (message) => {
134
+ const onManagerMessage = (message, encodedMessage) => {
131
135
  logger.debug?.("\u2B07\uFE0F Received event:", message);
132
136
  if (!isHistoryReplayed) {
133
137
  buffer.push(message);
134
138
  } else {
135
- safeEnqueue(json(message));
139
+ safeEnqueue(encodedMessage);
136
140
  }
137
141
  };
138
- const fetchHistory = async () => {
142
+ const executeHistoryPipeline = async () => {
139
143
  const pipeline = redis.pipeline();
140
144
  const channelAcks = /* @__PURE__ */ new Map();
141
145
  for (const channel of channels) {
@@ -146,36 +150,53 @@ function handle(config) {
146
150
  safeEnqueue(json(connectedEvent));
147
151
  const lastAck = searchParams.get(`last_ack_${channel}`) ?? String(Date.now());
148
152
  channelAcks.set(channel, lastAck);
149
- pipeline.xrange(channel, `(${lastAck}`, "+", "COUNT", maxRecoveryLimit);
153
+ pipeline.xrange(
154
+ channel,
155
+ `(${lastAck}`,
156
+ "+",
157
+ "COUNT",
158
+ maxRecoveryLimit
159
+ );
150
160
  }
151
161
  try {
152
- const results = await pipeline.exec();
153
- if (results) {
154
- results.forEach((result, index) => {
155
- const [err, rawMissing] = result;
156
- const channel = channels[index];
157
- if (!channel) return;
158
- if (err) {
159
- logger.error(`Error fetching history for channel ${channel}:`, err);
160
- return;
161
- }
162
- const missingMessages = parseStreamResponse(rawMissing);
163
- if (missingMessages.length > 0) {
164
- missingMessages.forEach((value) => {
165
- const eventWithId = value;
166
- const event = userEvent.safeParse(eventWithId);
167
- if (event.success) safeEnqueue(json(event.data));
168
- });
169
- lastHistoryIds.set(channel, missingMessages[missingMessages.length - 1]?.id ?? "");
170
- }
171
- });
172
- }
162
+ return await pipeline.exec();
173
163
  } catch (error) {
174
164
  logger.error("Error executing history pipeline:", error);
165
+ return null;
175
166
  }
167
+ };
168
+ const processHistoryResults = (results) => {
169
+ if (!results) return;
170
+ results.forEach((result, index) => {
171
+ const [err, rawMissing] = result;
172
+ const channel = channels[index];
173
+ if (!channel) return;
174
+ if (err) {
175
+ logger.error(
176
+ `Error fetching history for channel ${channel}:`,
177
+ err
178
+ );
179
+ return;
180
+ }
181
+ const missingMessages = parseStreamResponse(rawMissing);
182
+ if (missingMessages.length > 0) {
183
+ missingMessages.forEach((value) => {
184
+ const eventWithId = value;
185
+ const event = userEvent.safeParse(eventWithId);
186
+ if (event.success) safeEnqueue(json(event.data));
187
+ });
188
+ lastHistoryIds.set(
189
+ channel,
190
+ missingMessages[missingMessages.length - 1]?.id ?? ""
191
+ );
192
+ }
193
+ });
194
+ };
195
+ const flushBuffer = () => {
176
196
  for (const msg of buffer) {
177
197
  const channelLastId = lastHistoryIds.get(msg.channel);
178
- if (channelLastId && compareStreamIds(msg.id, channelLastId) <= 0) continue;
198
+ if (channelLastId && compareStreamIds(msg.id, channelLastId) <= 0)
199
+ continue;
179
200
  safeEnqueue(json(msg));
180
201
  }
181
202
  buffer = [];
@@ -183,11 +204,20 @@ function handle(config) {
183
204
  logger.info("\u2705 Subscription established:", { channels });
184
205
  };
185
206
  try {
186
- await Promise.all(channels.map(async (channel) => {
187
- const unsub = await subscriptionManager.subscribe(channel, onManagerMessage);
188
- unsubs.push(unsub);
189
- }));
190
- await fetchHistory();
207
+ const [_, historyResults] = await Promise.all([
208
+ Promise.all(
209
+ channels.map(async (channel) => {
210
+ const unsub = await subscriptionManager.subscribe(
211
+ channel,
212
+ onManagerMessage
213
+ );
214
+ unsubs.push(unsub);
215
+ })
216
+ ),
217
+ executeHistoryPipeline()
218
+ ]);
219
+ processHistoryResults(historyResults);
220
+ flushBuffer();
191
221
  } catch (err) {
192
222
  const errorMessage = err instanceof Error ? err.message : "Unknown error";
193
223
  logger.error("\u26A0\uFE0F Redis subscriber error:", errorMessage);
@@ -213,8 +243,9 @@ function handle(config) {
213
243
  return new StreamingResponse(stream);
214
244
  };
215
245
  }
246
+ var encoder = new TextEncoder();
216
247
  function json(data) {
217
- return new TextEncoder().encode(`data: ${JSON.stringify(data)}
248
+ return encoder.encode(`data: ${JSON.stringify(data)}
218
249
 
219
250
  `);
220
251
  }
@@ -237,17 +268,18 @@ var StreamingResponse = class extends Response {
237
268
  };
238
269
 
239
270
  // src/server/subscription-manager.ts
271
+ var encoder2 = new TextEncoder();
240
272
  var SubscriptionManager = class {
241
273
  redis;
242
274
  subRedis;
243
275
  // Map<ChannelName, Set<Listener>>
244
276
  listeners = /* @__PURE__ */ new Map();
245
277
  unsubscribeTimers = /* @__PURE__ */ new Map();
246
- verbose;
247
- constructor(redis, verbose = false) {
278
+ logger;
279
+ constructor(redis, logger) {
248
280
  this.redis = redis;
249
281
  this.subRedis = redis.duplicate();
250
- this.verbose = verbose;
282
+ this.logger = logger;
251
283
  this.setupMessageListener();
252
284
  }
253
285
  setupMessageListener() {
@@ -267,34 +299,49 @@ var SubscriptionManager = class {
267
299
  }
268
300
  const result = userEvent.safeParse(payload);
269
301
  if (result.success) {
270
- if (this.verbose) {
271
- console.log(`[SubscriptionManager] Dispatching message to ${handlers.size} listeners on ${channel}`);
272
- }
302
+ const encodedMessage = encoder2.encode(
303
+ `data: ${JSON.stringify(result.data)}
304
+
305
+ `
306
+ );
307
+ this.logger.debug?.(
308
+ `[SubscriptionManager] Dispatching message to ${handlers.size} listeners on ${channel}`
309
+ );
273
310
  handlers.forEach((listener) => {
274
311
  try {
275
- listener(result.data);
312
+ listener(result.data, encodedMessage);
276
313
  } catch (listenerErr) {
277
- console.error(`[SubscriptionManager] Error in listener for ${channel}:`, listenerErr);
314
+ this.logger.error(
315
+ `[SubscriptionManager] Error in listener for ${channel}:`,
316
+ listenerErr
317
+ );
278
318
  }
279
319
  });
280
320
  }
281
321
  } catch (err) {
282
- console.error(`[SubscriptionManager] Error processing message on ${channel}:`, err);
322
+ this.logger.error(
323
+ `[SubscriptionManager] Error processing message on ${channel}:`,
324
+ err
325
+ );
283
326
  }
284
327
  });
285
328
  this.subRedis.on("error", (err) => {
286
- console.error("[SubscriptionManager] Redis subscription error:", err);
329
+ this.logger.error("[SubscriptionManager] Redis subscription error:", err);
287
330
  });
288
331
  }
289
332
  async subscribe(channel, listener) {
290
333
  if (this.unsubscribeTimers.has(channel)) {
291
334
  clearTimeout(this.unsubscribeTimers.get(channel));
292
335
  this.unsubscribeTimers.delete(channel);
293
- if (this.verbose) console.log(`[SubscriptionManager] Cancelled pending unsubscribe for: ${channel}`);
336
+ this.logger.debug?.(
337
+ `[SubscriptionManager] Cancelled pending unsubscribe for: ${channel}`
338
+ );
294
339
  }
295
340
  if (!this.listeners.has(channel)) {
296
341
  this.listeners.set(channel, /* @__PURE__ */ new Set());
297
- if (this.verbose) console.log(`[SubscriptionManager] Subscribing to Redis channel: ${channel}`);
342
+ this.logger.debug?.(
343
+ `[SubscriptionManager] Subscribing to Redis channel: ${channel}`
344
+ );
298
345
  await this.subRedis.subscribe(channel);
299
346
  }
300
347
  const channelListeners = this.listeners.get(channel);
@@ -310,9 +357,14 @@ var SubscriptionManager = class {
310
357
  const timer = setTimeout(() => {
311
358
  this.listeners.delete(channel);
312
359
  this.unsubscribeTimers.delete(channel);
313
- if (this.verbose) console.log(`[SubscriptionManager] Unsubscribing from Redis channel: ${channel}`);
360
+ this.logger.debug?.(
361
+ `[SubscriptionManager] Unsubscribing from Redis channel: ${channel}`
362
+ );
314
363
  this.subRedis.unsubscribe(channel).catch((err) => {
315
- console.error(`[SubscriptionManager] Error unsubscribing from ${channel}:`, err);
364
+ this.logger.error(
365
+ `[SubscriptionManager] Error unsubscribing from ${channel}:`,
366
+ err
367
+ );
316
368
  });
317
369
  }, 2e3);
318
370
  this.unsubscribeTimers.set(channel, timer);
@@ -362,7 +414,10 @@ var RealtimeBase = class {
362
414
  }
363
415
  };
364
416
  if (this._redis) {
365
- this._subscriptionManager = new SubscriptionManager(this._redis, this._verbose);
417
+ this._subscriptionManager = new SubscriptionManager(
418
+ this._redis,
419
+ this._logger
420
+ );
366
421
  }
367
422
  Object.assign(this, this.createEventHandlers("default"));
368
423
  }
@@ -375,7 +430,10 @@ var RealtimeBase = class {
375
430
  let pingInterval = void 0;
376
431
  const startPingInterval = () => {
377
432
  pingInterval = setInterval(() => {
378
- this._redis?.publish(channel, JSON.stringify({ type: "ping", timestamp: Date.now() }));
433
+ this._redis?.publish(
434
+ channel,
435
+ JSON.stringify({ type: "ping", timestamp: Date.now() })
436
+ );
379
437
  }, 6e4);
380
438
  };
381
439
  const stopPingInterval = () => {
@@ -387,7 +445,13 @@ var RealtimeBase = class {
387
445
  const start = args?.start ? String(args.start) : "-";
388
446
  const end = args?.end ? String(args.end) : "+";
389
447
  const limit = args?.limit ?? 1e3;
390
- const rawHistory = await redis.xrange(channel, start, end, "COUNT", limit);
448
+ const rawHistory = await redis.xrange(
449
+ channel,
450
+ start,
451
+ end,
452
+ "COUNT",
453
+ limit
454
+ );
391
455
  const historyMessages = parseStreamResponse(rawHistory);
392
456
  return historyMessages.map((value) => {
393
457
  if (typeof value === "object" && value !== null) {
@@ -431,17 +495,26 @@ var RealtimeBase = class {
431
495
  const limit = typeof history === "object" ? history.limit : void 0;
432
496
  let rawMessages = [];
433
497
  if (limit) {
434
- rawMessages = await redis.xrange(channel, start, end, "COUNT", limit);
498
+ rawMessages = await redis.xrange(
499
+ channel,
500
+ start,
501
+ end,
502
+ "COUNT",
503
+ limit
504
+ );
435
505
  } else {
436
506
  rawMessages = await redis.xrange(channel, start, end);
437
507
  }
438
508
  const messages = parseStreamResponse(rawMessages);
439
509
  for (const message of messages) {
440
510
  const typedMessage = message;
441
- if (!typedMessage.event || events && !events.includes(typedMessage.event)) continue;
511
+ if (!typedMessage.event || events && !events.includes(typedMessage.event))
512
+ continue;
442
513
  const result = userEvent.safeParse(message);
443
514
  if (result.success) {
444
- onData(result.data);
515
+ onData(
516
+ result.data
517
+ );
445
518
  }
446
519
  }
447
520
  if (messages.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openart-realtime-sdk",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "author": "OpenArt",
5
5
  "repository": {
6
6
  "type": "git",