react-broadcast-sync 1.0.0 → 1.1.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.
Files changed (46) hide show
  1. package/.eslintignore +5 -0
  2. package/.github/workflows/ci.yml +3 -3
  3. package/.github/workflows/release.yml +39 -0
  4. package/.husky/pre-commit +0 -1
  5. package/.nvmrc +1 -0
  6. package/.prettierignore +1 -0
  7. package/.prettierrc +5 -1
  8. package/CHANGELOG.md +20 -0
  9. package/README.md +118 -47
  10. package/demo/react-broadcast-sync-demo/README.md +130 -0
  11. package/demo/react-broadcast-sync-demo/index.html +14 -0
  12. package/demo/react-broadcast-sync-demo/package-lock.json +6379 -0
  13. package/demo/react-broadcast-sync-demo/package.json +42 -0
  14. package/demo/react-broadcast-sync-demo/public/assets/react-broadcast-sync-logo.png +0 -0
  15. package/demo/react-broadcast-sync-demo/src/App.tsx +277 -0
  16. package/demo/react-broadcast-sync-demo/src/components/Counter.tsx +63 -0
  17. package/demo/react-broadcast-sync-demo/src/components/TextSync.tsx +61 -0
  18. package/demo/react-broadcast-sync-demo/src/components/TodoList.tsx +186 -0
  19. package/demo/react-broadcast-sync-demo/src/hooks/useCounterBroadcast.ts +94 -0
  20. package/demo/react-broadcast-sync-demo/src/hooks/useTextBroadcast.ts +37 -0
  21. package/demo/react-broadcast-sync-demo/src/hooks/useTodoBroadcast.ts +249 -0
  22. package/demo/react-broadcast-sync-demo/src/main.tsx +11 -0
  23. package/demo/react-broadcast-sync-demo/src/theme.ts +62 -0
  24. package/demo/react-broadcast-sync-demo/tsconfig.json +26 -0
  25. package/demo/react-broadcast-sync-demo/tsconfig.node.json +10 -0
  26. package/demo/react-broadcast-sync-demo/vite.config.ts +12 -0
  27. package/dist/index.cjs.js +1 -1
  28. package/dist/index.cjs.js.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.esm.js +1 -1
  31. package/dist/index.esm.js.map +1 -1
  32. package/eslint.config.js +14 -5
  33. package/jest.config.cjs +8 -9
  34. package/package.json +63 -33
  35. package/rollup.config.mjs +27 -21
  36. package/src/BroadcastProvider.tsx +6 -6
  37. package/src/hooks/useBroadcastChannel.ts +122 -95
  38. package/src/index.ts +1 -1
  39. package/src/tests/BroadcastProvider.test.tsx +7 -7
  40. package/src/tests/debug.test.ts +65 -25
  41. package/src/tests/messageUtils.test.ts +20 -15
  42. package/src/tests/useBroadcastChannel.test.tsx +48 -33
  43. package/src/types/types.ts +19 -23
  44. package/src/utils/debug.ts +1 -1
  45. package/src/utils/messageUtils.ts +19 -24
  46. package/dist/useBroadcastChannel.d.ts +0 -3
@@ -1,9 +1,14 @@
1
1
  import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
2
- import { BroadcastActions, BroadcastMessage, BroadcastOptions, SendMessageOptions } from '../types/types';
3
- import {
4
- generateSourceName,
5
- isValidMessage,
6
- isMessageExpired,
2
+ import {
3
+ BroadcastActions,
4
+ BroadcastMessage,
5
+ BroadcastOptions,
6
+ SendMessageOptions,
7
+ } from '../types/types';
8
+ import {
9
+ generateSourceName,
10
+ isValidMessage,
11
+ isMessageExpired,
7
12
  createMessage,
8
13
  getInternalMessageType,
9
14
  isInternalType,
@@ -13,9 +18,9 @@ import {
13
18
  import { debug } from '../utils/debug';
14
19
 
15
20
  const INTERNAL_MESSAGE_TYPES = {
16
- CLEAR_MESSAGE: 'CLEAR_MESSAGE',
17
- CLEAR_ALL_MESSAGES: 'CLEAR_ALL_MESSAGES',
18
- } as const;
21
+ CLEAR_MESSAGE: 'CLEAR_MESSAGE',
22
+ CLEAR_ALL_MESSAGES: 'CLEAR_ALL_MESSAGES',
23
+ } as const;
19
24
 
20
25
  export const useBroadcastChannel = (
21
26
  channelName: string,
@@ -25,10 +30,10 @@ export const useBroadcastChannel = (
25
30
  sourceName,
26
31
  cleaningInterval = 1000,
27
32
  keepLatestMessage = false,
28
- registeredTypes = [],
33
+ registeredTypes = [],
29
34
  namespace = '',
30
35
  deduplicationTTL = 5 * 60 * 1000,
31
- cleanupDebounceMs = 0
36
+ cleanupDebounceMs = 0,
32
37
  } = options;
33
38
 
34
39
  // State
@@ -41,8 +46,16 @@ export const useBroadcastChannel = (
41
46
  const receivedMessageIds = useRef(new Map<string, number>());
42
47
  const source = useRef(sourceName || generateSourceName()).current;
43
48
  const internalTypes = useRef({
44
- CLEAR_MESSAGE: getInternalMessageType(INTERNAL_MESSAGE_TYPES.CLEAR_MESSAGE, channelName, namespace),
45
- CLEAR_ALL_MESSAGES: getInternalMessageType(INTERNAL_MESSAGE_TYPES.CLEAR_ALL_MESSAGES, channelName, namespace),
49
+ CLEAR_MESSAGE: getInternalMessageType(
50
+ INTERNAL_MESSAGE_TYPES.CLEAR_MESSAGE,
51
+ channelName,
52
+ namespace
53
+ ),
54
+ CLEAR_ALL_MESSAGES: getInternalMessageType(
55
+ INTERNAL_MESSAGE_TYPES.CLEAR_ALL_MESSAGES,
56
+ channelName,
57
+ namespace
58
+ ),
46
59
  }).current;
47
60
 
48
61
  const resolvedChannelName = useMemo(() => {
@@ -67,107 +80,121 @@ export const useBroadcastChannel = (
67
80
  setTimeout(() => setError(null), 3000);
68
81
  }, []);
69
82
 
70
- const clearMessage = useCallback((id: string) => {
71
- setMessages(prev => prev.filter(msg => msg.id !== id));
72
- setSentMessages(prev => prev.filter(msg => msg.id !== id));
73
- channel.current?.postMessage({
74
- id,
75
- type: internalTypes.CLEAR_MESSAGE,
76
- source
77
- });
78
- debug.message.cleared(id);
79
- }, [source, internalTypes.CLEAR_MESSAGE]);
83
+ const clearMessage = useCallback(
84
+ (id: string) => {
85
+ setMessages(prev => prev.filter(msg => msg.id !== id));
86
+ setSentMessages(prev => prev.filter(msg => msg.id !== id));
87
+ channel.current?.postMessage({
88
+ id,
89
+ type: internalTypes.CLEAR_MESSAGE,
90
+ source,
91
+ });
92
+ debug.message.cleared(id);
93
+ },
94
+ [source, internalTypes.CLEAR_MESSAGE]
95
+ );
80
96
 
81
97
  const clearAllMessages = useCallback(() => {
82
98
  setMessages([]);
83
99
  setSentMessages([]);
84
- channel.current?.postMessage({
85
- id: internalTypes.CLEAR_ALL_MESSAGES,
86
- type: internalTypes.CLEAR_ALL_MESSAGES,
87
- source
100
+ channel.current?.postMessage({
101
+ id: internalTypes.CLEAR_ALL_MESSAGES,
102
+ type: internalTypes.CLEAR_ALL_MESSAGES,
103
+ source,
88
104
  });
89
105
  }, [source, internalTypes.CLEAR_ALL_MESSAGES]);
90
106
 
91
- const clearSentMessage = useCallback((id: string) => {
92
- setSentMessages(prev => prev.filter(msg => msg.id !== id));
93
- channel.current?.postMessage({
94
- id,
95
- type: internalTypes.CLEAR_MESSAGE,
96
- source
97
- });
98
- }, [source, internalTypes.CLEAR_MESSAGE]);
99
-
100
- const postMessage = useCallback((
101
- messageType: string,
102
- messageContent: any,
103
- options: SendMessageOptions = {}
104
- ) => {
105
- const channelCurrent = channel.current;
106
- if (!channelCurrent) {
107
- const error = 'Channel not available';
108
- debug.error(error);
109
- setErrorMessage(error);
110
- return;
111
- }
107
+ const clearSentMessage = useCallback(
108
+ (id: string) => {
109
+ setSentMessages(prev => prev.filter(msg => msg.id !== id));
110
+ channel.current?.postMessage({
111
+ id,
112
+ type: internalTypes.CLEAR_MESSAGE,
113
+ source,
114
+ });
115
+ },
116
+ [source, internalTypes.CLEAR_MESSAGE]
117
+ );
112
118
 
113
- try {
114
- const message = createMessage(messageType, messageContent, source, options);
115
- channelCurrent.postMessage(message);
116
- debug.message.sent(message);
117
- setSentMessages(prev => [...prev, message]);
118
- } catch (e) {
119
- const error = 'Failed to send message';
120
- debug.error(error);
121
- setErrorMessage(error);
122
- }
123
- }, [source, setErrorMessage]);
124
-
125
- const handleMessage = useCallback((event: MessageEvent) => {
126
- try {
127
- const message: BroadcastMessage = event.data;
128
- debug.message.received(message);
129
-
130
- if (!isValidMessage(message)) return;
131
- if (message.source === source) return;
132
- if (!isInternalType(message.type) && registeredTypes.length > 0 && !registeredTypes.includes(message.type)) return;
133
- if (isMessageExpired(message)) {
134
- debug.message.expired(message.id);
119
+ const postMessage = useCallback(
120
+ (messageType: string, messageContent: any, options: SendMessageOptions = {}) => {
121
+ const channelCurrent = channel.current;
122
+ if (!channelCurrent) {
123
+ const error = 'Channel not available';
124
+ debug.error(error);
125
+ setErrorMessage(error);
135
126
  return;
136
127
  }
137
128
 
138
- if (isValidInternalClearMessage(message)) {
139
- if (message.type === internalTypes.CLEAR_MESSAGE) {
140
- setMessages(prev =>
141
- prev.filter(msg => !(msg.id === message.id && msg.source === message.source))
142
- );
129
+ try {
130
+ const message = createMessage(messageType, messageContent, source, options);
131
+ channelCurrent.postMessage(message);
132
+ debug.message.sent(message);
133
+ setSentMessages(prev => [...prev, message]);
134
+ } catch (e) {
135
+ const error = 'Failed to send message';
136
+ debug.error(error);
137
+ setErrorMessage(error);
138
+ }
139
+ },
140
+ [source, setErrorMessage]
141
+ );
142
+
143
+ const handleMessage = useCallback(
144
+ (event: MessageEvent) => {
145
+ try {
146
+ const message: BroadcastMessage = event.data;
147
+ debug.message.received(message);
148
+
149
+ if (!isValidMessage(message)) return;
150
+ if (message.source === source) return;
151
+ if (
152
+ !isInternalType(message.type) &&
153
+ registeredTypes.length > 0 &&
154
+ !registeredTypes.includes(message.type)
155
+ )
156
+ return;
157
+ if (isMessageExpired(message)) {
158
+ debug.message.expired(message.id);
143
159
  return;
144
160
  }
145
-
146
- if (message.type === internalTypes.CLEAR_ALL_MESSAGES) {
147
- setMessages(prev => prev.filter(msg => msg.source !== message.source));
161
+
162
+ if (isValidInternalClearMessage(message)) {
163
+ if (message.type === internalTypes.CLEAR_MESSAGE) {
164
+ setMessages(prev =>
165
+ prev.filter(msg => !(msg.id === message.id && msg.source === message.source))
166
+ );
167
+ return;
168
+ }
169
+
170
+ if (message.type === internalTypes.CLEAR_ALL_MESSAGES) {
171
+ setMessages(prev => prev.filter(msg => msg.source !== message.source));
172
+ return;
173
+ }
174
+ }
175
+
176
+ const now = Date.now();
177
+ const receivedAt = receivedMessageIds.current.get(message.id);
178
+ if (receivedAt && now - receivedAt < deduplicationTTL) {
179
+ debug.message.duplicate(message.id);
148
180
  return;
149
181
  }
150
- }
151
182
 
152
- const now = Date.now();
153
- const receivedAt = receivedMessageIds.current.get(message.id);
154
- if (receivedAt && now - receivedAt < deduplicationTTL) {
155
- debug.message.duplicate(message.id);
156
- return;
183
+ receivedMessageIds.current.set(message.id, now);
184
+ setMessages(prev => (keepLatestMessage ? [message] : [...prev, message]));
185
+ } catch (e) {
186
+ const error = 'Error processing broadcast message';
187
+ debug.error(error);
188
+ setErrorMessage(error);
157
189
  }
158
-
159
- receivedMessageIds.current.set(message.id, now);
160
- setMessages(prev => keepLatestMessage ? [message] : [...prev, message]);
161
- } catch (e) {
162
- const error = 'Error processing broadcast message';
163
- debug.error(error);
164
- setErrorMessage(error);
165
- }
166
- }, [source, registeredTypes, keepLatestMessage, setErrorMessage, internalTypes, deduplicationTTL]);
190
+ },
191
+ [source, registeredTypes, keepLatestMessage, setErrorMessage, internalTypes, deduplicationTTL]
192
+ );
167
193
 
168
194
  useEffect(() => {
169
195
  if (typeof BroadcastChannel === 'undefined') {
170
- const error = 'BroadcastChannel is not supported in this browser. Please check browser compatibility.';
196
+ const error =
197
+ 'BroadcastChannel is not supported in this browser. Please check browser compatibility.';
171
198
  debug.error(error);
172
199
  setErrorMessage(error);
173
200
  return;
@@ -213,7 +240,7 @@ export const useBroadcastChannel = (
213
240
  }
214
241
  }
215
242
  }, 60 * 1000);
216
-
243
+
217
244
  return () => clearInterval(interval);
218
245
  }, [deduplicationTTL]);
219
246
 
@@ -229,4 +256,4 @@ export const useBroadcastChannel = (
229
256
  };
230
257
  };
231
258
 
232
- export default useBroadcastChannel;
259
+ export default useBroadcastChannel;
package/src/index.ts CHANGED
@@ -5,5 +5,5 @@ export type {
5
5
  BroadcastOptions,
6
6
  SendMessageOptions,
7
7
  BroadcastMessage,
8
- BroadcastActions
8
+ BroadcastActions,
9
9
  } from './types/types';
@@ -36,7 +36,7 @@ const TestComponent = () => {
36
36
  <div>
37
37
  <div data-testid="error">{error}</div>
38
38
  <div data-testid="messages">
39
- {messages.map((msg) => (
39
+ {messages.map(msg => (
40
40
  <div key={msg.id} data-testid={`message-${msg.id}`}>
41
41
  {msg.message.text}
42
42
  </div>
@@ -92,7 +92,7 @@ describe('BroadcastProvider', () => {
92
92
  type: 'test',
93
93
  message: { text: 'Hello' },
94
94
  source: 'other-tab',
95
- timestamp: Date.now()
95
+ timestamp: Date.now(),
96
96
  };
97
97
 
98
98
  act(() => {
@@ -138,7 +138,7 @@ describe('BroadcastProvider', () => {
138
138
  };
139
139
 
140
140
  const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
141
-
141
+
142
142
  render(
143
143
  <BroadcastProvider channelName="test-channel">
144
144
  <ThrowComponent />
@@ -147,17 +147,17 @@ describe('BroadcastProvider', () => {
147
147
 
148
148
  expect(screen.getByText('Error occurred in BroadcastProvider')).toBeInTheDocument();
149
149
  expect(consoleError).toHaveBeenCalledWith('BroadcastChannel error:', expect.any(Error));
150
-
150
+
151
151
  consoleError.mockRestore();
152
152
  });
153
153
 
154
154
  it('throws error when useBroadcastProvider is used outside provider', () => {
155
155
  const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
156
-
156
+
157
157
  expect(() => {
158
158
  render(<TestComponent />);
159
159
  }).toThrow('useBroadcastProvider must be used within a BroadcastProvider');
160
-
160
+
161
161
  consoleError.mockRestore();
162
162
  });
163
- });
163
+ });
@@ -48,14 +48,18 @@ describe('debug', () => {
48
48
  it('logs channel events', () => {
49
49
  debug.channel.created('test-channel');
50
50
  expect(console.log).toHaveBeenCalledWith(
51
- expect.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/),
51
+ expect.stringMatching(
52
+ /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
53
+ ),
52
54
  'Channel created:',
53
55
  'test-channel'
54
56
  );
55
57
 
56
58
  debug.channel.closed('test-channel');
57
59
  expect(console.log).toHaveBeenCalledWith(
58
- expect.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/),
60
+ expect.stringMatching(
61
+ /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
62
+ ),
59
63
  'Channel closed:',
60
64
  'test-channel'
61
65
  );
@@ -63,38 +67,48 @@ describe('debug', () => {
63
67
 
64
68
  it('logs message events', () => {
65
69
  const testMessage = { id: '123', type: 'test' };
66
-
70
+
67
71
  debug.message.sent(testMessage);
68
72
  expect(console.log).toHaveBeenCalledWith(
69
- expect.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/),
73
+ expect.stringMatching(
74
+ /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
75
+ ),
70
76
  'Message sent:',
71
77
  testMessage
72
78
  );
73
79
 
74
80
  debug.message.received(testMessage);
75
81
  expect(console.log).toHaveBeenCalledWith(
76
- expect.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/),
82
+ expect.stringMatching(
83
+ /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
84
+ ),
77
85
  'Message received:',
78
86
  testMessage
79
87
  );
80
88
 
81
89
  debug.message.cleared('123');
82
90
  expect(console.log).toHaveBeenCalledWith(
83
- expect.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/),
91
+ expect.stringMatching(
92
+ /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
93
+ ),
84
94
  'Message cleared:',
85
95
  '123'
86
96
  );
87
97
 
88
98
  debug.message.expired('123');
89
99
  expect(console.log).toHaveBeenCalledWith(
90
- expect.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/),
100
+ expect.stringMatching(
101
+ /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
102
+ ),
91
103
  'Message expired:',
92
104
  '123'
93
105
  );
94
106
 
95
107
  debug.message.duplicate('123');
96
108
  expect(console.warn).toHaveBeenCalledWith(
97
- expect.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/),
109
+ expect.stringMatching(
110
+ /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
111
+ ),
98
112
  'Duplicate message ignored:',
99
113
  '123'
100
114
  );
@@ -103,14 +117,18 @@ describe('debug', () => {
103
117
  it('logs cleanup events', () => {
104
118
  debug.cleanup.started();
105
119
  expect(console.log).toHaveBeenCalledWith(
106
- expect.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/),
120
+ expect.stringMatching(
121
+ /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
122
+ ),
107
123
  'Cleanup started',
108
124
  ''
109
125
  );
110
126
 
111
127
  debug.cleanup.completed(5);
112
128
  expect(console.log).toHaveBeenCalledWith(
113
- expect.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/),
129
+ expect.stringMatching(
130
+ /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
131
+ ),
114
132
  'Cleanup completed:',
115
133
  { removedCount: 5 }
116
134
  );
@@ -119,7 +137,9 @@ describe('debug', () => {
119
137
  it('logs errors', () => {
120
138
  debug.error('test error');
121
139
  expect(console.error).toHaveBeenCalledWith(
122
- expect.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/),
140
+ expect.stringMatching(
141
+ /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
142
+ ),
123
143
  'Error:',
124
144
  'test error'
125
145
  );
@@ -153,18 +173,18 @@ describe('debug', () => {
153
173
  });
154
174
 
155
175
  it('logs message with no data', () => {
156
- debug.cleanup.started();
157
- expect(console.log).toHaveBeenCalledWith(
158
- expect.stringContaining('[react-broadcast-sync]'),
159
- 'Cleanup started',
160
- ''
161
- );
162
- });
176
+ debug.cleanup.started();
177
+ expect(console.log).toHaveBeenCalledWith(
178
+ expect.stringContaining('[react-broadcast-sync]'),
179
+ 'Cleanup started',
180
+ ''
181
+ );
182
+ });
163
183
  });
164
184
 
165
185
  describe('when process.env is undefined', () => {
166
186
  beforeEach(() => {
167
- // @ts-ignore - Simulating environment where process.env is undefined
187
+ // @ts-expect-error - Testing invalid input
168
188
  process.env = undefined;
169
189
  });
170
190
 
@@ -176,24 +196,44 @@ describe('debug', () => {
176
196
 
177
197
  it('logs info messages', () => {
178
198
  debug.channel.created('test-channel');
179
- expect(console.log).toHaveBeenCalledWith(expect.stringContaining('[react-broadcast-sync]'), 'Channel created:', 'test-channel');
199
+ expect(console.log).toHaveBeenCalledWith(
200
+ expect.stringContaining('[react-broadcast-sync]'),
201
+ 'Channel created:',
202
+ 'test-channel'
203
+ );
180
204
  });
181
205
 
182
206
  it('logs warning messages', () => {
183
207
  debug.message.duplicate('test-id');
184
- expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('[react-broadcast-sync]'), 'Duplicate message ignored:', 'test-id');
208
+ expect(console.warn).toHaveBeenCalledWith(
209
+ expect.stringContaining('[react-broadcast-sync]'),
210
+ 'Duplicate message ignored:',
211
+ 'test-id'
212
+ );
185
213
  });
186
214
 
187
215
  it('logs error messages', () => {
188
216
  debug.error('Test error');
189
- expect(console.error).toHaveBeenCalledWith(expect.stringContaining('[react-broadcast-sync]'), 'Error:', 'Test error');
217
+ expect(console.error).toHaveBeenCalledWith(
218
+ expect.stringContaining('[react-broadcast-sync]'),
219
+ 'Error:',
220
+ 'Test error'
221
+ );
190
222
  });
191
223
 
192
224
  it('logs cleanup information', () => {
193
225
  debug.cleanup.started();
194
- expect(console.log).toHaveBeenCalledWith(expect.stringContaining('[react-broadcast-sync]'), 'Cleanup started', '');
226
+ expect(console.log).toHaveBeenCalledWith(
227
+ expect.stringContaining('[react-broadcast-sync]'),
228
+ 'Cleanup started',
229
+ ''
230
+ );
195
231
 
196
232
  debug.cleanup.completed(2);
197
- expect(console.log).toHaveBeenCalledWith(expect.stringContaining('[react-broadcast-sync]'), 'Cleanup completed:', { removedCount: 2 });
233
+ expect(console.log).toHaveBeenCalledWith(
234
+ expect.stringContaining('[react-broadcast-sync]'),
235
+ 'Cleanup completed:',
236
+ { removedCount: 2 }
237
+ );
198
238
  });
199
- });
239
+ });
@@ -8,7 +8,7 @@ import {
8
8
  getInternalMessageType,
9
9
  isInternalType,
10
10
  isValidInternalClearMessage,
11
- debounce
11
+ debounce,
12
12
  } from '../utils/messageUtils';
13
13
 
14
14
  beforeEach(() => {
@@ -86,7 +86,7 @@ describe('messageUtils', () => {
86
86
  const valid = {
87
87
  id: 'abc',
88
88
  type: getInternalMessageType('CLEAR_ALL_MESSAGES', 'ch'),
89
- source: 'tab-1'
89
+ source: 'tab-1',
90
90
  };
91
91
  const invalid = { id: 123, type: 'bad', source: 'tab-1' };
92
92
  expect(isValidInternalClearMessage(valid)).toBe(true);
@@ -112,9 +112,14 @@ describe('debounce', () => {
112
112
  });
113
113
 
114
114
  it('debounce handles multiple calls and preserves this context', () => {
115
- const obj = { value: 0, increment: function() { this.value++; } };
115
+ const obj = {
116
+ value: 0,
117
+ increment: function () {
118
+ this.value++;
119
+ },
120
+ };
116
121
  const debounced = debounce(obj.increment.bind(obj), 100);
117
-
122
+
118
123
  debounced();
119
124
  debounced();
120
125
  debounced.flush();
@@ -124,7 +129,7 @@ describe('debounce', () => {
124
129
  it('debounce flush returns undefined when no pending call', () => {
125
130
  const fn = jest.fn();
126
131
  const debounced = debounce(fn, 100);
127
-
132
+
128
133
  expect(debounced.flush()).toBeUndefined();
129
134
  expect(fn).not.toHaveBeenCalled();
130
135
  });
@@ -132,7 +137,7 @@ describe('debounce', () => {
132
137
  it('debounce handles function with return value', () => {
133
138
  const fn = jest.fn().mockReturnValue('test');
134
139
  const debounced = debounce(fn, 100);
135
-
140
+
136
141
  debounced();
137
142
  expect(debounced.flush()).toBe('test');
138
143
  });
@@ -145,7 +150,7 @@ describe('debounce', () => {
145
150
  jest.advanceTimersByTime(200);
146
151
  expect(fn).not.toHaveBeenCalled();
147
152
  });
148
-
153
+
149
154
  it('debounce.flush executes immediately', () => {
150
155
  const fn = jest.fn();
151
156
  const debounced = debounce(fn, 100);
@@ -157,11 +162,11 @@ describe('debounce', () => {
157
162
  it('debounce handles multiple calls with different arguments', () => {
158
163
  const fn = jest.fn();
159
164
  const debounced = debounce(fn, 100);
160
-
165
+
161
166
  debounced('first');
162
167
  debounced('second');
163
168
  debounced('third');
164
-
169
+
165
170
  expect(fn).not.toHaveBeenCalled();
166
171
  debounced.flush();
167
172
  expect(fn).toHaveBeenCalledTimes(1);
@@ -171,7 +176,7 @@ describe('debounce', () => {
171
176
  it('debounce handles cancel and flush in sequence', () => {
172
177
  const fn = jest.fn();
173
178
  const debounced = debounce(fn, 100);
174
-
179
+
175
180
  debounced('test');
176
181
  debounced.cancel();
177
182
  expect(debounced.flush()).toBeUndefined();
@@ -217,14 +222,14 @@ describe('debounce', () => {
217
222
  it('debounce.flush handles case where timeoutId exists but lastArgs is null', () => {
218
223
  const fn = jest.fn();
219
224
  const debounced = debounce(fn, 100);
220
-
225
+
221
226
  // Schedule a call
222
227
  debounced('test');
223
-
228
+
224
229
  // Force the timeout to be set but clear lastArgs
225
230
  jest.advanceTimersByTime(50); // Advance partially through the timeout
226
231
  debounced.cancel(); // This clears lastArgs but might not clear timeoutId immediately
227
-
232
+
228
233
  // Call flush when timeoutId exists but lastArgs is null
229
234
  const result = debounced.flush();
230
235
  expect(result).toBeUndefined();
@@ -234,11 +239,11 @@ describe('debounce', () => {
234
239
  it('debounce.flush handles multiple calls with different arguments and preserves return value', () => {
235
240
  const fn = jest.fn((x: number) => x * 2);
236
241
  const debounced = debounce(fn, 100);
237
-
242
+
238
243
  debounced(1);
239
244
  debounced(2);
240
245
  debounced(3);
241
-
246
+
242
247
  const result = debounced.flush();
243
248
  expect(result).toBe(6); // 3 * 2
244
249
  expect(fn).toHaveBeenCalledTimes(1);