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.
- package/.github/workflows/ci.yml +61 -0
- package/.husky/pre-commit +6 -0
- package/.prettierignore +3 -0
- package/.prettierrc +5 -0
- package/.releaserc.json +22 -0
- package/LICENSE +21 -0
- package/README.md +495 -0
- package/assets/react-broadcast-sync-logo.png +0 -0
- package/commitlint.config.js +3 -0
- package/dist/BroadcastProvider.d.ts +14 -0
- package/dist/hooks/useBroadcastChannel.d.ts +3 -0
- package/dist/index.cjs.js +2 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.esm.js +2 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/types/types.d.ts +66 -0
- package/dist/useBroadcastChannel.d.ts +3 -0
- package/dist/utils/debug.d.ts +18 -0
- package/dist/utils/messageUtils.d.ts +22 -0
- package/eslint.config.js +29 -0
- package/jest.config.cjs +10 -0
- package/jest.setup.ts +1 -0
- package/package.json +109 -0
- package/rollup.config.mjs +92 -0
- package/src/BroadcastProvider.tsx +80 -0
- package/src/hooks/useBroadcastChannel.ts +232 -0
- package/src/index.ts +9 -0
- package/src/tests/BroadcastProvider.test.tsx +163 -0
- package/src/tests/debug.test.ts +199 -0
- package/src/tests/messageUtils.test.ts +247 -0
- package/src/tests/useBroadcastChannel.test.tsx +504 -0
- package/src/types/types.ts +92 -0
- package/src/utils/debug.ts +47 -0
- package/src/utils/messageUtils.ts +128 -0
- package/tsconfig.json +36 -0
|
@@ -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
|
package/.prettierignore
ADDED
package/.prettierrc
ADDED
package/.releaserc.json
ADDED
|
@@ -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
|
+

|
|
8
|
+
[](https://codecov.io/gh/IdanShalem/react-broadcast-sync)
|
|
9
|
+
[](https://www.npmjs.com/package/react-broadcast-sync)
|
|
10
|
+
[](https://www.npmjs.com/package/react-broadcast-sync)
|
|
11
|
+
[](https://bundlephobia.com/result?p=react-broadcast-sync)
|
|
12
|
+
[](https://opensource.org/licenses/MIT)
|
|
13
|
+
[](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)
|
|
Binary file
|
|
@@ -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,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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"}
|