react-broadcast-sync 1.0.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.
@@ -0,0 +1,61 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ llint-and-typecheck:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v3
14
+
15
+ - name: Setup Node.js
16
+ uses: actions/setup-node@v4
17
+ with:
18
+ node-version: 18
19
+
20
+ - name: Install dependencies
21
+ run: npm ci
22
+
23
+ - name: Run linter
24
+ run: npm run lint
25
+
26
+ - name: Run Prettier check
27
+ run: npm run format:check
28
+
29
+ - name: Run type checks
30
+ run: npm run typecheck
31
+
32
+ build-and-test:
33
+ runs-on: ubuntu-latest
34
+
35
+ strategy:
36
+ matrix:
37
+ node-version: [18, 20]
38
+
39
+ steps:
40
+ - uses: actions/checkout@v3
41
+
42
+ - name: Setup Node.js ${{ matrix.node-version }}
43
+ uses: actions/setup-node@v4
44
+ with:
45
+ node-version: ${{ matrix.node-version }}
46
+
47
+ - name: Install dependencies
48
+ run: npm ci
49
+
50
+ - name: Run build
51
+ run: npm run build
52
+
53
+ - name: Run tests with coverage
54
+ run: npm test -- --coverage
55
+
56
+ - name: Upload coverage to Codecov
57
+ uses: codecov/codecov-action@v4
58
+ with:
59
+ token: ${{ secrets.CODECOV_TOKEN }}
60
+ files: ./coverage/lcov.info
61
+ fail_ci_if_error: true
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ npx lint-staged
5
+ npm run typecheck
6
+ npm run lint-staged
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+ coverage
package/.prettierrc ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "es5"
5
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "branches": ["main"],
3
+ "plugins": [
4
+ "@semantic-release/commit-analyzer",
5
+ "@semantic-release/release-notes-generator",
6
+ [
7
+ "@semantic-release/changelog",
8
+ {
9
+ "changelogFile": "CHANGELOG.md"
10
+ }
11
+ ],
12
+ "@semantic-release/npm",
13
+ [
14
+ "@semantic-release/git",
15
+ {
16
+ "assets": ["CHANGELOG.md", "package.json"],
17
+ "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
18
+ }
19
+ ],
20
+ "@semantic-release/github"
21
+ ]
22
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Idan Shalem
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,495 @@
1
+ <p align="center">
2
+ <img src="./assets/react-broadcast-sync-logo.png" alt="react-broadcast-sync logo" width="300" />
3
+ </p>
4
+
5
+ # react-broadcast-sync
6
+
7
+ ![CI](https://github.com/IdanShalem/react-broadcast-sync/actions/workflows/ci.yml/badge.svg)
8
+ [![codecov](https://codecov.io/gh/IdanShalem/react-broadcast-sync/branch/main/graph/badge.svg)](https://codecov.io/gh/IdanShalem/react-broadcast-sync)
9
+ [![npm version](https://img.shields.io/npm/v/react-broadcast-sync.svg)](https://www.npmjs.com/package/react-broadcast-sync)
10
+ [![npm downloads](https://img.shields.io/npm/dm/react-broadcast-sync.svg)](https://www.npmjs.com/package/react-broadcast-sync)
11
+ [![bundlephobia](https://badgen.net/bundlephobia/minzip/react-broadcast-sync)](https://bundlephobia.com/result?p=react-broadcast-sync)
12
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
13
+ [![Issues](https://img.shields.io/github/issues/IdanShalem/react-broadcast-sync)](https://github.com/IdanShalem/react-broadcast-sync/issues)
14
+
15
+ A lightweight React hook for syncing state and communication across browser tabs using the BroadcastChannel API. This package provides a clean and type-safe abstraction over the native API, enabling efficient, scoped, and reliable cross-tab messaging.
16
+
17
+ ## โœจ Features
18
+
19
+ - ๐Ÿš€ Simple and intuitive API
20
+ - ๐Ÿ”„ Real-time synchronization across tabs
21
+ - โœ… TypeScript support
22
+ - ๐Ÿ“ฆ Zero dependencies
23
+ - ๐Ÿงน Automatic message expiration and cleanup
24
+ - ๐Ÿงช Send/receive any serializable message
25
+ - ๐Ÿงฉ Namespace and source scoping support
26
+ - ๐Ÿ”’ Clear individual or all messages
27
+ - โœ… Only accept allowed message types (optional)
28
+ - ๐Ÿง  `BroadcastProvider` for context-based usage
29
+
30
+ ---
31
+
32
+ ## ๐Ÿ“ฆ Installation
33
+
34
+ ```bash
35
+ npm install react-broadcast-sync
36
+ # or
37
+ yarn add react-broadcast-sync
38
+ # or
39
+ pnpm add react-broadcast-sync
40
+ ```
41
+
42
+ ---
43
+
44
+ ## ๐Ÿš€ Quick Start
45
+
46
+ ### ๐Ÿ”ง Basic Usage
47
+
48
+ ```tsx
49
+ import { useBroadcastChannel } from 'react-broadcast-sync';
50
+
51
+ function MyComponent() {
52
+ const { messages, postMessage, clearMessage } =
53
+ useBroadcastChannel('my-channel');
54
+
55
+ const handleSend = () => {
56
+ postMessage('greeting', { text: 'Hello from another tab!' });
57
+ };
58
+
59
+ return (
60
+ <>
61
+ <button onClick={handleSend}>Send</button>
62
+ {messages.map((msg) => (
63
+ <div key={msg.id}>
64
+ {msg.message.text}
65
+ <button onClick={() => clearMessage(msg.id)}>Clear</button>
66
+ </div>
67
+ ))}
68
+ </>
69
+ );
70
+ }
71
+ ```
72
+
73
+ ---
74
+
75
+ ### โš™๏ธ Advanced Usage
76
+
77
+ ```tsx
78
+ const {
79
+ channelName,
80
+ messages,
81
+ sentMessages,
82
+ postMessage,
83
+ clearMessage,
84
+ clearAllMessages,
85
+ clearSentMessage,
86
+ error,
87
+ } = useBroadcastChannel('my-channel', {
88
+ sourceName: 'my-tab',
89
+ cleaningInterval: 2000,
90
+ keepLatestMessage: true,
91
+ registeredTypes: ['greeting', 'notification'],
92
+ namespace: 'my-app',
93
+ deduplicationTTL: 10 * 60 * 1000, // 10 minutes
94
+ cleanupDebounceMs: 500, // Debounce cleanup operations by 500ms
95
+ });
96
+ ```
97
+
98
+ #### ๐Ÿ’ฌ Sending a message with expiration:
99
+
100
+ ```tsx
101
+ postMessage(
102
+ 'notification',
103
+ { text: 'This disappears in 5s' },
104
+ { expirationDuration: 5000 }
105
+ );
106
+ ```
107
+
108
+ ---
109
+
110
+ ### ๐Ÿงฉ Using `BroadcastProvider`
111
+
112
+ You can wrap part of your app with `BroadcastProvider` and use `useBroadcastProvider()` to consume the channel context.
113
+
114
+ ```tsx
115
+ import { BroadcastProvider, useBroadcastProvider } from 'react-broadcast-sync';
116
+
117
+ function App() {
118
+ return (
119
+ <BroadcastProvider channelName="notifications">
120
+ <NotificationBar />
121
+ </BroadcastProvider>
122
+ );
123
+ }
124
+
125
+ function NotificationBar() {
126
+ const { messages } = useBroadcastProvider();
127
+
128
+ return (
129
+ <div>
130
+ {messages.map((msg) => (
131
+ <p key={msg.id}>{msg.message.text}</p>
132
+ ))}
133
+ </div>
134
+ );
135
+ }
136
+ ```
137
+
138
+ ---
139
+
140
+ ## ๐Ÿ“˜ API Reference
141
+
142
+ ### `useBroadcastChannel` Hook
143
+
144
+ ```typescript
145
+ const {
146
+ channelName,
147
+ messages,
148
+ sentMessages,
149
+ postMessage,
150
+ clearMessage,
151
+ clearAllMessages,
152
+ clearSentMessage,
153
+ error,
154
+ } = useBroadcastChannel(channelName, options);
155
+ ```
156
+
157
+ #### Options
158
+
159
+ ```typescript
160
+ interface BroadcastOptions {
161
+ sourceName?: string; // Custom name for the message source
162
+ cleaningInterval?: number; // Interval in ms for cleaning expired messages (default: 1000)
163
+ keepLatestMessage?: boolean; // Keep only the latest message (default: false)
164
+ registeredTypes?: string[]; // List of allowed message types
165
+ namespace?: string; // Channel namespace for isolation
166
+ deduplicationTTL?: number; // Time in ms to keep message IDs for deduplication (default: 5 minutes)
167
+ cleanupDebounceMs?: number; // Debounce time in ms for cleanup operations (default: 0)
168
+ }
169
+ ```
170
+
171
+ #### Default Values
172
+
173
+ | Option | Default Value | Description |
174
+ | ------------------- | ------------- | ----------------------------------- |
175
+ | `sourceName` | `undefined` | Auto-generated if not provided |
176
+ | `cleaningInterval` | `1000` | 1 second between cleanup runs |
177
+ | `keepLatestMessage` | `false` | Keep all messages by default |
178
+ | `registeredTypes` | `[]` | Accept all message types by default |
179
+ | `namespace` | `''` | No namespace by default |
180
+ | `deduplicationTTL` | `300000` | 5 minutes (5 _ 60 _ 1000 ms) |
181
+ | `cleanupDebounceMs` | `0` | No debounce by default |
182
+
183
+ #### Return Value
184
+
185
+ ```typescript
186
+ interface BroadcastActions {
187
+ channelName: string; // The resolved channel name (includes namespace)
188
+ messages: BroadcastMessage[]; // Received messages
189
+ sentMessages: BroadcastMessage[]; // Messages sent by this instance
190
+ postMessage: (
191
+ type: string,
192
+ content: any,
193
+ options?: SendMessageOptions
194
+ ) => void;
195
+ clearMessage: (id: string) => void;
196
+ clearAllMessages: () => void;
197
+ clearSentMessage: (id: string) => void;
198
+ error: string | null; // Current error state
199
+ }
200
+ ```
201
+
202
+ ### `useBroadcastChannel(channelName, options?)`
203
+
204
+ Returns an object with:
205
+
206
+ | Property | Type | Description |
207
+ | -------------------- | -------------------- | ------------------------------------------------------ |
208
+ | `channelName` | `string` | The resolved channel name (includes namespace) |
209
+ | `messages` | `BroadcastMessage[]` | All received messages from other tabs |
210
+ | `sentMessages` | `BroadcastMessage[]` | Messages sent from this tab |
211
+ | `postMessage()` | `function` | Send a message to all tabs |
212
+ | `clearMessage()` | `function` | Remove a message locally and notify others to clear it |
213
+ | `clearAllMessages()` | `function` | Remove all messages locally and notify others |
214
+ | `clearSentMessage()` | `function` | Remove a sent message without affecting others |
215
+ | `error` | `string \| null` | Any runtime error from the channel |
216
+
217
+ #### ๐Ÿ“จ Send Options:
218
+
219
+ ```ts
220
+ interface SendMessageOptions {
221
+ expirationDuration?: number; // TTL in ms
222
+ expirationDate?: number; // Exact expiry timestamp
223
+ }
224
+ ```
225
+
226
+ #### ๐Ÿ“ฉ Message Format:
227
+
228
+ ```ts
229
+ interface BroadcastMessage {
230
+ id: string;
231
+ type: string;
232
+ message: any;
233
+ timestamp: number;
234
+ source: string;
235
+ expirationDate?: number;
236
+ }
237
+ ```
238
+
239
+ ---
240
+
241
+ ## โœ… Best Practices
242
+
243
+ - **Use `namespace`** to isolate functionality between different app modules.
244
+ - **Register allowed message types** using `registeredTypes` to avoid processing unknown or irrelevant messages.
245
+ - **Always handle `error`** state in UI or logs to detect channel failures.
246
+ - **Use `keepLatestMessage: true`** if you only care about the most recent message (e.g. status updates).
247
+ - **Set appropriate `deduplicationTTL`** based on your message frequency and importance.
248
+ - **Use `cleanupDebounceMs`** when dealing with rapid message updates to prevent performance issues.
249
+
250
+ ## ๐ŸŽฏ Common Use Cases
251
+
252
+ ### Real-time Notifications
253
+
254
+ ```tsx
255
+ function NotificationSystem() {
256
+ const { messages, postMessage } = useBroadcastChannel('notifications', {
257
+ keepLatestMessage: true,
258
+ registeredTypes: ['alert', 'info', 'warning'],
259
+ deduplicationTTL: 60000, // 1 minute
260
+ });
261
+
262
+ return (
263
+ <div>
264
+ {messages.map((msg) => (
265
+ <Notification key={msg.id} type={msg.type} content={msg.message} />
266
+ ))}
267
+ </div>
268
+ );
269
+ }
270
+ ```
271
+
272
+ ### Multi-tab Form Synchronization
273
+
274
+ ```tsx
275
+ function FormSync() {
276
+ const { messages, postMessage } = useBroadcastChannel('form-sync', {
277
+ namespace: 'my-form',
278
+ cleaningInterval: 5000,
279
+ });
280
+
281
+ const handleChange = (field: string, value: string) => {
282
+ postMessage(
283
+ 'field-update',
284
+ { field, value },
285
+ { expirationDuration: 300000 }
286
+ ); // 5 minutes
287
+ };
288
+
289
+ return <Form onChange={handleChange} />;
290
+ }
291
+ ```
292
+
293
+ ### Tab Status Synchronization
294
+
295
+ ```tsx
296
+ function TabStatus() {
297
+ const { postMessage } = useBroadcastChannel('tab-status', {
298
+ sourceName: 'main-tab',
299
+ keepLatestMessage: true,
300
+ });
301
+
302
+ useEffect(() => {
303
+ postMessage('tab-active', { timestamp: Date.now() });
304
+ return () => postMessage('tab-inactive', { timestamp: Date.now() });
305
+ }, []);
306
+
307
+ return null;
308
+ }
309
+ ```
310
+
311
+ ## ๐Ÿ”ง Performance Considerations
312
+
313
+ ### Message Deduplication
314
+
315
+ The `deduplicationTTL` option creates a time window (in milliseconds) during which messages with the same content and type from the same source are considered duplicates and will be ignored. This is particularly useful for:
316
+
317
+ - **Preventing Message Loops**: Avoids infinite message echo between tabs when they broadcast the same message back and forth
318
+ - **Reducing Redundancy**: Filters out identical messages sent in rapid succession, preventing unnecessary processing
319
+ - **Natural Debouncing**: Provides built-in debouncing behavior for broadcast events without additional code
320
+
321
+ Recommended TTL values based on use case:
322
+
323
+ - **High-frequency updates** (e.g., real-time typing, cursor position): 1000-5000ms
324
+ - **Medium-frequency updates** (e.g., form sync, status changes): 5000-15000ms
325
+ - **Low-frequency updates** (e.g., notifications, alerts): 15000-30000ms
326
+
327
+ Example:
328
+
329
+ ```tsx
330
+ // Without deduplication, this could cause a message loop
331
+ function ChatComponent() {
332
+ const { postMessage } = useBroadcastChannel('chat', {
333
+ deduplicationTTL: 5000, // Ignore duplicate messages for 5 seconds
334
+ });
335
+
336
+ const handleMessage = (text: string) => {
337
+ postMessage('chat-message', { text });
338
+ };
339
+ }
340
+ ```
341
+
342
+ ### Cleanup Optimization
343
+
344
+ - Use `cleanupDebounceMs` to prevent excessive cleanup operations
345
+ - Recommended values:
346
+ - For frequent updates: 500-1000ms
347
+ - For infrequent updates: 0ms (no debounce)
348
+ - Adjust `cleaningInterval` based on your message expiration needs
349
+
350
+ ### Memory Management
351
+
352
+ - Clear messages when they're no longer needed using `clearMessage` or `clearAllMessages`
353
+ - Use message expiration for temporary data
354
+ - Consider using `keepLatestMessage: true` for status updates to prevent memory buildup
355
+
356
+ ## ๐Ÿšจ Troubleshooting
357
+
358
+ ### Common Issues
359
+
360
+ 1. **Messages Not Received**
361
+
362
+ - Check if `registeredTypes` includes your message type
363
+ - Verify the channel name and namespace match
364
+ - Ensure the message hasn't expired
365
+ - Check if `deduplicationTTL` isn't too short
366
+
367
+ 2. **Performance Issues**
368
+
369
+ - Increase `cleanupDebounceMs` if cleanup is too frequent
370
+ - Use `keepLatestMessage: true` for high-frequency updates
371
+ - Consider increasing `cleaningInterval` if cleanup is too aggressive
372
+
373
+ 3. **Memory Leaks**
374
+ - Ensure proper cleanup in component unmount
375
+ - Use message expiration for temporary data
376
+ - Clear messages when they're no longer needed
377
+
378
+ ### Debug Mode
379
+
380
+ Enable debug logging by setting the environment variable:
381
+
382
+ ```bash
383
+ REACT_APP_DEBUG_BROADCAST=true
384
+ ```
385
+
386
+ This will log:
387
+
388
+ - Channel creation and closure
389
+ - Message sending and receiving
390
+ - Cleanup operations
391
+ - Error states
392
+
393
+ ## โšก Performance Considerations
394
+
395
+ 1. **Message Size**
396
+
397
+ - Keep messages small and serializable
398
+ - Avoid sending large objects or circular references
399
+ - Consider using message IDs to reference larger data
400
+
401
+ 2. **Message Frequency**
402
+
403
+ - Use `keepLatestMessage: true` for high-frequency updates
404
+ - Implement debouncing for rapid state changes
405
+ - Consider using `expirationDuration` for temporary messages
406
+
407
+ 3. **Memory Management**
408
+ - Clear messages when they're no longer needed
409
+ - Use `cleaningInterval` to automatically remove expired messages
410
+ - Implement proper cleanup in component unmount
411
+
412
+ ## ๐Ÿงช Testing
413
+
414
+ ### Unit Testing
415
+
416
+ ```tsx
417
+ import { renderHook, act } from '@testing-library/react-hooks';
418
+ import { useBroadcastChannel } from 'react-broadcast-sync';
419
+
420
+ test('should send and receive messages', () => {
421
+ const { result } = renderHook(() => useBroadcastChannel('test-channel'));
422
+
423
+ act(() => {
424
+ result.current.postMessage('test', { data: 'hello' });
425
+ });
426
+
427
+ expect(result.current.messages).toHaveLength(1);
428
+ expect(result.current.messages[0].message.data).toBe('hello');
429
+ });
430
+ ```
431
+
432
+ ### Integration Testing
433
+
434
+ ```tsx
435
+ import { render, screen } from '@testing-library/react';
436
+ import { BroadcastProvider } from 'react-broadcast-sync';
437
+
438
+ test('should render messages from provider', () => {
439
+ render(
440
+ <BroadcastProvider channelName="test-channel">
441
+ <TestComponent />
442
+ </BroadcastProvider>
443
+ );
444
+
445
+ // Your test assertions here
446
+ });
447
+ ```
448
+
449
+ ---
450
+
451
+ ## ๐ŸŒ Browser Support
452
+
453
+ Relies on [BroadcastChannel API](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel):
454
+
455
+ - โœ… Chrome 54+
456
+ - โœ… Firefox 38+
457
+ - โœ… Edge 79+
458
+ - โœ… Safari 15.4+
459
+ - โœ… Opera 41+
460
+
461
+ ---
462
+
463
+ ## ๐Ÿ› ๏ธ Coming Soon
464
+
465
+ We're actively improving `react-broadcast-sync`! Here are some features and enhancements planned for upcoming versions:
466
+
467
+ - โœ… **Unit and Integration Tests**
468
+ Ensure robust behavior and edge-case coverage.
469
+
470
+ - ๐Ÿ”„ **Automatic Channel Recovery**
471
+ Reconnect automatically if the `BroadcastChannel` gets disconnected or closed by the browser.
472
+
473
+ - ๐Ÿงผ **`clearMessagesByType()`**
474
+ Clear all messages of a specific type with a single call.
475
+
476
+ - ๐ŸŽฏ **Per-Type Callbacks**
477
+ Define message handlers for specific types with `onMessage({ type, callback })`.
478
+
479
+ - ๐Ÿงน **`clearAllSentMessages()` / `clearAllReceivedMessages()`**
480
+ Fine-grained control for clearing messages based on source.
481
+
482
+ We're committed to keeping this package lightweight, flexible, and production-ready.
483
+ Your feedback and contributions are welcome โ€” feel free to [open an issue](https://github.com/IdanShalem/react-broadcast-sync/issues)!
484
+
485
+ ---
486
+
487
+ ## ๐Ÿค Contributing
488
+
489
+ PRs and feature suggestions welcome! Open an issue or submit a pull request.
490
+
491
+ ---
492
+
493
+ ## ๐Ÿชช License
494
+
495
+ MIT ยฉ [Idan Shalem](https://github.com/IdanShalem)
@@ -0,0 +1,3 @@
1
+ export default {
2
+ extends: ['@commitlint/config-conventional'],
3
+ };
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { BroadcastMessage } from './types/types';
3
+ interface BroadcastContextValue {
4
+ messages: BroadcastMessage[];
5
+ error: string | null;
6
+ channel: BroadcastChannel | null;
7
+ }
8
+ interface BroadcastProviderProps {
9
+ channelName: string;
10
+ children: React.ReactNode;
11
+ }
12
+ export declare const BroadcastProvider: React.FC<BroadcastProviderProps>;
13
+ export declare const useBroadcastProvider: () => BroadcastContextValue;
14
+ export {};
@@ -0,0 +1,3 @@
1
+ import { BroadcastActions, BroadcastOptions } from '../types/types';
2
+ export declare const useBroadcastChannel: (channelName: string, options?: BroadcastOptions) => BroadcastActions;
3
+ export default useBroadcastChannel;
@@ -0,0 +1,2 @@
1
+ "use strict";var e=require("react"),r=require("react/jsx-runtime");const t="__INTERNAL__",s=()=>Math.random().toString(36).substr(2,9),n=(e,r)=>{const t=`${s()}-${e}-${r}`;return btoa(t).replace(/=+$/,"")},a=e=>!!e.expirationDate&&e.expirationDate<Date.now(),o=(e,r,s="")=>{const n=btoa(`react-broadcast-sync:${e}:${`${r}-${s}`}`);return`${t}:${e}:${n}`},c=e=>e.startsWith(`${t}:`),l=(e,r)=>{let t,s=null,n=null,a=null;const o=function(...o){n=o,a=this,null!==s&&clearTimeout(s),s=setTimeout((()=>{s=null,null!==n&&(t=e.apply(a,n),n=null,a=null)}),r)};return o.cancel=()=>{null!==s&&(clearTimeout(s),s=null,n=null,a=null)},o.flush=()=>(null!==s&&(clearTimeout(s),s=null,null!==n&&(t=e.apply(a,n),n=null,a=null)),t),o},i=(e,r,t)=>{if("undefined"==typeof process||!process.env||"true"!==process.env.REACT_APP_DEBUG_BROADCAST)return;const s=`[${(new Date).toISOString()}] [react-broadcast-sync]`;switch(e){case"info":console.log(s,r,t||"");break;case"warn":console.warn(s,r,t||"");break;case"error":console.error(s,r,t||"")}},u={created:e=>i("info","Channel created:",e),closed:e=>i("info","Channel closed:",e)},d={sent:e=>i("info","Message sent:",e),received:e=>i("info","Message received:",e),cleared:e=>i("info","Message cleared:",e),expired:e=>i("info","Message expired:",e),duplicate:e=>i("warn","Duplicate message ignored:",e)},p={started:()=>i("info","Cleanup started"),completed:e=>i("info","Cleanup completed:",{removedCount:e})},f=e=>i("error","Error:",e),E="CLEAR_MESSAGE",v="CLEAR_ALL_MESSAGES",h=(r,t={})=>{const{sourceName:i,cleaningInterval:h=1e3,keepLatestMessage:S=!1,registeredTypes:C=[],namespace:g="",deduplicationTTL:A=3e5,cleanupDebounceMs:m=0}=t,[M,L]=e.useState([]),[b,y]=e.useState([]),[_,R]=e.useState(null),w=e.useRef(null),x=e.useRef(new Map),D=e.useRef(i||`tab-${s()}`).current,B=e.useRef({CLEAR_MESSAGE:o(E,r,g),CLEAR_ALL_MESSAGES:o(v,r,g)}).current,$=e.useMemo((()=>`${r}-${g}`),[r,g]),G=e.useCallback((()=>{L((e=>e.filter((e=>!a(e)))))}),[]),k=e.useRef(m>0?l(G,m):Object.assign(G,{cancel:()=>{}})).current,T=e.useCallback((e=>{R(e),setTimeout((()=>R(null)),3e3)}),[]),P=e.useCallback((e=>{var r;L((r=>r.filter((r=>r.id!==e)))),y((r=>r.filter((r=>r.id!==e)))),null===(r=w.current)||void 0===r||r.postMessage({id:e,type:B.CLEAR_MESSAGE,source:D}),d.cleared(e)}),[D,B.CLEAR_MESSAGE]),j=e.useCallback((()=>{var e;L([]),y([]),null===(e=w.current)||void 0===e||e.postMessage({id:B.CLEAR_ALL_MESSAGES,type:B.CLEAR_ALL_MESSAGES,source:D})}),[D,B.CLEAR_ALL_MESSAGES]),I=e.useCallback((e=>{var r;y((r=>r.filter((r=>r.id!==e)))),null===(r=w.current)||void 0===r||r.postMessage({id:e,type:B.CLEAR_MESSAGE,source:D})}),[D,B.CLEAR_MESSAGE]),N=e.useCallback(((e,r,t={})=>{const s=w.current;if(!s){const e="Channel not available";return f(e),void T(e)}try{const a=((e,r,t,s={})=>{var a;const o=Date.now();return{id:n(t,o),type:e,message:r,timestamp:o,source:t,expirationDate:null!==(a=s.expirationDate)&&void 0!==a?a:s.expirationDuration?o+s.expirationDuration:void 0}})(e,r,D,t);s.postMessage(a),d.sent(a),y((e=>[...e,a]))}catch(e){const r="Failed to send message";f(r),T(r)}}),[D,T]),O=e.useCallback((e=>{try{const r=e.data;if(d.received(r),!(e=>Boolean(e&&"object"==typeof e&&"id"in e))(r))return;if(r.source===D)return;if(!c(r.type)&&C.length>0&&!C.includes(r.type))return;if(a(r))return void d.expired(r.id);if((e=>e&&"object"==typeof e&&"string"==typeof e.id&&"string"==typeof e.source&&"string"==typeof e.type&&c(e.type))(r)){if(r.type===B.CLEAR_MESSAGE)return void L((e=>e.filter((e=>!(e.id===r.id&&e.source===r.source)))));if(r.type===B.CLEAR_ALL_MESSAGES)return void L((e=>e.filter((e=>e.source!==r.source))))}const t=Date.now(),s=x.current.get(r.id);if(s&&t-s<A)return void d.duplicate(r.id);x.current.set(r.id,t),L((e=>S?[r]:[...e,r]))}catch(e){const r="Error processing broadcast message";f(r),T(r)}}),[D,C,S,T,B,A]);return e.useEffect((()=>{if("undefined"==typeof BroadcastChannel){const e="BroadcastChannel is not supported in this browser. Please check browser compatibility.";return f(e),void T(e)}const e=new BroadcastChannel($);return w.current=e,u.created($),e.addEventListener("message",O),()=>{e.removeEventListener("message",O),e.close(),u.closed($)}}),[$,O,T]),e.useEffect((()=>{if(h<=0)return;const e=setInterval((()=>{p.started();const e=M.length;k(),p.completed(e-M.length)}),h);return()=>{var r;clearInterval(e),m>0&&(null===(r=k.cancel)||void 0===r||r.call(k))}}),[h,k,m]),e.useEffect((()=>{const e=setInterval((()=>{const e=Date.now();for(const[r,t]of x.current.entries())e-t>=A&&x.current.delete(r)}),6e4);return()=>clearInterval(e)}),[A]),{channelName:$,messages:M,sentMessages:b,postMessage:N,clearMessage:P,clearAllMessages:j,clearSentMessage:I,error:_}},S=e.createContext(void 0);class C extends e.Component{constructor(){super(...arguments),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(e){console.error("BroadcastChannel error:",e)}render(){return this.state.hasError?r.jsx("div",{children:"Error occurred in BroadcastProvider"}):this.props.children}}exports.BroadcastProvider=({children:t,channelName:s})=>{const{messages:n,error:a}=h(s),[o,c]=e.useState(null),l=e.useRef(null);e.useEffect((()=>{if("undefined"==typeof BroadcastChannel)return void c("BroadcastChannel is not supported in this browser. Please check browser compatibility.");const e=new BroadcastChannel(s);return l.current=e,()=>{e.close()}}),[s]);const i=e.useMemo((()=>({messages:n,error:o||a,channel:l.current})),[n,o,a]);return r.jsx(C,{children:r.jsx(S.Provider,{value:i,children:t})})},exports.useBroadcastChannel=h,exports.useBroadcastProvider=()=>{const r=e.useContext(S);if(!r)throw new Error("useBroadcastProvider must be used within a BroadcastProvider");return r};
2
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":["../src/utils/messageUtils.ts","../src/utils/debug.ts","../src/hooks/useBroadcastChannel.ts","../src/BroadcastProvider.tsx"],"sourcesContent":[null,null,null,null],"names":["INTERNAL_PREFIX","generateRandomPart","Math","random","toString","substr","generateMessageId","source","timestamp","raw","btoa","replace","isMessageExpired","message","expirationDate","Date","now","getInternalMessageType","baseType","channelName","namespace","hash","isInternalType","type","startsWith","debounce","fn","wait","result","timeoutId","lastArgs","lastThis","debounced","args","this","clearTimeout","setTimeout","apply","cancel","flush","log","level","data","process","env","REACT_APP_DEBUG_BROADCAST","prefix","toISOString","console","warn","error","debug","created","closed","sent","received","cleared","messageId","expired","duplicate","started","completed","removedCount","INTERNAL_MESSAGE_TYPES","useBroadcastChannel","options","sourceName","cleaningInterval","keepLatestMessage","registeredTypes","deduplicationTTL","cleanupDebounceMs","messages","setMessages","useState","sentMessages","setSentMessages","setError","channel","useRef","receivedMessageIds","Map","current","internalTypes","CLEAR_MESSAGE","CLEAR_ALL_MESSAGES","resolvedChannelName","useMemo","performCleanup","useCallback","prev","filter","msg","debouncedCleanup","Object","assign","setErrorMessage","clearMessage","id","_a","postMessage","clearAllMessages","clearSentMessage","messageType","messageContent","channelCurrent","createMessage","content","expirationDuration","undefined","e","handleMessage","event","Boolean","isValidMessage","length","includes","isValidInternalClearMessage","receivedAt","get","set","useEffect","BroadcastChannel","bc","addEventListener","removeEventListener","close","interval","setInterval","before","clearInterval","call","entries","delete","BroadcastChannelContext","createContext","ErrorBoundary","React","Component","constructor","state","hasError","getDerivedStateFromError","componentDidCatch","render","_jsx","props","children","BroadcastProvider","hookError","value","Provider","useBroadcastProvider","context","useContext","Error"],"mappings":"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"}