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
@@ -28,7 +28,7 @@ class MockBroadcastChannel {
28
28
  }
29
29
 
30
30
  const waitForChannel = async () => {
31
- return new Promise<void>((resolve) => {
31
+ return new Promise<void>(resolve => {
32
32
  const checkChannel = () => {
33
33
  if (mockChannels.length > 0) {
34
34
  resolve();
@@ -73,7 +73,7 @@ describe('useBroadcastChannel', () => {
73
73
  type: 'greeting',
74
74
  message: { text: 'hi' },
75
75
  source: 'another-tab',
76
- timestamp: Date.now()
76
+ timestamp: Date.now(),
77
77
  };
78
78
 
79
79
  act(() => {
@@ -108,7 +108,7 @@ describe('useBroadcastChannel', () => {
108
108
  type: 'greeting',
109
109
  message: 'hi',
110
110
  source: 'external',
111
- timestamp: Date.now()
111
+ timestamp: Date.now(),
112
112
  };
113
113
 
114
114
  act(() => {
@@ -135,14 +135,14 @@ describe('useBroadcastChannel', () => {
135
135
  type: 'a',
136
136
  message: 'a',
137
137
  source: 'x',
138
- timestamp: Date.now()
138
+ timestamp: Date.now(),
139
139
  });
140
140
  channel.simulateMessage({
141
141
  id: '2',
142
142
  type: 'b',
143
143
  message: 'b',
144
144
  source: 'x',
145
- timestamp: Date.now()
145
+ timestamp: Date.now(),
146
146
  });
147
147
  });
148
148
 
@@ -158,7 +158,7 @@ describe('useBroadcastChannel', () => {
158
158
  it('clears sent message', async () => {
159
159
  const { result } = renderHook(() => useBroadcastChannel('test-channel'));
160
160
  await waitForChannel();
161
-
161
+
162
162
  act(() => {
163
163
  result.current.postMessage('test-type', { key: 'value' });
164
164
  });
@@ -177,7 +177,7 @@ describe('useBroadcastChannel', () => {
177
177
  jest.useFakeTimers();
178
178
 
179
179
  const original = global.BroadcastChannel;
180
- // @ts-ignore
180
+ // @ts-expect-error - Testing invalid input
181
181
  delete global.BroadcastChannel;
182
182
 
183
183
  const { result } = renderHook(() => useBroadcastChannel('test-channel'));
@@ -200,7 +200,9 @@ describe('useBroadcastChannel', () => {
200
200
 
201
201
  const { result } = renderHook(() => useBroadcastChannel('unsupported-channel'));
202
202
 
203
- expect(result.current.error).toBe('BroadcastChannel is not supported in this browser. Please check browser compatibility.');
203
+ expect(result.current.error).toBe(
204
+ 'BroadcastChannel is not supported in this browser. Please check browser compatibility.'
205
+ );
204
206
  global.BroadcastChannel = original;
205
207
  });
206
208
 
@@ -227,7 +229,7 @@ describe('useBroadcastChannel', () => {
227
229
  message: 'expired',
228
230
  source: 'another-tab',
229
231
  timestamp: Date.now() - 1000000,
230
- expirationDate: Date.now() - 1000000
232
+ expirationDate: Date.now() - 1000000,
231
233
  };
232
234
 
233
235
  act(() => {
@@ -238,7 +240,9 @@ describe('useBroadcastChannel', () => {
238
240
  });
239
241
 
240
242
  it('handles duplicate messages within TTL', async () => {
241
- const { result } = renderHook(() => useBroadcastChannel('test-channel', { deduplicationTTL: 5000 }));
243
+ const { result } = renderHook(() =>
244
+ useBroadcastChannel('test-channel', { deduplicationTTL: 5000 })
245
+ );
242
246
  await waitForChannel();
243
247
  const channel = mockChannels[0];
244
248
 
@@ -247,7 +251,7 @@ describe('useBroadcastChannel', () => {
247
251
  type: 'test',
248
252
  message: 'duplicate',
249
253
  source: 'another-tab',
250
- timestamp: Date.now()
254
+ timestamp: Date.now(),
251
255
  };
252
256
 
253
257
  act(() => {
@@ -269,7 +273,7 @@ describe('useBroadcastChannel', () => {
269
273
  type: 'test',
270
274
  message: 'will be cleared',
271
275
  source: 'another-tab',
272
- timestamp: Date.now()
276
+ timestamp: Date.now(),
273
277
  });
274
278
  });
275
279
 
@@ -278,9 +282,10 @@ describe('useBroadcastChannel', () => {
278
282
  act(() => {
279
283
  channel.simulateMessage({
280
284
  id: 'to-clear',
281
- type: '__INTERNAL__:CLEAR_MESSAGE:' + btoa('react-broadcast-sync:CLEAR_MESSAGE:test-channel-'),
285
+ type:
286
+ '__INTERNAL__:CLEAR_MESSAGE:' + btoa('react-broadcast-sync:CLEAR_MESSAGE:test-channel-'),
282
287
  source: 'another-tab',
283
- timestamp: Date.now()
288
+ timestamp: Date.now(),
284
289
  });
285
290
  });
286
291
 
@@ -298,14 +303,14 @@ describe('useBroadcastChannel', () => {
298
303
  type: 'test1',
299
304
  message: 'first',
300
305
  source: 'another-tab',
301
- timestamp: Date.now()
306
+ timestamp: Date.now(),
302
307
  });
303
308
  channel.simulateMessage({
304
309
  id: 'msg2',
305
310
  type: 'test2',
306
311
  message: 'second',
307
312
  source: 'another-tab',
308
- timestamp: Date.now()
313
+ timestamp: Date.now(),
309
314
  });
310
315
  });
311
316
 
@@ -313,10 +318,14 @@ describe('useBroadcastChannel', () => {
313
318
 
314
319
  act(() => {
315
320
  channel.simulateMessage({
316
- id: '__INTERNAL__:CLEAR_ALL_MESSAGES:' + btoa('react-broadcast-sync:CLEAR_ALL_MESSAGES:test-channel-'),
317
- type: '__INTERNAL__:CLEAR_ALL_MESSAGES:' + btoa('react-broadcast-sync:CLEAR_ALL_MESSAGES:test-channel-'),
321
+ id:
322
+ '__INTERNAL__:CLEAR_ALL_MESSAGES:' +
323
+ btoa('react-broadcast-sync:CLEAR_ALL_MESSAGES:test-channel-'),
324
+ type:
325
+ '__INTERNAL__:CLEAR_ALL_MESSAGES:' +
326
+ btoa('react-broadcast-sync:CLEAR_ALL_MESSAGES:test-channel-'),
318
327
  source: 'another-tab',
319
- timestamp: Date.now()
328
+ timestamp: Date.now(),
320
329
  });
321
330
  });
322
331
 
@@ -325,7 +334,9 @@ describe('useBroadcastChannel', () => {
325
334
 
326
335
  it('performs cleanup of expired messages', async () => {
327
336
  jest.useFakeTimers();
328
- const { result } = renderHook(() => useBroadcastChannel('test-channel', { cleaningInterval: 1000 }));
337
+ const { result } = renderHook(() =>
338
+ useBroadcastChannel('test-channel', { cleaningInterval: 1000 })
339
+ );
329
340
  await waitForChannel();
330
341
  const channel = mockChannels[0];
331
342
 
@@ -336,7 +347,7 @@ describe('useBroadcastChannel', () => {
336
347
  message: 'expired',
337
348
  source: 'another-tab',
338
349
  timestamp: Date.now() - 1000000,
339
- expirationDate: Date.now() - 1000000
350
+ expirationDate: Date.now() - 1000000,
340
351
  });
341
352
  });
342
353
 
@@ -349,7 +360,7 @@ describe('useBroadcastChannel', () => {
349
360
  });
350
361
 
351
362
  it('handles registered types filtering', async () => {
352
- const { result } = renderHook(() =>
363
+ const { result } = renderHook(() =>
353
364
  useBroadcastChannel('test-channel', { registeredTypes: ['allowed-type'] })
354
365
  );
355
366
  await waitForChannel();
@@ -361,7 +372,7 @@ describe('useBroadcastChannel', () => {
361
372
  type: 'unregistered-type',
362
373
  message: 'test',
363
374
  source: 'another-tab',
364
- timestamp: Date.now()
375
+ timestamp: Date.now(),
365
376
  });
366
377
  });
367
378
 
@@ -373,7 +384,7 @@ describe('useBroadcastChannel', () => {
373
384
  type: 'allowed-type',
374
385
  message: 'test',
375
386
  source: 'another-tab',
376
- timestamp: Date.now()
387
+ timestamp: Date.now(),
377
388
  });
378
389
  });
379
390
 
@@ -381,7 +392,7 @@ describe('useBroadcastChannel', () => {
381
392
  });
382
393
 
383
394
  it('handles keepLatestMessage option', async () => {
384
- const { result } = renderHook(() =>
395
+ const { result } = renderHook(() =>
385
396
  useBroadcastChannel('test-channel', { keepLatestMessage: true })
386
397
  );
387
398
  await waitForChannel();
@@ -393,14 +404,14 @@ describe('useBroadcastChannel', () => {
393
404
  type: 'test',
394
405
  message: 'first',
395
406
  source: 'another-tab',
396
- timestamp: Date.now()
407
+ timestamp: Date.now(),
397
408
  });
398
409
  channel.simulateMessage({
399
410
  id: 'msg2',
400
411
  type: 'test',
401
412
  message: 'second',
402
413
  source: 'another-tab',
403
- timestamp: Date.now()
414
+ timestamp: Date.now(),
404
415
  });
405
416
  });
406
417
 
@@ -410,10 +421,10 @@ describe('useBroadcastChannel', () => {
410
421
 
411
422
  it('handles cleanup debounce', async () => {
412
423
  jest.useFakeTimers();
413
- const { result } = renderHook(() =>
414
- useBroadcastChannel('test-channel', {
424
+ const { result } = renderHook(() =>
425
+ useBroadcastChannel('test-channel', {
415
426
  cleaningInterval: 1000,
416
- cleanupDebounceMs: 500
427
+ cleanupDebounceMs: 500,
417
428
  })
418
429
  );
419
430
  await waitForChannel();
@@ -426,7 +437,7 @@ describe('useBroadcastChannel', () => {
426
437
  message: 'expired',
427
438
  source: 'another-tab',
428
439
  timestamp: Date.now() - 1000000,
429
- expirationDate: Date.now() - 1000000
440
+ expirationDate: Date.now() - 1000000,
430
441
  });
431
442
  });
432
443
 
@@ -463,7 +474,9 @@ describe('useBroadcastChannel', () => {
463
474
 
464
475
  const badMessage: any = {};
465
476
  Object.defineProperty(badMessage, 'id', {
466
- get() { throw new Error('fail'); }
477
+ get() {
478
+ throw new Error('fail');
479
+ },
467
480
  });
468
481
  badMessage.type = 'test';
469
482
  badMessage.source = 'z';
@@ -484,7 +497,9 @@ describe('useBroadcastChannel', () => {
484
497
 
485
498
  const { result } = renderHook(() => useBroadcastChannel('test-channel'));
486
499
 
487
- expect(result.current.error).toBe('BroadcastChannel is not supported in this browser. Please check browser compatibility.');
500
+ expect(result.current.error).toBe(
501
+ 'BroadcastChannel is not supported in this browser. Please check browser compatibility.'
502
+ );
488
503
 
489
504
  global.BroadcastChannel = originalBroadcastChannel;
490
505
  });
@@ -4,16 +4,16 @@
4
4
  export interface BroadcastOptions {
5
5
  /** Custom source name for this instance */
6
6
  sourceName?: string;
7
-
7
+
8
8
  /** Interval in milliseconds for cleaning expired messages (default: 1000) */
9
9
  cleaningInterval?: number;
10
-
10
+
11
11
  /** If true, only the latest message is kept */
12
12
  keepLatestMessage?: boolean;
13
-
13
+
14
14
  /** Array of allowed message types */
15
15
  registeredTypes?: string[];
16
-
16
+
17
17
  /** Channel namespace for isolation */
18
18
  namespace?: string;
19
19
 
@@ -30,7 +30,7 @@ export interface BroadcastOptions {
30
30
  export interface SendMessageOptions {
31
31
  /** Message expiration duration in milliseconds */
32
32
  expirationDuration?: number;
33
-
33
+
34
34
  /** Specific expiration timestamp */
35
35
  expirationDate?: number;
36
36
  }
@@ -41,19 +41,19 @@ export interface SendMessageOptions {
41
41
  export interface BroadcastMessage {
42
42
  /** Unique message identifier */
43
43
  id: string;
44
-
44
+
45
45
  /** Message type for categorization */
46
46
  type: string;
47
-
47
+
48
48
  /** Message content */
49
49
  message: any;
50
-
50
+
51
51
  /** Creation timestamp */
52
52
  timestamp: number;
53
-
53
+
54
54
  /** Source identifier */
55
55
  source: string;
56
-
56
+
57
57
  /** Optional expiration timestamp */
58
58
  expirationDate?: number;
59
59
  }
@@ -64,29 +64,25 @@ export interface BroadcastMessage {
64
64
  export interface BroadcastActions {
65
65
  /** The resolved channel name including namespace */
66
66
  channelName: string;
67
-
67
+
68
68
  /** Array of received messages */
69
69
  messages: BroadcastMessage[];
70
-
70
+
71
71
  /** Array of sent messages */
72
72
  sentMessages: BroadcastMessage[];
73
-
73
+
74
74
  /** Function to send a message */
75
- postMessage: (
76
- messageType: string,
77
- messageContent: any,
78
- options?: SendMessageOptions
79
- ) => void;
80
-
75
+ postMessage: (messageType: string, messageContent: any, options?: SendMessageOptions) => void;
76
+
81
77
  /** Function to clear a specific message */
82
78
  clearMessage: (id: string) => void;
83
-
79
+
84
80
  /** Function to clear all messages */
85
81
  clearAllMessages: () => void;
86
-
82
+
87
83
  /** Function to clear a specific sent message */
88
84
  clearSentMessage: (id: string) => void;
89
-
85
+
90
86
  /** Current error state */
91
87
  error: string | null;
92
- }
88
+ }
@@ -44,4 +44,4 @@ export const debug = {
44
44
  completed: (removedCount: number) => log('info', 'Cleanup completed:', { removedCount }),
45
45
  },
46
46
  error: (error: string) => log('error', 'Error:', error),
47
- };
47
+ };
@@ -37,28 +37,30 @@ export const createMessage = (
37
37
  message: content,
38
38
  timestamp,
39
39
  source,
40
- expirationDate: options.expirationDate ??
40
+ expirationDate:
41
+ options.expirationDate ??
41
42
  (options.expirationDuration ? timestamp + options.expirationDuration : undefined),
42
43
  };
43
44
  };
44
45
 
45
-
46
46
  export const getInternalMessageType = (
47
- baseType: 'CLEAR_MESSAGE' | 'CLEAR_ALL_MESSAGES',
48
- channelName: string,
49
- namespace = ''
50
- ): string => {
51
- const fullChannel = `${channelName}-${namespace}`;
52
- const input = `${SECRET}:${baseType}:${fullChannel}`;
53
- const hash = btoa(input); // optional: use sha256 if added
54
- return `${INTERNAL_PREFIX}:${baseType}:${hash}`;
55
- };
47
+ baseType: 'CLEAR_MESSAGE' | 'CLEAR_ALL_MESSAGES',
48
+ channelName: string,
49
+ namespace = ''
50
+ ): string => {
51
+ const fullChannel = `${channelName}-${namespace}`;
52
+ const input = `${SECRET}:${baseType}:${fullChannel}`;
53
+ const hash = btoa(input); // optional: use sha256 if added
54
+ return `${INTERNAL_PREFIX}:${baseType}:${hash}`;
55
+ };
56
56
 
57
57
  export const isInternalType = (type: string): boolean => {
58
- return type.startsWith(`${INTERNAL_PREFIX}:`);
58
+ return type.startsWith(`${INTERNAL_PREFIX}:`);
59
59
  };
60
60
 
61
- export const isValidInternalClearMessage = (message: any): message is { id: string, type: string, source: string } => {
61
+ export const isValidInternalClearMessage = (
62
+ message: any
63
+ ): message is { id: string; type: string; source: string } => {
62
64
  return (
63
65
  message &&
64
66
  typeof message === 'object' &&
@@ -67,7 +69,7 @@ export const isValidInternalClearMessage = (message: any): message is { id: stri
67
69
  typeof message.type === 'string' &&
68
70
  isInternalType(message.type)
69
71
  );
70
- }
72
+ };
71
73
 
72
74
  export const debounce = <T extends (...args: any[]) => any>(
73
75
  fn: T,
@@ -78,23 +80,18 @@ export const debounce = <T extends (...args: any[]) => any>(
78
80
  } => {
79
81
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
80
82
  let lastArgs: Parameters<T> | null = null;
81
- let lastThis: any = null;
82
83
  let result: ReturnType<T> | undefined;
83
84
 
84
- const debounced = function(this: any, ...args: Parameters<T>) {
85
+ const debounced = (...args: Parameters<T>) => {
85
86
  lastArgs = args;
86
- lastThis = this;
87
-
88
87
  if (timeoutId !== null) {
89
88
  clearTimeout(timeoutId);
90
89
  }
91
-
92
90
  timeoutId = setTimeout(() => {
93
91
  timeoutId = null;
94
92
  if (lastArgs !== null) {
95
- result = fn.apply(lastThis, lastArgs);
93
+ result = fn(...lastArgs);
96
94
  lastArgs = null;
97
- lastThis = null;
98
95
  }
99
96
  }, wait);
100
97
  };
@@ -104,7 +101,6 @@ export const debounce = <T extends (...args: any[]) => any>(
104
101
  clearTimeout(timeoutId);
105
102
  timeoutId = null;
106
103
  lastArgs = null;
107
- lastThis = null;
108
104
  }
109
105
  };
110
106
 
@@ -113,9 +109,8 @@ export const debounce = <T extends (...args: any[]) => any>(
113
109
  clearTimeout(timeoutId);
114
110
  timeoutId = null;
115
111
  if (lastArgs !== null) {
116
- result = fn.apply(lastThis, lastArgs);
112
+ result = fn(...lastArgs);
117
113
  lastArgs = null;
118
- lastThis = null;
119
114
  }
120
115
  }
121
116
  return result;
@@ -1,3 +0,0 @@
1
- import { BroadcastActions, BroadcastOptions } from './types/types';
2
- export declare const useBroadcastChannel: (channelName: string, options?: BroadcastOptions) => BroadcastActions;
3
- export default useBroadcastChannel;