react-broadcast-sync 1.2.1 → 1.4.0

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.
@@ -3,9 +3,10 @@ import {
3
3
  BroadcastActions,
4
4
  BroadcastMessage,
5
5
  BroadcastOptions,
6
- ClearMessage,
7
6
  ClearReceivedMessagesOptions,
8
7
  ClearSentMessagesOptions,
8
+ GetLatestMessageOptions,
9
+ InternalMessage,
9
10
  SendMessageOptions,
10
11
  } from '../types/types';
11
12
  import {
@@ -14,16 +15,27 @@ import {
14
15
  isMessageExpired,
15
16
  createMessage,
16
17
  getInternalMessageType,
17
- isInternalType,
18
18
  isValidInternalClearMessage,
19
19
  debounce,
20
20
  } from '../utils/messageUtils';
21
21
  import { debug } from '../utils/debug';
22
22
 
23
- const INTERNAL_MESSAGE_TYPES: Record<string, ClearMessage> = {
23
+ const INTERNAL_MESSAGE_TYPES: Record<string, InternalMessage> = {
24
24
  CLEAR_SENT_MESSAGES: 'CLEAR_SENT_MESSAGES',
25
+ PING: 'PING',
26
+ PONG: 'PONG',
25
27
  } as const;
26
28
 
29
+ let flushSyncFn: undefined | ((cb: () => void) => void) = undefined;
30
+ if (process.env.NODE_ENV === 'test') {
31
+ try {
32
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
33
+ flushSyncFn = require('react-dom').flushSync;
34
+ } catch {
35
+ // ignore error in test env
36
+ }
37
+ }
38
+
27
39
  export const useBroadcastChannel = (
28
40
  channelName: string,
29
41
  options: BroadcastOptions = {}
@@ -42,23 +54,34 @@ export const useBroadcastChannel = (
42
54
  const [messages, setMessages] = useState<BroadcastMessage[]>([]);
43
55
  const [sentMessages, setSentMessages] = useState<BroadcastMessage[]>([]);
44
56
  const [error, setError] = useState<string | null>(null);
57
+ const [isPingInProgress, setIsPingInProgress] = useState(false);
45
58
 
46
59
  // Refs
47
60
  const channel = useRef<BroadcastChannel | null>(null);
48
61
  const receivedMessageIds = useRef(new Map<string, number>());
49
- const source = useRef(sourceName || generateSourceName()).current;
50
- const internalTypes = useRef({
51
- CLEAR_SENT_MESSAGES: getInternalMessageType(
52
- INTERNAL_MESSAGE_TYPES.CLEAR_SENT_MESSAGES,
53
- channelName,
54
- namespace
55
- ),
56
- }).current;
62
+ const activeSourcesCollector = useRef<Set<string> | null>(null);
63
+
64
+ // Memoized values
65
+ const source = useMemo(() => sourceName || generateSourceName(), [sourceName]);
66
+ const internalTypes = useMemo(
67
+ () => ({
68
+ CLEAR_SENT_MESSAGES: getInternalMessageType(
69
+ INTERNAL_MESSAGE_TYPES.CLEAR_SENT_MESSAGES,
70
+ channelName,
71
+ namespace
72
+ ),
73
+ PING: getInternalMessageType(INTERNAL_MESSAGE_TYPES.PING, channelName, namespace),
74
+ PONG: getInternalMessageType(INTERNAL_MESSAGE_TYPES.PONG, channelName, namespace),
75
+ }),
76
+ [channelName, namespace]
77
+ );
57
78
 
58
79
  const resolvedChannelName = useMemo(() => {
59
80
  return `${channelName}-${namespace}`;
60
81
  }, [channelName, namespace]);
61
82
 
83
+ const stableRegisteredTypes = useMemo(() => registeredTypes, [JSON.stringify(registeredTypes)]);
84
+
62
85
  const performCleanup = useCallback(() => {
63
86
  setMessages(prev => prev.filter(msg => !isMessageExpired(msg)));
64
87
  }, []);
@@ -77,11 +100,51 @@ export const useBroadcastChannel = (
77
100
  setTimeout(() => setError(null), 3000);
78
101
  }, []);
79
102
 
103
+ const ping = useCallback(
104
+ (timeoutMs: number = 300): Promise<string[]> => {
105
+ if (isPingInProgress) {
106
+ debug.ping.inProgress();
107
+ return Promise.resolve([]);
108
+ }
109
+
110
+ if (!channel.current) {
111
+ const error =
112
+ 'BroadcastChannel is not supported in this browser. Please check browser compatibility.';
113
+ debug.error(error);
114
+ setErrorMessage(error);
115
+ return Promise.resolve([]);
116
+ }
117
+
118
+ // Set synchronously before returning the promise
119
+ if (flushSyncFn) {
120
+ flushSyncFn(() => setIsPingInProgress(true));
121
+ } else {
122
+ setIsPingInProgress(true);
123
+ }
124
+ const collector = new Set<string>();
125
+ activeSourcesCollector.current = collector;
126
+
127
+ channel.current.postMessage(createMessage(internalTypes.PING, null, source));
128
+
129
+ return new Promise(resolve => {
130
+ setTimeout(() => {
131
+ setIsPingInProgress(false);
132
+ if (activeSourcesCollector.current === collector) {
133
+ activeSourcesCollector.current = null;
134
+ }
135
+ resolve(Array.from(collector));
136
+ }, timeoutMs);
137
+ });
138
+ },
139
+ [internalTypes.PING, source, setErrorMessage, isPingInProgress, flushSyncFn]
140
+ );
141
+
80
142
  const postMessage = useCallback(
81
143
  (messageType: string, messageContent: any, options: SendMessageOptions = {}) => {
82
144
  const channelCurrent = channel.current;
83
145
  if (!channelCurrent) {
84
- const error = 'Channel not available';
146
+ const error =
147
+ 'BroadcastChannel is not supported in this browser. Please check browser compatibility.';
85
148
  debug.error(error);
86
149
  setErrorMessage(error);
87
150
  return;
@@ -150,6 +213,24 @@ export const useBroadcastChannel = (
150
213
  }
151
214
  }, []);
152
215
 
216
+ const getLatestMessage = useCallback(
217
+ (options: GetLatestMessageOptions = {}) => {
218
+ const { source, type } = options;
219
+
220
+ for (let i = messages.length - 1; i >= 0; i--) {
221
+ const msg = messages[i];
222
+ const sourceMatches = !source || msg.source === source;
223
+ const typeMatches = !type || msg.type === type;
224
+ if (sourceMatches && typeMatches) {
225
+ return msg;
226
+ }
227
+ }
228
+
229
+ return null;
230
+ },
231
+ [messages]
232
+ );
233
+
153
234
  const handleMessage = useCallback(
154
235
  (event: MessageEvent) => {
155
236
  try {
@@ -158,23 +239,12 @@ export const useBroadcastChannel = (
158
239
 
159
240
  if (!isValidMessage(message)) return;
160
241
  if (message.source === source) return;
161
- if (
162
- !isInternalType(message.type) &&
163
- registeredTypes.length > 0 &&
164
- !registeredTypes.includes(message.type)
165
- )
166
- return;
167
- if (isMessageExpired(message)) {
168
- debug.message.expired(message.id);
169
- return;
170
- }
171
-
172
242
  if (isValidInternalClearMessage(message)) {
173
243
  if (message.type === internalTypes.CLEAR_SENT_MESSAGES) {
174
244
  const { ids = [], types = [] } = (message as any).message || {};
175
245
 
176
- setMessages(prev =>
177
- prev.filter(msg => {
246
+ setMessages(prev => [
247
+ ...prev.filter(msg => {
178
248
  // Only consider messages from the same sender
179
249
  if (msg.source !== message.source) return true;
180
250
 
@@ -184,14 +254,35 @@ export const useBroadcastChannel = (
184
254
  const idMatches = ids.length === 0 || ids.includes(msg.id);
185
255
  const typeMatches = types.length === 0 || types.includes(msg.type);
186
256
 
187
- // Remove when BOTH criteria match (wildcards included)
188
257
  return !(idMatches && typeMatches);
189
- })
190
- );
258
+ }),
259
+ ]);
191
260
 
192
- // After handling we don't want to process this internal control packet further
193
261
  return;
194
262
  }
263
+ if (message.type === internalTypes.PING) {
264
+ channel.current?.postMessage(createMessage(internalTypes.PONG, null, source));
265
+ return;
266
+ }
267
+
268
+ if (message.type === internalTypes.PONG) {
269
+ const pongSource = message.source;
270
+ const collector = activeSourcesCollector.current;
271
+
272
+ if (collector && collector.has(pongSource) === false) {
273
+ collector.add(pongSource);
274
+ }
275
+
276
+ return;
277
+ }
278
+ }
279
+ if (stableRegisteredTypes.length > 0 && !stableRegisteredTypes.includes(message.type)) {
280
+ debug.message.ignored(message.type);
281
+ return;
282
+ }
283
+ if (isMessageExpired(message)) {
284
+ debug.message.expired(message.id);
285
+ return;
195
286
  }
196
287
 
197
288
  const now = Date.now();
@@ -209,9 +300,26 @@ export const useBroadcastChannel = (
209
300
  setErrorMessage(error);
210
301
  }
211
302
  },
212
- [source, registeredTypes, keepLatestMessage, setErrorMessage, internalTypes, deduplicationTTL]
303
+ [
304
+ source,
305
+ stableRegisteredTypes,
306
+ keepLatestMessage,
307
+ setErrorMessage,
308
+ internalTypes,
309
+ deduplicationTTL,
310
+ ]
213
311
  );
214
312
 
313
+ const closeChannel = useCallback(() => {
314
+ const bc = channel.current;
315
+ if (bc && typeof bc.close === 'function') {
316
+ bc.removeEventListener('message', handleMessage);
317
+ bc.close();
318
+ debug.channel.closed(resolvedChannelName);
319
+ channel.current = null;
320
+ }
321
+ }, [handleMessage, resolvedChannelName]);
322
+
215
323
  useEffect(() => {
216
324
  if (typeof BroadcastChannel === 'undefined') {
217
325
  const error =
@@ -227,9 +335,7 @@ export const useBroadcastChannel = (
227
335
 
228
336
  bc.addEventListener('message', handleMessage);
229
337
  return () => {
230
- bc.removeEventListener('message', handleMessage);
231
- bc.close();
232
- debug.channel.closed(resolvedChannelName);
338
+ closeChannel();
233
339
  };
234
340
  }, [resolvedChannelName, handleMessage, setErrorMessage]);
235
341
 
@@ -269,10 +375,14 @@ export const useBroadcastChannel = (
269
375
  channelName: resolvedChannelName,
270
376
  messages,
271
377
  sentMessages,
378
+ isPingInProgress,
379
+ ping,
272
380
  postMessage,
273
381
  clearReceivedMessages,
274
382
  clearSentMessages,
383
+ getLatestMessage,
275
384
  error,
385
+ closeChannel,
276
386
  };
277
387
  };
278
388