react-broadcast-sync 1.6.0 → 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.
Files changed (52) hide show
  1. package/README.md +59 -11
  2. package/dist/index.cjs.js +1 -1
  3. package/dist/index.cjs.js.map +1 -1
  4. package/dist/index.esm.js +1 -1
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/types/types.d.ts +17 -0
  7. package/package.json +6 -1
  8. package/.eslintignore +0 -5
  9. package/.github/workflows/ci.yml +0 -61
  10. package/.github/workflows/release.yml +0 -39
  11. package/.husky/pre-commit +0 -5
  12. package/.nvmrc +0 -1
  13. package/.prettierignore +0 -4
  14. package/.prettierrc +0 -9
  15. package/.releaserc.json +0 -22
  16. package/CHANGELOG.md +0 -97
  17. package/assets/react-broadcast-sync-logo.png +0 -0
  18. package/commitlint.config.js +0 -3
  19. package/demo/react-broadcast-sync-demo/README.md +0 -130
  20. package/demo/react-broadcast-sync-demo/index.html +0 -14
  21. package/demo/react-broadcast-sync-demo/package-lock.json +0 -6379
  22. package/demo/react-broadcast-sync-demo/package.json +0 -42
  23. package/demo/react-broadcast-sync-demo/public/assets/react-broadcast-sync-demo-video.gif +0 -0
  24. package/demo/react-broadcast-sync-demo/public/assets/react-broadcast-sync-logo.png +0 -0
  25. package/demo/react-broadcast-sync-demo/src/App.tsx +0 -277
  26. package/demo/react-broadcast-sync-demo/src/components/Counter.tsx +0 -63
  27. package/demo/react-broadcast-sync-demo/src/components/TextSync.tsx +0 -61
  28. package/demo/react-broadcast-sync-demo/src/components/TodoList.tsx +0 -186
  29. package/demo/react-broadcast-sync-demo/src/hooks/useCounterBroadcast.ts +0 -96
  30. package/demo/react-broadcast-sync-demo/src/hooks/useTextBroadcast.ts +0 -37
  31. package/demo/react-broadcast-sync-demo/src/hooks/useTodoBroadcast.ts +0 -249
  32. package/demo/react-broadcast-sync-demo/src/main.tsx +0 -11
  33. package/demo/react-broadcast-sync-demo/src/theme.ts +0 -62
  34. package/demo/react-broadcast-sync-demo/tsconfig.json +0 -26
  35. package/demo/react-broadcast-sync-demo/tsconfig.node.json +0 -10
  36. package/demo/react-broadcast-sync-demo/vite.config.ts +0 -12
  37. package/eslint.config.js +0 -38
  38. package/jest.config.cjs +0 -9
  39. package/jest.setup.ts +0 -1
  40. package/rollup.config.mjs +0 -98
  41. package/src/BroadcastProvider.tsx +0 -28
  42. package/src/hooks/useBroadcastChannel.ts +0 -513
  43. package/src/index.ts +0 -9
  44. package/src/tests/BroadcastProvider.test.tsx +0 -157
  45. package/src/tests/debug.test.ts +0 -273
  46. package/src/tests/messageUtils.test.ts +0 -252
  47. package/src/tests/useBroadcastChannel.same-tab.test.tsx +0 -992
  48. package/src/tests/useBroadcastChannel.test.tsx +0 -1243
  49. package/src/types/types.ts +0 -124
  50. package/src/utils/debug.ts +0 -80
  51. package/src/utils/messageUtils.ts +0 -122
  52. package/tsconfig.json +0 -36
@@ -1,273 +0,0 @@
1
- // Set debug mode before requiring the debug module
2
- process.env.REACT_APP_DEBUG_BROADCAST = 'true';
3
- import { debug } from '../utils/debug';
4
-
5
- describe('debug', () => {
6
- const originalEnv = process.env;
7
- const originalConsole = {
8
- log: console.log,
9
- warn: console.warn,
10
- error: console.error,
11
- };
12
-
13
- beforeEach(() => {
14
- process.env = { ...originalEnv };
15
- console.log = jest.fn();
16
- console.warn = jest.fn();
17
- console.error = jest.fn();
18
- });
19
-
20
- afterEach(() => {
21
- console.log = originalConsole.log;
22
- console.warn = originalConsole.warn;
23
- console.error = originalConsole.error;
24
- process.env = originalEnv;
25
- });
26
-
27
- describe('when debug mode is disabled', () => {
28
- beforeEach(() => {
29
- process.env.REACT_APP_DEBUG_BROADCAST = 'false';
30
- });
31
-
32
- it('does not log anything', () => {
33
- debug.channel.created('test-channel');
34
- debug.message.sent({ id: '123' });
35
- debug.error({ action: 'test error' });
36
-
37
- expect(console.log).not.toHaveBeenCalled();
38
- expect(console.warn).not.toHaveBeenCalled();
39
- expect(console.error).not.toHaveBeenCalled();
40
- });
41
- });
42
-
43
- describe('when debug mode is enabled', () => {
44
- beforeEach(() => {
45
- process.env.REACT_APP_DEBUG_BROADCAST = 'true';
46
- });
47
-
48
- it('logs channel events', () => {
49
- debug.channel.created('test-channel');
50
- expect(console.log).toHaveBeenCalledWith(
51
- expect.stringMatching(
52
- /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
53
- ),
54
- 'Channel created:',
55
- 'test-channel'
56
- );
57
-
58
- debug.channel.closed('test-channel');
59
- expect(console.log).toHaveBeenCalledWith(
60
- expect.stringMatching(
61
- /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
62
- ),
63
- 'Channel closed:',
64
- 'test-channel'
65
- );
66
- });
67
-
68
- it('logs message events', () => {
69
- const testMessage = { id: '123', type: 'test' };
70
-
71
- debug.message.sent(testMessage);
72
- expect(console.log).toHaveBeenCalledWith(
73
- expect.stringMatching(
74
- /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
75
- ),
76
- 'Message sent:',
77
- testMessage
78
- );
79
-
80
- debug.message.received(testMessage);
81
- expect(console.log).toHaveBeenCalledWith(
82
- expect.stringMatching(
83
- /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
84
- ),
85
- 'Message received:',
86
- testMessage
87
- );
88
-
89
- debug.message.cleared('123');
90
- expect(console.log).toHaveBeenCalledWith(
91
- expect.stringMatching(
92
- /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
93
- ),
94
- 'Message cleared:',
95
- '123'
96
- );
97
-
98
- debug.message.expired('123');
99
- expect(console.log).toHaveBeenCalledWith(
100
- expect.stringMatching(
101
- /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
102
- ),
103
- 'Message expired:',
104
- '123'
105
- );
106
-
107
- debug.message.duplicate('123');
108
- expect(console.warn).toHaveBeenCalledWith(
109
- expect.stringMatching(
110
- /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
111
- ),
112
- 'Duplicate message ignored:',
113
- '123'
114
- );
115
- });
116
-
117
- it('logs cleanup events', () => {
118
- debug.cleanup.started();
119
- expect(console.log).toHaveBeenCalledWith(
120
- expect.stringMatching(
121
- /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
122
- ),
123
- 'Cleanup started',
124
- ''
125
- );
126
-
127
- debug.cleanup.completed(5);
128
- expect(console.log).toHaveBeenCalledWith(
129
- expect.stringMatching(
130
- /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
131
- ),
132
- 'Cleanup completed:',
133
- 5
134
- );
135
- });
136
-
137
- it('logs errors', () => {
138
- debug.error({ action: 'test error' });
139
- expect(console.error).toHaveBeenCalledWith(
140
- expect.stringMatching(
141
- /\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
142
- ),
143
- 'Error:',
144
- 'test error'
145
- );
146
- });
147
-
148
- it('includes string cause in error logs', () => {
149
- debug.error({ action: 'action', originalError: 'string-cause' });
150
- expect(console.error).toHaveBeenCalledWith(
151
- expect.stringMatching(/\[.*react-broadcast-sync\]/),
152
- 'Error:',
153
- expect.stringContaining('action | cause: string-cause')
154
- );
155
- });
156
-
157
- it('includes object cause in error logs (JSON stringified)', () => {
158
- debug.error({ action: 'action', originalError: { reason: 'bad', code: 500 } });
159
- expect(console.error).toHaveBeenCalledWith(
160
- expect.stringMatching(/\[.*react-broadcast-sync\]/),
161
- 'Error:',
162
- expect.stringContaining('action | cause: {"reason":"bad","code":500}')
163
- );
164
- });
165
-
166
- it('logs info with no data', () => {
167
- debug.channel.created('test-channel');
168
- expect(console.log).toHaveBeenCalledWith(
169
- expect.stringMatching(/\[.*react-broadcast-sync\]/),
170
- 'Channel created:',
171
- 'test-channel'
172
- );
173
- });
174
-
175
- it('logs warn with no data', () => {
176
- debug.message.duplicate('123');
177
- expect(console.warn).toHaveBeenCalledWith(
178
- expect.stringMatching(/\[.*react-broadcast-sync\]/),
179
- 'Duplicate message ignored:',
180
- '123'
181
- );
182
- });
183
-
184
- it('logs error with no data', () => {
185
- debug.error({ action: 'error!' });
186
- expect(console.error).toHaveBeenCalledWith(
187
- expect.stringMatching(/\[.*react-broadcast-sync\]/),
188
- 'Error:',
189
- 'error!'
190
- );
191
- });
192
-
193
- it('logs message with no data', () => {
194
- debug.cleanup.started();
195
- expect(console.log).toHaveBeenCalledWith(
196
- expect.stringContaining('[react-broadcast-sync]'),
197
- 'Cleanup started',
198
- ''
199
- );
200
- });
201
- });
202
-
203
- describe('when process.env is undefined', () => {
204
- beforeEach(() => {
205
- // @ts-expect-error - Testing invalid input
206
- process.env = undefined;
207
- });
208
-
209
- it('does not log anything', () => {
210
- debug.channel.created('test-channel');
211
- expect(console.log).not.toHaveBeenCalled();
212
- });
213
- });
214
-
215
- it('logs info messages', () => {
216
- debug.channel.created('test-channel');
217
- expect(console.log).toHaveBeenCalledWith(
218
- expect.stringContaining('[react-broadcast-sync]'),
219
- 'Channel created:',
220
- 'test-channel'
221
- );
222
- });
223
-
224
- it('logs warning messages', () => {
225
- debug.message.duplicate('test-id');
226
- expect(console.warn).toHaveBeenCalledWith(
227
- expect.stringContaining('[react-broadcast-sync]'),
228
- 'Duplicate message ignored:',
229
- 'test-id'
230
- );
231
- });
232
-
233
- it('logs error messages', () => {
234
- debug.error({ action: 'Test error' });
235
- expect(console.error).toHaveBeenCalledWith(
236
- expect.stringContaining('[react-broadcast-sync]'),
237
- 'Error:',
238
- 'Test error'
239
- );
240
- });
241
-
242
- it('logs cleanup information', () => {
243
- debug.cleanup.started();
244
- expect(console.log).toHaveBeenCalledWith(
245
- expect.stringContaining('[react-broadcast-sync]'),
246
- 'Cleanup started',
247
- ''
248
- );
249
-
250
- debug.cleanup.completed(2);
251
- expect(console.log).toHaveBeenCalledWith(
252
- expect.stringContaining('[react-broadcast-sync]'),
253
- 'Cleanup completed:',
254
- 2
255
- );
256
- });
257
-
258
- it('logs all-cleared messages', () => {
259
- debug.message.allSentCleared();
260
- expect(console.log).toHaveBeenCalledWith(
261
- expect.stringContaining('[react-broadcast-sync]'),
262
- 'All sent messages cleared',
263
- ''
264
- );
265
-
266
- debug.message.allReceivedCleared();
267
- expect(console.log).toHaveBeenCalledWith(
268
- expect.stringContaining('[react-broadcast-sync]'),
269
- 'All received messages cleared',
270
- ''
271
- );
272
- });
273
- });
@@ -1,252 +0,0 @@
1
- import {
2
- generateRandomPart,
3
- generateSourceName,
4
- generateMessageId,
5
- isValidMessage,
6
- isMessageExpired,
7
- createMessage,
8
- getInternalMessageType,
9
- isInternalType,
10
- isValidInternalClearMessage,
11
- debounce,
12
- } from '../utils/messageUtils';
13
-
14
- beforeEach(() => {
15
- jest.useFakeTimers();
16
- });
17
- afterEach(() => {
18
- jest.useRealTimers();
19
- });
20
-
21
- describe('messageUtils', () => {
22
- it('generateRandomPart returns a non-empty string', () => {
23
- const part = generateRandomPart();
24
- expect(typeof part).toBe('string');
25
- expect(part).toHaveLength(9);
26
- });
27
-
28
- it('generateSourceName includes "tab-" prefix', () => {
29
- expect(generateSourceName()).toMatch(/^tab-/);
30
- });
31
-
32
- it('generateMessageId returns a base64 string', () => {
33
- const id = generateMessageId('source-1', Date.now());
34
- expect(typeof id).toBe('string');
35
- expect(id).toMatch(/^[A-Za-z0-9+/]+={0,2}$/);
36
- });
37
-
38
- it('isValidMessage identifies a valid message object', () => {
39
- expect(isValidMessage({ id: '123' })).toBe(true);
40
- expect(isValidMessage(null)).toBe(false);
41
- expect(isValidMessage({})).toBe(false);
42
- });
43
-
44
- it('isMessageExpired works correctly', () => {
45
- const now = Date.now();
46
- expect(isMessageExpired({ expirationDate: now - 1000 })).toBe(true);
47
- expect(isMessageExpired({ expirationDate: now + 1000 })).toBe(false);
48
- expect(isMessageExpired({})).toBe(false);
49
- });
50
-
51
- it('createMessage returns a valid message object', () => {
52
- const source = 'tab-abc';
53
- const msg = createMessage('type1', { hello: 'world' }, source, { expirationDuration: 1000 });
54
- expect(msg).toHaveProperty('id');
55
- expect(msg).toHaveProperty('type', 'type1');
56
- expect(msg).toHaveProperty('message.hello', 'world');
57
- expect(msg).toHaveProperty('source', source);
58
- expect(msg.expirationDate).toBeGreaterThan(Date.now());
59
- });
60
-
61
- it('createMessage uses provided expirationDate when given', () => {
62
- const expirationDate = Date.now() + 5000;
63
- const message = createMessage('type', {}, 'source', { expirationDate });
64
- expect(message.expirationDate).toBe(expirationDate);
65
- });
66
-
67
- it('getInternalMessageType produces unique hashes for different channels', () => {
68
- const one = getInternalMessageType('CLEAR_SENT_MESSAGES', 'chanA');
69
- const two = getInternalMessageType('CLEAR_SENT_MESSAGES', 'chanB');
70
- expect(one).not.toBe(two);
71
- });
72
-
73
- it('getInternalMessageType generates consistent hash string', () => {
74
- const type = getInternalMessageType('CLEAR_SENT_MESSAGES', 'my-channel', 'ns');
75
- expect(type).toContain('__INTERNAL__');
76
- expect(type).toContain('CLEAR_SENT_MESSAGES');
77
- });
78
-
79
- it('isInternalType detects internal message types', () => {
80
- const type = getInternalMessageType('CLEAR_SENT_MESSAGES', 'chan');
81
- expect(isInternalType(type)).toBe(true);
82
- expect(isInternalType('normal-type')).toBe(false);
83
- });
84
-
85
- it('isValidInternalClearMessage detects valid structure', () => {
86
- const valid = {
87
- id: 'abc',
88
- type: getInternalMessageType('CLEAR_SENT_MESSAGES', 'ch'),
89
- source: 'tab-1',
90
- };
91
- const invalid = { id: 123, type: 'bad', source: 'tab-1' };
92
- expect(isValidInternalClearMessage(valid)).toBe(true);
93
- expect(isValidInternalClearMessage(invalid)).toBe(false);
94
- });
95
- });
96
-
97
- describe('debounce', () => {
98
- it('debounce delays function call and can cancel/flush', () => {
99
- const fn = jest.fn();
100
- const debounced = debounce(fn, 100);
101
-
102
- debounced();
103
- expect(fn).not.toHaveBeenCalled();
104
-
105
- debounced.flush();
106
- expect(fn).toHaveBeenCalled();
107
-
108
- debounced();
109
- debounced.cancel();
110
- jest.advanceTimersByTime(150);
111
- expect(fn).toHaveBeenCalledTimes(1);
112
- });
113
-
114
- it('debounce handles multiple calls and preserves this context', () => {
115
- const obj = {
116
- value: 0,
117
- increment: function () {
118
- this.value++;
119
- },
120
- };
121
- const debounced = debounce(obj.increment.bind(obj), 100);
122
-
123
- debounced();
124
- debounced();
125
- debounced.flush();
126
- expect(obj.value).toBe(1);
127
- });
128
-
129
- it('debounce flush returns undefined when no pending call', () => {
130
- const fn = jest.fn();
131
- const debounced = debounce(fn, 100);
132
-
133
- expect(debounced.flush()).toBeUndefined();
134
- expect(fn).not.toHaveBeenCalled();
135
- });
136
-
137
- it('debounce handles function with return value', () => {
138
- const fn = jest.fn().mockReturnValue('test');
139
- const debounced = debounce(fn, 100);
140
-
141
- debounced();
142
- expect(debounced.flush()).toBe('test');
143
- });
144
-
145
- it('debounce.cancel cancels the timeout', () => {
146
- const fn = jest.fn();
147
- const debounced = debounce(fn, 100);
148
- debounced();
149
- debounced.cancel();
150
- jest.advanceTimersByTime(200);
151
- expect(fn).not.toHaveBeenCalled();
152
- });
153
-
154
- it('debounce.flush executes immediately', () => {
155
- const fn = jest.fn();
156
- const debounced = debounce(fn, 100);
157
- debounced('test');
158
- debounced.flush();
159
- expect(fn).toHaveBeenCalledWith('test');
160
- });
161
-
162
- it('debounce handles multiple calls with different arguments', () => {
163
- const fn = jest.fn();
164
- const debounced = debounce(fn, 100);
165
-
166
- debounced('first');
167
- debounced('second');
168
- debounced('third');
169
-
170
- expect(fn).not.toHaveBeenCalled();
171
- debounced.flush();
172
- expect(fn).toHaveBeenCalledTimes(1);
173
- expect(fn).toHaveBeenCalledWith('third');
174
- });
175
-
176
- it('debounce handles cancel and flush in sequence', () => {
177
- const fn = jest.fn();
178
- const debounced = debounce(fn, 100);
179
-
180
- debounced('test');
181
- debounced.cancel();
182
- expect(debounced.flush()).toBeUndefined();
183
- expect(fn).not.toHaveBeenCalled();
184
- });
185
-
186
- it('debounce.flush executes the function with latest arguments immediately', () => {
187
- jest.useFakeTimers();
188
-
189
- const fn = jest.fn((x: number) => x + 1);
190
- const debounced = debounce(fn, 200);
191
-
192
- debounced(41);
193
-
194
- expect(fn).not.toHaveBeenCalled();
195
-
196
- const flushResult = debounced.flush();
197
-
198
- expect(fn).toHaveBeenCalledTimes(1);
199
- expect(fn).toHaveBeenCalledWith(41);
200
- expect(flushResult).toBe(42);
201
-
202
- jest.useRealTimers();
203
- });
204
-
205
- it('debounce.flush does nothing if timeout is set but lastArgs were reset manually', () => {
206
- const fn = jest.fn();
207
-
208
- const debounced = debounce(fn, 100);
209
-
210
- // Call once, schedule the debounce
211
- debounced('first');
212
-
213
- // Simulate internal cleanup (cannot access lastArgs directly, so instead cancel it)
214
- debounced.cancel(); // This will clear lastArgs and lastThis but not call fn
215
-
216
- const result = debounced.flush(); // Should do nothing
217
-
218
- expect(fn).not.toHaveBeenCalled();
219
- expect(result).toBeUndefined();
220
- });
221
-
222
- it('debounce.flush handles case where timeoutId exists but lastArgs is null', () => {
223
- const fn = jest.fn();
224
- const debounced = debounce(fn, 100);
225
-
226
- // Schedule a call
227
- debounced('test');
228
-
229
- // Force the timeout to be set but clear lastArgs
230
- jest.advanceTimersByTime(50); // Advance partially through the timeout
231
- debounced.cancel(); // This clears lastArgs but might not clear timeoutId immediately
232
-
233
- // Call flush when timeoutId exists but lastArgs is null
234
- const result = debounced.flush();
235
- expect(result).toBeUndefined();
236
- expect(fn).not.toHaveBeenCalled();
237
- });
238
-
239
- it('debounce.flush handles multiple calls with different arguments and preserves return value', () => {
240
- const fn = jest.fn((x: number) => x * 2);
241
- const debounced = debounce(fn, 100);
242
-
243
- debounced(1);
244
- debounced(2);
245
- debounced(3);
246
-
247
- const result = debounced.flush();
248
- expect(result).toBe(6); // 3 * 2
249
- expect(fn).toHaveBeenCalledTimes(1);
250
- expect(fn).toHaveBeenCalledWith(3);
251
- });
252
- });