react-broadcast-sync 1.5.1 → 1.8.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.
- package/README.md +59 -11
- package/dist/BroadcastProvider.d.ts +2 -7
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/types/types.d.ts +17 -0
- package/dist/utils/debug.d.ts +7 -1
- package/package.json +6 -1
- package/.eslintignore +0 -5
- package/.github/workflows/ci.yml +0 -61
- package/.github/workflows/release.yml +0 -39
- package/.husky/pre-commit +0 -5
- package/.nvmrc +0 -1
- package/.prettierignore +0 -4
- package/.prettierrc +0 -9
- package/.releaserc.json +0 -22
- package/CHANGELOG.md +0 -90
- package/assets/react-broadcast-sync-logo.png +0 -0
- package/commitlint.config.js +0 -3
- package/demo/react-broadcast-sync-demo/README.md +0 -130
- package/demo/react-broadcast-sync-demo/index.html +0 -14
- package/demo/react-broadcast-sync-demo/package-lock.json +0 -6379
- package/demo/react-broadcast-sync-demo/package.json +0 -42
- package/demo/react-broadcast-sync-demo/public/assets/react-broadcast-sync-demo-video.gif +0 -0
- package/demo/react-broadcast-sync-demo/public/assets/react-broadcast-sync-logo.png +0 -0
- package/demo/react-broadcast-sync-demo/src/App.tsx +0 -277
- package/demo/react-broadcast-sync-demo/src/components/Counter.tsx +0 -63
- package/demo/react-broadcast-sync-demo/src/components/TextSync.tsx +0 -61
- package/demo/react-broadcast-sync-demo/src/components/TodoList.tsx +0 -186
- package/demo/react-broadcast-sync-demo/src/hooks/useCounterBroadcast.ts +0 -96
- package/demo/react-broadcast-sync-demo/src/hooks/useTextBroadcast.ts +0 -37
- package/demo/react-broadcast-sync-demo/src/hooks/useTodoBroadcast.ts +0 -249
- package/demo/react-broadcast-sync-demo/src/main.tsx +0 -11
- package/demo/react-broadcast-sync-demo/src/theme.ts +0 -62
- package/demo/react-broadcast-sync-demo/tsconfig.json +0 -26
- package/demo/react-broadcast-sync-demo/tsconfig.node.json +0 -10
- package/demo/react-broadcast-sync-demo/vite.config.ts +0 -12
- package/eslint.config.js +0 -38
- package/jest.config.cjs +0 -9
- package/jest.setup.ts +0 -1
- package/rollup.config.mjs +0 -98
- package/src/BroadcastProvider.tsx +0 -80
- package/src/hooks/useBroadcastChannel.ts +0 -464
- package/src/index.ts +0 -9
- package/src/tests/BroadcastProvider.test.tsx +0 -163
- package/src/tests/debug.test.ts +0 -239
- package/src/tests/messageUtils.test.ts +0 -252
- package/src/tests/useBroadcastChannel.same-tab.test.tsx +0 -992
- package/src/tests/useBroadcastChannel.test.tsx +0 -1243
- package/src/types/types.ts +0 -124
- package/src/utils/debug.ts +0 -54
- package/src/utils/messageUtils.ts +0 -122
- package/tsconfig.json +0 -36
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
|
|
2
|
-
import { useBroadcastChannel } from './hooks/useBroadcastChannel';
|
|
3
|
-
import { BroadcastMessage } from './types/types';
|
|
4
|
-
|
|
5
|
-
interface BroadcastContextValue {
|
|
6
|
-
messages: BroadcastMessage[];
|
|
7
|
-
error: string | null;
|
|
8
|
-
channel: BroadcastChannel | null;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const BroadcastChannelContext = createContext<BroadcastContextValue | undefined>(undefined);
|
|
12
|
-
|
|
13
|
-
interface ErrorBoundaryProps {
|
|
14
|
-
children: React.ReactNode;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
class ErrorBoundary extends React.Component<ErrorBoundaryProps, { hasError: boolean }> {
|
|
18
|
-
state = { hasError: false };
|
|
19
|
-
|
|
20
|
-
static getDerivedStateFromError() {
|
|
21
|
-
return { hasError: true };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
componentDidCatch(error: Error) {
|
|
25
|
-
console.error('BroadcastChannel error:', error);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
render() {
|
|
29
|
-
if (this.state.hasError) {
|
|
30
|
-
return <div>Error occurred in BroadcastProvider</div>;
|
|
31
|
-
}
|
|
32
|
-
return this.props.children;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface BroadcastProviderProps {
|
|
37
|
-
channelName: string;
|
|
38
|
-
children: React.ReactNode;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const BroadcastProvider: React.FC<BroadcastProviderProps> = ({ children, channelName }) => {
|
|
42
|
-
const { messages, error: hookError } = useBroadcastChannel(channelName);
|
|
43
|
-
|
|
44
|
-
const [error, setError] = React.useState<string | null>(null);
|
|
45
|
-
const channel = useRef<BroadcastChannel | null>(null);
|
|
46
|
-
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
if (typeof BroadcastChannel === 'undefined') {
|
|
49
|
-
setError(
|
|
50
|
-
'BroadcastChannel is not supported in this browser. Please check browser compatibility.'
|
|
51
|
-
);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const bc = new BroadcastChannel(channelName);
|
|
55
|
-
channel.current = bc;
|
|
56
|
-
|
|
57
|
-
return () => {
|
|
58
|
-
bc.close();
|
|
59
|
-
};
|
|
60
|
-
}, [channelName]);
|
|
61
|
-
|
|
62
|
-
const value = useMemo(
|
|
63
|
-
() => ({ messages, error: error || hookError, channel: channel.current }),
|
|
64
|
-
[messages, error, hookError]
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<ErrorBoundary>
|
|
69
|
-
<BroadcastChannelContext.Provider value={value}>{children}</BroadcastChannelContext.Provider>
|
|
70
|
-
</ErrorBoundary>
|
|
71
|
-
);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export const useBroadcastProvider = (): BroadcastContextValue => {
|
|
75
|
-
const context = useContext(BroadcastChannelContext);
|
|
76
|
-
if (!context) {
|
|
77
|
-
throw new Error('useBroadcastProvider must be used within a BroadcastProvider');
|
|
78
|
-
}
|
|
79
|
-
return context;
|
|
80
|
-
};
|
|
@@ -1,464 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
BroadcastActions,
|
|
4
|
-
BroadcastMessage,
|
|
5
|
-
BroadcastOptions,
|
|
6
|
-
ClearReceivedMessagesOptions,
|
|
7
|
-
ClearSentMessagesOptions,
|
|
8
|
-
GetLatestMessageOptions,
|
|
9
|
-
InternalMessage,
|
|
10
|
-
SendMessageOptions,
|
|
11
|
-
} from '../types/types';
|
|
12
|
-
import {
|
|
13
|
-
generateSourceName,
|
|
14
|
-
isValidMessage,
|
|
15
|
-
isMessageExpired,
|
|
16
|
-
createMessage,
|
|
17
|
-
getInternalMessageType,
|
|
18
|
-
isValidInternalClearMessage,
|
|
19
|
-
debounce,
|
|
20
|
-
} from '../utils/messageUtils';
|
|
21
|
-
import { debug } from '../utils/debug';
|
|
22
|
-
|
|
23
|
-
const INTERNAL_MESSAGE_TYPES: Record<string, InternalMessage> = {
|
|
24
|
-
CLEAR_SENT_MESSAGES: 'CLEAR_SENT_MESSAGES',
|
|
25
|
-
PING: 'PING',
|
|
26
|
-
PONG: 'PONG',
|
|
27
|
-
} as const;
|
|
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
|
-
|
|
39
|
-
/**
|
|
40
|
-
* useBroadcastChannel hook
|
|
41
|
-
*
|
|
42
|
-
* Note: When batching is enabled (batchingDelayMs > 0), messages sent over the channel may be received as either:
|
|
43
|
-
* - a single BroadcastMessage (object), or
|
|
44
|
-
* - an array of BroadcastMessage (batch)
|
|
45
|
-
*
|
|
46
|
-
* The event handler in this hook supports both formats. If you listen to the channel directly, always check:
|
|
47
|
-
* if (Array.isArray(event.data)) { ... } else { ... }
|
|
48
|
-
*/
|
|
49
|
-
export const useBroadcastChannel = (
|
|
50
|
-
channelName: string,
|
|
51
|
-
options: BroadcastOptions = {}
|
|
52
|
-
): BroadcastActions => {
|
|
53
|
-
const {
|
|
54
|
-
sourceName,
|
|
55
|
-
cleaningInterval = 1000,
|
|
56
|
-
keepLatestMessage = false,
|
|
57
|
-
registeredTypes = [],
|
|
58
|
-
namespace = '',
|
|
59
|
-
deduplicationTTL = 5 * 60 * 1000,
|
|
60
|
-
cleanupDebounceMs = 0,
|
|
61
|
-
batchingDelayMs = 20,
|
|
62
|
-
excludedBatchMessageTypes = [],
|
|
63
|
-
} = options;
|
|
64
|
-
|
|
65
|
-
// State
|
|
66
|
-
const [messages, setMessages] = useState<BroadcastMessage[]>([]);
|
|
67
|
-
const [sentMessages, setSentMessages] = useState<BroadcastMessage[]>([]);
|
|
68
|
-
const [error, setError] = useState<string | null>(null);
|
|
69
|
-
const [isPingInProgress, setIsPingInProgress] = useState(false);
|
|
70
|
-
|
|
71
|
-
// Refs
|
|
72
|
-
const channel = useRef<BroadcastChannel | null>(null);
|
|
73
|
-
const receivedMessageIds = useRef(new Map<string, number>());
|
|
74
|
-
const activeSourcesCollector = useRef<Set<string> | null>(null);
|
|
75
|
-
const batchingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
76
|
-
const batchingMessagesRef = useRef<BroadcastMessage[]>([]);
|
|
77
|
-
const batchingErrorRef = useRef(false);
|
|
78
|
-
|
|
79
|
-
// Memoized values
|
|
80
|
-
const source = useMemo(() => sourceName || generateSourceName(), [sourceName]);
|
|
81
|
-
const internalTypes = useMemo(
|
|
82
|
-
() => ({
|
|
83
|
-
CLEAR_SENT_MESSAGES: getInternalMessageType(
|
|
84
|
-
INTERNAL_MESSAGE_TYPES.CLEAR_SENT_MESSAGES,
|
|
85
|
-
channelName,
|
|
86
|
-
namespace
|
|
87
|
-
),
|
|
88
|
-
PING: getInternalMessageType(INTERNAL_MESSAGE_TYPES.PING, channelName, namespace),
|
|
89
|
-
PONG: getInternalMessageType(INTERNAL_MESSAGE_TYPES.PONG, channelName, namespace),
|
|
90
|
-
}),
|
|
91
|
-
[channelName, namespace]
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
const resolvedChannelName = useMemo(() => {
|
|
95
|
-
// Only append dash if namespace is non-empty
|
|
96
|
-
return namespace ? `${channelName}-${namespace}` : channelName;
|
|
97
|
-
}, [channelName, namespace]);
|
|
98
|
-
|
|
99
|
-
const stableRegisteredTypes = useMemo(() => registeredTypes, [JSON.stringify(registeredTypes)]);
|
|
100
|
-
|
|
101
|
-
const performCleanup = useCallback(() => {
|
|
102
|
-
setMessages(prev => prev.filter(msg => !isMessageExpired(msg)));
|
|
103
|
-
}, []);
|
|
104
|
-
|
|
105
|
-
const debouncedCleanup = useRef<{
|
|
106
|
-
(...args: Parameters<typeof performCleanup>): void;
|
|
107
|
-
cancel: () => void;
|
|
108
|
-
}>(
|
|
109
|
-
cleanupDebounceMs > 0
|
|
110
|
-
? debounce(performCleanup, cleanupDebounceMs)
|
|
111
|
-
: Object.assign(performCleanup, { cancel: () => {} })
|
|
112
|
-
).current;
|
|
113
|
-
|
|
114
|
-
const setErrorMessage = useCallback((error: string) => {
|
|
115
|
-
setError(error);
|
|
116
|
-
setTimeout(() => setError(null), 3000);
|
|
117
|
-
}, []);
|
|
118
|
-
|
|
119
|
-
const ping = useCallback(
|
|
120
|
-
(timeoutMs: number = 300): Promise<string[]> => {
|
|
121
|
-
if (isPingInProgress) {
|
|
122
|
-
debug.ping.inProgress();
|
|
123
|
-
return Promise.resolve([]);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (!channel.current) {
|
|
127
|
-
const error =
|
|
128
|
-
'BroadcastChannel is not supported in this browser. Please check browser compatibility.';
|
|
129
|
-
debug.error(error);
|
|
130
|
-
setErrorMessage(error);
|
|
131
|
-
return Promise.resolve([]);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Set synchronously before returning the promise
|
|
135
|
-
if (flushSyncFn) {
|
|
136
|
-
flushSyncFn(() => setIsPingInProgress(true));
|
|
137
|
-
} else {
|
|
138
|
-
setIsPingInProgress(true);
|
|
139
|
-
}
|
|
140
|
-
const collector = new Set<string>();
|
|
141
|
-
activeSourcesCollector.current = collector;
|
|
142
|
-
|
|
143
|
-
channel.current.postMessage(createMessage(internalTypes.PING, null, source));
|
|
144
|
-
|
|
145
|
-
return new Promise(resolve => {
|
|
146
|
-
setTimeout(() => {
|
|
147
|
-
setIsPingInProgress(false);
|
|
148
|
-
if (activeSourcesCollector.current === collector) {
|
|
149
|
-
activeSourcesCollector.current = null;
|
|
150
|
-
}
|
|
151
|
-
resolve(Array.from(collector));
|
|
152
|
-
}, timeoutMs);
|
|
153
|
-
});
|
|
154
|
-
},
|
|
155
|
-
[internalTypes.PING, source, setErrorMessage, isPingInProgress, flushSyncFn]
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
const postMessage = useCallback(
|
|
159
|
-
(messageType: string, messageContent: any, options: SendMessageOptions = {}) => {
|
|
160
|
-
const channelCurrent = channel.current;
|
|
161
|
-
if (!channelCurrent) {
|
|
162
|
-
const error =
|
|
163
|
-
'BroadcastChannel is not supported in this browser. Please check browser compatibility.';
|
|
164
|
-
debug.error(error);
|
|
165
|
-
setErrorMessage(error);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const message = createMessage(messageType, messageContent, source, options);
|
|
170
|
-
// Only batch if batchingDelayMs > 0 and messageType is not excluded
|
|
171
|
-
const shouldSendImmediately =
|
|
172
|
-
!batchingDelayMs || batchingDelayMs < 0 || excludedBatchMessageTypes.includes(messageType);
|
|
173
|
-
if (shouldSendImmediately) {
|
|
174
|
-
try {
|
|
175
|
-
channelCurrent.postMessage(message);
|
|
176
|
-
} catch (e) {
|
|
177
|
-
const error = 'Failed to send message';
|
|
178
|
-
debug.error(error);
|
|
179
|
-
setErrorMessage(error);
|
|
180
|
-
batchingErrorRef.current = true;
|
|
181
|
-
}
|
|
182
|
-
} else {
|
|
183
|
-
batchingMessagesRef.current.push(message);
|
|
184
|
-
if (!batchingTimeoutRef.current) {
|
|
185
|
-
batchingTimeoutRef.current = setTimeout(() => {
|
|
186
|
-
if (batchingErrorRef.current) {
|
|
187
|
-
batchingMessagesRef.current = [];
|
|
188
|
-
batchingTimeoutRef.current = null;
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
try {
|
|
192
|
-
channelCurrent.postMessage(batchingMessagesRef.current);
|
|
193
|
-
} catch (e) {
|
|
194
|
-
const error = 'Failed to send message';
|
|
195
|
-
debug.error(error);
|
|
196
|
-
setErrorMessage(error);
|
|
197
|
-
batchingErrorRef.current = true;
|
|
198
|
-
}
|
|
199
|
-
batchingMessagesRef.current = [];
|
|
200
|
-
batchingTimeoutRef.current = null;
|
|
201
|
-
}, batchingDelayMs);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
debug.message.sent(message);
|
|
205
|
-
setSentMessages(prev => [...prev, message]);
|
|
206
|
-
},
|
|
207
|
-
[source, setErrorMessage, batchingDelayMs, excludedBatchMessageTypes]
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
const clearReceivedMessages = useCallback((options: ClearReceivedMessagesOptions = {}) => {
|
|
211
|
-
const hasFilters = Boolean(
|
|
212
|
-
(options.ids && options.ids.length) ||
|
|
213
|
-
(options.types && options.types.length) ||
|
|
214
|
-
(options.sources && options.sources.length)
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
setMessages(prev =>
|
|
218
|
-
hasFilters
|
|
219
|
-
? prev.filter(
|
|
220
|
-
msg =>
|
|
221
|
-
!(options.ids && options.ids.includes(msg.id)) &&
|
|
222
|
-
!(options.types && options.types.includes(msg.type)) &&
|
|
223
|
-
!(options.sources && options.sources.includes(msg.source))
|
|
224
|
-
)
|
|
225
|
-
: []
|
|
226
|
-
);
|
|
227
|
-
}, []);
|
|
228
|
-
|
|
229
|
-
const clearSentMessages = useCallback((options: ClearSentMessagesOptions = {}) => {
|
|
230
|
-
const { ids = [], types = [], sync = false } = options ?? {};
|
|
231
|
-
setSentMessages(prev =>
|
|
232
|
-
ids.length > 0 || types.length > 0
|
|
233
|
-
? prev.filter(msg => {
|
|
234
|
-
// Only consider messages from the same sender
|
|
235
|
-
if (msg.source !== source) return true;
|
|
236
|
-
|
|
237
|
-
// Decide whether this message should be cleared:
|
|
238
|
-
// - If the ids array is empty, treat as wildcard (match all ids)
|
|
239
|
-
// - If the types array is empty, treat as wildcard (match all types)
|
|
240
|
-
const idMatches = ids.length === 0 || ids.includes(msg.id);
|
|
241
|
-
const typeMatches = types.length === 0 || types.includes(msg.type);
|
|
242
|
-
|
|
243
|
-
// Remove when BOTH criteria match (wildcards included)
|
|
244
|
-
return !(idMatches && typeMatches);
|
|
245
|
-
})
|
|
246
|
-
: []
|
|
247
|
-
);
|
|
248
|
-
if (sync) {
|
|
249
|
-
channel.current?.postMessage(
|
|
250
|
-
createMessage(
|
|
251
|
-
internalTypes.CLEAR_SENT_MESSAGES,
|
|
252
|
-
{ ids: options.ids ?? [], types: options.types ?? [] },
|
|
253
|
-
source
|
|
254
|
-
)
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
}, []);
|
|
258
|
-
|
|
259
|
-
const getLatestMessage = useCallback(
|
|
260
|
-
(options: GetLatestMessageOptions = {}) => {
|
|
261
|
-
const { source, type } = options;
|
|
262
|
-
|
|
263
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
264
|
-
const msg = messages[i];
|
|
265
|
-
const sourceMatches = !source || msg.source === source;
|
|
266
|
-
const typeMatches = !type || msg.type === type;
|
|
267
|
-
if (sourceMatches && typeMatches) {
|
|
268
|
-
return msg;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return null;
|
|
273
|
-
},
|
|
274
|
-
[messages]
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
const handleMessage = useCallback(
|
|
278
|
-
(event: MessageEvent) => {
|
|
279
|
-
try {
|
|
280
|
-
const message: BroadcastMessage = event.data;
|
|
281
|
-
debug.message.received(message);
|
|
282
|
-
|
|
283
|
-
if (!isValidMessage(message)) return;
|
|
284
|
-
if (message.source === source) return;
|
|
285
|
-
if (isValidInternalClearMessage(message)) {
|
|
286
|
-
if (message.type === internalTypes.CLEAR_SENT_MESSAGES) {
|
|
287
|
-
const { ids = [], types = [] } = (message as any).message || {};
|
|
288
|
-
|
|
289
|
-
setMessages(prev => [
|
|
290
|
-
...prev.filter(msg => {
|
|
291
|
-
// Only consider messages from the same sender
|
|
292
|
-
if (msg.source !== message.source) return true;
|
|
293
|
-
|
|
294
|
-
// Decide whether this message should be cleared:
|
|
295
|
-
// - If the ids array is empty, treat as wildcard (match all ids)
|
|
296
|
-
// - If the types array is empty, treat as wildcard (match all types)
|
|
297
|
-
const idMatches = ids.length === 0 || ids.includes(msg.id);
|
|
298
|
-
const typeMatches = types.length === 0 || types.includes(msg.type);
|
|
299
|
-
|
|
300
|
-
return !(idMatches && typeMatches);
|
|
301
|
-
}),
|
|
302
|
-
]);
|
|
303
|
-
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (message.type === internalTypes.PING) {
|
|
307
|
-
channel.current?.postMessage(createMessage(internalTypes.PONG, null, source));
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (message.type === internalTypes.PONG) {
|
|
312
|
-
const pongSource = message.source;
|
|
313
|
-
const collector = activeSourcesCollector.current;
|
|
314
|
-
|
|
315
|
-
if (collector && collector.has(pongSource) === false) {
|
|
316
|
-
collector.add(pongSource);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
if (stableRegisteredTypes.length > 0 && !stableRegisteredTypes.includes(message.type)) {
|
|
323
|
-
debug.message.ignored(message.type);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
if (isMessageExpired(message)) {
|
|
327
|
-
debug.message.expired(message.id);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const now = Date.now();
|
|
332
|
-
const receivedAt = receivedMessageIds.current.get(message.id);
|
|
333
|
-
if (receivedAt && now - receivedAt < deduplicationTTL) {
|
|
334
|
-
debug.message.duplicate(message.id);
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
receivedMessageIds.current.set(message.id, now);
|
|
339
|
-
setMessages(prev => (keepLatestMessage ? [message] : [...prev, message]));
|
|
340
|
-
} catch (e) {
|
|
341
|
-
const error = 'Error processing broadcast message';
|
|
342
|
-
debug.error(error);
|
|
343
|
-
setErrorMessage(error);
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
[
|
|
347
|
-
source,
|
|
348
|
-
stableRegisteredTypes,
|
|
349
|
-
keepLatestMessage,
|
|
350
|
-
setErrorMessage,
|
|
351
|
-
internalTypes,
|
|
352
|
-
deduplicationTTL,
|
|
353
|
-
]
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
const closeChannel = useCallback(() => {
|
|
357
|
-
const bc = channel.current;
|
|
358
|
-
if (bc && typeof bc.close === 'function') {
|
|
359
|
-
bc.removeEventListener('message', handleMessage);
|
|
360
|
-
bc.close();
|
|
361
|
-
debug.channel.closed(resolvedChannelName);
|
|
362
|
-
channel.current = null;
|
|
363
|
-
}
|
|
364
|
-
}, [handleMessage, resolvedChannelName]);
|
|
365
|
-
|
|
366
|
-
useEffect(() => {
|
|
367
|
-
if (typeof BroadcastChannel === 'undefined') {
|
|
368
|
-
const error =
|
|
369
|
-
'BroadcastChannel is not supported in this browser. Please check browser compatibility.';
|
|
370
|
-
debug.error(error);
|
|
371
|
-
setErrorMessage(error);
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const bc = new BroadcastChannel(resolvedChannelName);
|
|
376
|
-
channel.current = bc;
|
|
377
|
-
debug.channel.created(resolvedChannelName);
|
|
378
|
-
|
|
379
|
-
bc.addEventListener('message', (event: MessageEvent) => {
|
|
380
|
-
// event.data may be a single message or an array of messages (batch)
|
|
381
|
-
if (Array.isArray(event.data)) {
|
|
382
|
-
event.data.forEach((message: BroadcastMessage) => {
|
|
383
|
-
handleMessage({ data: message } as MessageEvent<BroadcastMessage>);
|
|
384
|
-
});
|
|
385
|
-
} else {
|
|
386
|
-
handleMessage(event);
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
return () => {
|
|
390
|
-
closeChannel();
|
|
391
|
-
};
|
|
392
|
-
}, [resolvedChannelName, handleMessage, setErrorMessage]);
|
|
393
|
-
|
|
394
|
-
useEffect(() => {
|
|
395
|
-
if (cleaningInterval <= 0) return;
|
|
396
|
-
|
|
397
|
-
const interval = setInterval(() => {
|
|
398
|
-
debug.cleanup.started();
|
|
399
|
-
const before = messages.length;
|
|
400
|
-
debouncedCleanup();
|
|
401
|
-
debug.cleanup.completed(before - messages.length);
|
|
402
|
-
}, cleaningInterval);
|
|
403
|
-
|
|
404
|
-
return () => {
|
|
405
|
-
clearInterval(interval);
|
|
406
|
-
// Cancel any pending debounced cleanup on unmount
|
|
407
|
-
if (cleanupDebounceMs > 0) {
|
|
408
|
-
debouncedCleanup.cancel?.();
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
}, [cleaningInterval, debouncedCleanup, cleanupDebounceMs]);
|
|
412
|
-
|
|
413
|
-
useEffect(() => {
|
|
414
|
-
const interval = setInterval(() => {
|
|
415
|
-
const now = Date.now();
|
|
416
|
-
for (const [key, timestamp] of receivedMessageIds.current.entries()) {
|
|
417
|
-
if (now - timestamp >= deduplicationTTL) {
|
|
418
|
-
receivedMessageIds.current.delete(key);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}, 60 * 1000);
|
|
422
|
-
|
|
423
|
-
return () => clearInterval(interval);
|
|
424
|
-
}, [deduplicationTTL]);
|
|
425
|
-
|
|
426
|
-
useEffect(() => {
|
|
427
|
-
return () => {
|
|
428
|
-
// Always flush any unsent batched messages on unmount
|
|
429
|
-
if (batchingMessagesRef.current.length > 0 && channel.current && !batchingErrorRef.current) {
|
|
430
|
-
try {
|
|
431
|
-
channel.current.postMessage(batchingMessagesRef.current);
|
|
432
|
-
} catch (e) {
|
|
433
|
-
const error = 'Failed to send message';
|
|
434
|
-
debug.error(error);
|
|
435
|
-
setErrorMessage(error);
|
|
436
|
-
}
|
|
437
|
-
batchingMessagesRef.current = [];
|
|
438
|
-
// Allow event loop to process delivery for tests
|
|
439
|
-
setTimeout(() => {}, 0);
|
|
440
|
-
}
|
|
441
|
-
if (batchingTimeoutRef.current) {
|
|
442
|
-
clearTimeout(batchingTimeoutRef.current);
|
|
443
|
-
batchingTimeoutRef.current = null;
|
|
444
|
-
}
|
|
445
|
-
batchingErrorRef.current = false;
|
|
446
|
-
};
|
|
447
|
-
}, []);
|
|
448
|
-
|
|
449
|
-
return {
|
|
450
|
-
channelName: resolvedChannelName,
|
|
451
|
-
messages,
|
|
452
|
-
sentMessages,
|
|
453
|
-
isPingInProgress,
|
|
454
|
-
ping,
|
|
455
|
-
postMessage,
|
|
456
|
-
clearReceivedMessages,
|
|
457
|
-
clearSentMessages,
|
|
458
|
-
getLatestMessage,
|
|
459
|
-
error,
|
|
460
|
-
closeChannel,
|
|
461
|
-
};
|
|
462
|
-
};
|
|
463
|
-
|
|
464
|
-
export default useBroadcastChannel;
|
package/src/index.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export { useBroadcastChannel } from './hooks/useBroadcastChannel';
|
|
2
|
-
export { BroadcastProvider, useBroadcastProvider } from './BroadcastProvider';
|
|
3
|
-
|
|
4
|
-
export type {
|
|
5
|
-
BroadcastOptions,
|
|
6
|
-
SendMessageOptions,
|
|
7
|
-
BroadcastMessage,
|
|
8
|
-
BroadcastActions,
|
|
9
|
-
} from './types/types';
|