react-broadcast-sync 1.2.1 → 1.3.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/CHANGELOG.md +7 -0
- package/README.md +66 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/types/types.d.ts +8 -0
- package/dist/utils/debug.d.ts +1 -0
- package/package.json +1 -1
- package/src/hooks/useBroadcastChannel.ts +67 -29
- package/src/tests/useBroadcastChannel.test.tsx +459 -68
- package/src/types/types.ts +11 -0
- package/src/utils/debug.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.3.0](https://github.com/IdanShalem/react-broadcast-sync/compare/v1.2.1...v1.3.0) (2025-06-16)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* added getLatestMessage and closeChannel methods ([2ca0c32](https://github.com/IdanShalem/react-broadcast-sync/commit/2ca0c320c6558fd4205a8965d364d49c07609e97))
|
|
7
|
+
|
|
1
8
|
## [1.2.1](https://github.com/IdanShalem/react-broadcast-sync/compare/v1.2.0...v1.2.1) (2025-06-11)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -252,6 +252,8 @@ interface BroadcastActions {
|
|
|
252
252
|
postMessage: (type: string, content: any, options?: SendMessageOptions) => void;
|
|
253
253
|
clearReceivedMessages: (opts?: { ids?: string[]; types?: string[]; sources?: string[] }) => void;
|
|
254
254
|
clearSentMessages: (opts?: { ids?: string[]; types?: string[]; sync?: boolean }) => void;
|
|
255
|
+
getLatestMessage: (opts?: { type?: string; source?: string }) => BroadcastMessage | null;
|
|
256
|
+
closeChannel: () => void;
|
|
255
257
|
error: string | null; // Current error state
|
|
256
258
|
}
|
|
257
259
|
```
|
|
@@ -268,6 +270,8 @@ Returns an object with:
|
|
|
268
270
|
| `postMessage()` | `function` | Send a message to all tabs |
|
|
269
271
|
| `clearReceivedMessages()` | `function` | Clear received messages. No filters ⇒ clear all. With filters, a message is deleted only if it matches **every** provided filter (`ids`, `types`, `sources`). Empty arrays act as wildcards. |
|
|
270
272
|
| `clearSentMessages()` | `function` | Clear messages this tab sent (same matching rules). Pass `sync: true` to broadcast the clear to other tabs. |
|
|
273
|
+
| `getLatestMessage()` | `function` | Get the latest message matching optional filters (`type`, `source`). Returns the most recent message that matches, or `null` if none. |
|
|
274
|
+
| `closeChannel()` | `function` | Explicitly closes the broadcast channel and removes event listeners. Safe to call multiple times. |
|
|
271
275
|
| `error` | `string \| null` | Any runtime error from the channel |
|
|
272
276
|
|
|
273
277
|
#### Clearing examples
|
|
@@ -310,6 +314,68 @@ interface BroadcastMessage {
|
|
|
310
314
|
}
|
|
311
315
|
```
|
|
312
316
|
|
|
317
|
+
#### Getting the Latest Message
|
|
318
|
+
|
|
319
|
+
You can use `getLatestMessage` to retrieve the most recent message received, optionally filtered by `type` and/or `source`. If no options are provided, it returns the latest message of any kind. If no message matches, it returns `null`.
|
|
320
|
+
|
|
321
|
+
**Signature:**
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
getLatestMessage(options?: { type?: string; source?: string }): BroadcastMessage | null
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Examples:**
|
|
328
|
+
|
|
329
|
+
```tsx
|
|
330
|
+
// Get the latest message of any type/source
|
|
331
|
+
const latest = getLatestMessage();
|
|
332
|
+
|
|
333
|
+
// Get the latest message of a specific type
|
|
334
|
+
const latestAlert = getLatestMessage({ type: 'alert' });
|
|
335
|
+
|
|
336
|
+
// Get the latest message from a specific source
|
|
337
|
+
const latestFromTab = getLatestMessage({ source: 'tab-123' });
|
|
338
|
+
|
|
339
|
+
// Get the latest message of a specific type from a specific source
|
|
340
|
+
const latestInfoFromTab = getLatestMessage({ type: 'info', source: 'tab-123' });
|
|
341
|
+
|
|
342
|
+
// Check if there are any messages of a type
|
|
343
|
+
if (getLatestMessage({ type: 'notification' })) {
|
|
344
|
+
// ...
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**Behavior:**
|
|
349
|
+
|
|
350
|
+
- If no messages are present, returns `null`.
|
|
351
|
+
- If no message matches the filter, returns `null`.
|
|
352
|
+
- If multiple messages match, returns the most recently received one.
|
|
353
|
+
|
|
354
|
+
#### Closing the Channel Explicitly
|
|
355
|
+
|
|
356
|
+
You can use `closeChannel` to explicitly close the underlying BroadcastChannel and remove all event listeners. This is useful if you want to clean up resources before the component unmounts, or to stop all cross-tab communication on demand. Note that the channel will automatically close and all event listeners will be removed when the component unmounts, so this method is mainly useful for manual cleanup.
|
|
357
|
+
|
|
358
|
+
**Signature:**
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
closeChannel(): void
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Example:**
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
const { closeChannel } = useBroadcastChannel('my-channel');
|
|
368
|
+
|
|
369
|
+
// ... later, when you want to stop all communication:
|
|
370
|
+
closeChannel();
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Notes:**
|
|
374
|
+
|
|
375
|
+
- After calling `closeChannel`, the channel is closed and will not send or receive any more messages.
|
|
376
|
+
- It is safe to call `closeChannel` multiple times (idempotent).
|
|
377
|
+
- You do not need to call this for normal React unmounting; the hook will clean up automatically. Use it for explicit/manual cleanup only.
|
|
378
|
+
|
|
313
379
|
---
|
|
314
380
|
|
|
315
381
|
## Best Practices
|
package/dist/index.cjs.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var e=require("react"),
|
|
1
|
+
"use strict";var e=require("react"),t=require("react/jsx-runtime");const r="__INTERNAL__",s=()=>Math.random().toString(36).substr(2,9),n=(e,t)=>{const r=`${s()}-${e}-${t}`;return btoa(r).replace(/=+$/,"")},o=e=>!!e.expirationDate&&e.expirationDate<Date.now(),a=(e,t,r,s={})=>{var o;const a=Date.now();return{id:n(r,a),type:e,message:t,timestamp:a,source:r,expirationDate:null!==(o=s.expirationDate)&&void 0!==o?o:s.expirationDuration?a+s.expirationDuration:void 0}},c=(e,t,s="")=>{const n=btoa(`react-broadcast-sync:${e}:${`${t}-${s}`}`);return`${r}:${e}:${n}`},l=e=>e&&"object"==typeof e&&"string"==typeof e.type&&e.type.startsWith(`${r}:`)&&"string"==typeof e.source,i=(e,t,r)=>{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,t,r||"");break;case"warn":console.warn(s,t,r||"");break;case"error":console.error(s,t,r||"")}},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),allSentCleared:()=>i("info","All sent messages cleared"),allReceivedCleared:()=>i("info","All received messages cleared"),ignored:e=>i("info","Message ignored due to type filter:",e)},p={started:()=>i("info","Cleanup started"),completed:e=>i("info","Cleanup completed:",{removedCount:e})},f=e=>i("error","Error:",e),g="CLEAR_SENT_MESSAGES",h=(t,r={})=>{const{sourceName:n,cleaningInterval:i=1e3,keepLatestMessage:h=!1,registeredTypes:v=[],namespace:m="",deduplicationTTL:y=3e5,cleanupDebounceMs:C=0}=r,[E,b]=e.useState([]),[S,M]=e.useState([]),[w,x]=e.useState(null),B=e.useRef(null),D=e.useRef(new Map),$=e.useMemo((()=>n||`tab-${s()}`),[n]),A=e.useMemo((()=>({CLEAR_SENT_MESSAGES:c(g,t,m)})),[t,m]),T=e.useMemo((()=>`${t}-${m}`),[t,m]),_=e.useMemo((()=>v),[JSON.stringify(v)]),k=e.useCallback((()=>{b((e=>e.filter((e=>!o(e)))))}),[]),R=e.useRef(C>0?((e,t)=>{let r,s=null,n=null;const o=(...o)=>{n=o,null!==s&&clearTimeout(s),s=setTimeout((()=>{s=null,null!==n&&(r=e(...n),n=null)}),t)};return o.cancel=()=>{null!==s&&(clearTimeout(s),s=null,n=null)},o.flush=()=>(null!==s&&(clearTimeout(s),s=null,null!==n&&(r=e(...n),n=null)),r),o})(k,C):Object.assign(k,{cancel:()=>{}})).current,P=e.useCallback((e=>{x(e),setTimeout((()=>x(null)),3e3)}),[]),L=e.useCallback(((e,t,r={})=>{const s=B.current;if(!s){const e="BroadcastChannel is not supported in this browser. Please check browser compatibility.";return f(e),void P(e)}try{const n=a(e,t,$,r);s.postMessage(n),d.sent(n),M((e=>[...e,n]))}catch(e){const t="Failed to send message";f(t),P(t)}}),[$,P]),N=e.useCallback(((e={})=>{const t=Boolean(e.ids&&e.ids.length||e.types&&e.types.length||e.sources&&e.sources.length);b((r=>t?r.filter((t=>!(e.ids&&e.ids.includes(t.id)||e.types&&e.types.includes(t.type)||e.sources&&e.sources.includes(t.source)))):[]))}),[]),j=e.useCallback(((e={})=>{var t,r,s;const{ids:n=[],types:o=[],sync:c=!1}=null!=e?e:{};M((e=>n.length>0||o.length>0?e.filter((e=>{if(e.source!==$)return!0;const t=0===n.length||n.includes(e.id),r=0===o.length||o.includes(e.type);return!(t&&r)})):[])),c&&(null===(t=B.current)||void 0===t||t.postMessage(a(A.CLEAR_SENT_MESSAGES,{ids:null!==(r=e.ids)&&void 0!==r?r:[],types:null!==(s=e.types)&&void 0!==s?s:[]},$)))}),[]),I=e.useCallback(((e={})=>{const{source:t,type:r}=e;for(let e=E.length-1;e>=0;e--){const s=E[e],n=!t||s.source===t,o=!r||s.type===r;if(n&&o)return s}return null}),[E]),G=e.useCallback((e=>{try{const t=e.data;if(d.received(t),!(e=>Boolean(e&&"object"==typeof e&&"id"in e))(t))return;if(t.source===$)return;if(l(t)&&t.type===A.CLEAR_SENT_MESSAGES){const{ids:e=[],types:r=[]}=t.message||{};return void b((s=>[...s.filter((s=>{if(s.source!==t.source)return!0;const n=0===e.length||e.includes(s.id),o=0===r.length||r.includes(s.type);return!(n&&o)}))]))}if(_.length>0&&!_.includes(t.type))return void d.ignored(t.type);if(o(t))return void d.expired(t.id);const r=Date.now(),s=D.current.get(t.id);if(s&&r-s<y)return void d.duplicate(t.id);D.current.set(t.id,r),b((e=>h?[t]:[...e,t]))}catch(e){const t="Error processing broadcast message";f(t),P(t)}}),[$,_,h,P,A,y]),O=e.useCallback((()=>{const e=B.current;e&&"function"==typeof e.close&&(e.removeEventListener("message",G),e.close(),u.closed(T),B.current=null)}),[G,T]);return e.useEffect((()=>{if("undefined"==typeof BroadcastChannel){const e="BroadcastChannel is not supported in this browser. Please check browser compatibility.";return f(e),void P(e)}const e=new BroadcastChannel(T);return B.current=e,u.created(T),e.addEventListener("message",G),()=>{O()}}),[T,G,P]),e.useEffect((()=>{if(i<=0)return;const e=setInterval((()=>{p.started();const e=E.length;R(),p.completed(e-E.length)}),i);return()=>{var t;clearInterval(e),C>0&&(null===(t=R.cancel)||void 0===t||t.call(R))}}),[i,R,C]),e.useEffect((()=>{const e=setInterval((()=>{const e=Date.now();for(const[t,r]of D.current.entries())e-r>=y&&D.current.delete(t)}),6e4);return()=>clearInterval(e)}),[y]),{channelName:T,messages:E,sentMessages:S,postMessage:L,clearReceivedMessages:N,clearSentMessages:j,getLatestMessage:I,error:w,closeChannel:O}},v=e.createContext(void 0);class m 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?t.jsx("div",{children:"Error occurred in BroadcastProvider"}):this.props.children}}exports.BroadcastProvider=({children:r,channelName:s})=>{const{messages:n,error:o}=h(s),[a,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:a||o,channel:l.current})),[n,a,o]);return t.jsx(m,{children:t.jsx(v.Provider,{value:i,children:r})})},exports.useBroadcastChannel=h,exports.useBroadcastProvider=()=>{const t=e.useContext(v);if(!t)throw new Error("useBroadcastProvider must be used within a BroadcastProvider");return t};
|
|
2
2
|
//# sourceMappingURL=index.cjs.js.map
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -1 +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","createMessage","type","content","options","id","_a","expirationDuration","undefined","getInternalMessageType","baseType","channelName","namespace","hash","
|
|
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","createMessage","type","content","options","id","_a","expirationDuration","undefined","getInternalMessageType","baseType","channelName","namespace","hash","isValidInternalClearMessage","startsWith","log","level","data","process","env","REACT_APP_DEBUG_BROADCAST","prefix","toISOString","console","warn","error","debug","created","name","closed","sent","received","cleared","messageId","expired","duplicate","allSentCleared","allReceivedCleared","ignored","messageType","started","completed","removedCount","INTERNAL_MESSAGE_TYPES","useBroadcastChannel","sourceName","cleaningInterval","keepLatestMessage","registeredTypes","deduplicationTTL","cleanupDebounceMs","messages","setMessages","useState","sentMessages","setSentMessages","setError","channel","useRef","receivedMessageIds","Map","useMemo","internalTypes","CLEAR_SENT_MESSAGES","resolvedChannelName","stableRegisteredTypes","JSON","stringify","performCleanup","useCallback","prev","filter","msg","debouncedCleanup","debounce","fn","wait","result","timeoutId","lastArgs","debounced","args","clearTimeout","setTimeout","cancel","flush","Object","assign","current","setErrorMessage","postMessage","messageContent","channelCurrent","e","clearReceivedMessages","hasFilters","Boolean","ids","length","types","sources","includes","clearSentMessages","sync","idMatches","typeMatches","_b","_c","getLatestMessage","i","sourceMatches","handleMessage","event","isValidMessage","receivedAt","get","set","closeChannel","bc","close","removeEventListener","useEffect","BroadcastChannel","addEventListener","interval","setInterval","before","clearInterval","call","entries","delete","BroadcastChannelContext","createContext","ErrorBoundary","React","Component","constructor","this","state","hasError","getDerivedStateFromError","componentDidCatch","render","_jsx","props","children","BroadcastProvider","hookError","value","Provider","useBroadcastProvider","context","useContext","Error"],"mappings":"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"}
|
package/dist/index.esm.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import e,{useState as r,useRef as t,useMemo as n,useCallback as s,useEffect as o,createContext as a,useContext as c}from"react";import{jsx as
|
|
1
|
+
import e,{useState as r,useRef as t,useMemo as n,useCallback as s,useEffect as o,createContext as a,useContext as c}from"react";import{jsx as i}from"react/jsx-runtime";const l="__INTERNAL__",u=()=>Math.random().toString(36).substr(2,9),d=(e,r)=>{const t=`${u()}-${e}-${r}`;return btoa(t).replace(/=+$/,"")},p=e=>!!e.expirationDate&&e.expirationDate<Date.now(),g=(e,r,t,n={})=>{var s;const o=Date.now();return{id:d(t,o),type:e,message:r,timestamp:o,source:t,expirationDate:null!==(s=n.expirationDate)&&void 0!==s?s:n.expirationDuration?o+n.expirationDuration:void 0}},h=(e,r,t="")=>{const n=btoa(`react-broadcast-sync:${e}:${`${r}-${t}`}`);return`${l}:${e}:${n}`},f=e=>e&&"object"==typeof e&&"string"==typeof e.type&&e.type.startsWith(`${l}:`)&&"string"==typeof e.source,m=(e,r,t)=>{if("undefined"==typeof process||!process.env||"true"!==process.env.REACT_APP_DEBUG_BROADCAST)return;const n=`[${(new Date).toISOString()}] [react-broadcast-sync]`;switch(e){case"info":console.log(n,r,t||"");break;case"warn":console.warn(n,r,t||"");break;case"error":console.error(n,r,t||"")}},v={created:e=>m("info","Channel created:",e),closed:e=>m("info","Channel closed:",e)},y={sent:e=>m("info","Message sent:",e),received:e=>m("info","Message received:",e),cleared:e=>m("info","Message cleared:",e),expired:e=>m("info","Message expired:",e),duplicate:e=>m("warn","Duplicate message ignored:",e),allSentCleared:()=>m("info","All sent messages cleared"),allReceivedCleared:()=>m("info","All received messages cleared"),ignored:e=>m("info","Message ignored due to type filter:",e)},E={started:()=>m("info","Cleanup started"),completed:e=>m("info","Cleanup completed:",{removedCount:e})},S=e=>m("error","Error:",e),C="CLEAR_SENT_MESSAGES",b=(e,a={})=>{const{sourceName:c,cleaningInterval:i=1e3,keepLatestMessage:l=!1,registeredTypes:d=[],namespace:m="",deduplicationTTL:b=3e5,cleanupDebounceMs:w=0}=a,[M,D]=r([]),[$,A]=r([]),[B,T]=r(null),_=t(null),x=t(new Map),L=n((()=>c||`tab-${u()}`),[c]),N=n((()=>({CLEAR_SENT_MESSAGES:h(C,e,m)})),[e,m]),P=n((()=>`${e}-${m}`),[e,m]),R=n((()=>d),[JSON.stringify(d)]),I=s((()=>{D((e=>e.filter((e=>!p(e)))))}),[]),k=t(w>0?((e,r)=>{let t,n=null,s=null;const o=(...o)=>{s=o,null!==n&&clearTimeout(n),n=setTimeout((()=>{n=null,null!==s&&(t=e(...s),s=null)}),r)};return o.cancel=()=>{null!==n&&(clearTimeout(n),n=null,s=null)},o.flush=()=>(null!==n&&(clearTimeout(n),n=null,null!==s&&(t=e(...s),s=null)),t),o})(I,w):Object.assign(I,{cancel:()=>{}})).current,G=s((e=>{T(e),setTimeout((()=>T(null)),3e3)}),[]),j=s(((e,r,t={})=>{const n=_.current;if(!n){const e="BroadcastChannel is not supported in this browser. Please check browser compatibility.";return S(e),void G(e)}try{const s=g(e,r,L,t);n.postMessage(s),y.sent(s),A((e=>[...e,s]))}catch(e){const r="Failed to send message";S(r),G(r)}}),[L,G]),O=s(((e={})=>{const r=Boolean(e.ids&&e.ids.length||e.types&&e.types.length||e.sources&&e.sources.length);D((t=>r?t.filter((r=>!(e.ids&&e.ids.includes(r.id)||e.types&&e.types.includes(r.type)||e.sources&&e.sources.includes(r.source)))):[]))}),[]),F=s(((e={})=>{var r,t,n;const{ids:s=[],types:o=[],sync:a=!1}=null!=e?e:{};A((e=>s.length>0||o.length>0?e.filter((e=>{if(e.source!==L)return!0;const r=0===s.length||s.includes(e.id),t=0===o.length||o.includes(e.type);return!(r&&t)})):[])),a&&(null===(r=_.current)||void 0===r||r.postMessage(g(N.CLEAR_SENT_MESSAGES,{ids:null!==(t=e.ids)&&void 0!==t?t:[],types:null!==(n=e.types)&&void 0!==n?n:[]},L)))}),[]),J=s(((e={})=>{const{source:r,type:t}=e;for(let e=M.length-1;e>=0;e--){const n=M[e],s=!r||n.source===r,o=!t||n.type===t;if(s&&o)return n}return null}),[M]),U=s((e=>{try{const r=e.data;if(y.received(r),!(e=>Boolean(e&&"object"==typeof e&&"id"in e))(r))return;if(r.source===L)return;if(f(r)&&r.type===N.CLEAR_SENT_MESSAGES){const{ids:e=[],types:t=[]}=r.message||{};return void D((n=>[...n.filter((n=>{if(n.source!==r.source)return!0;const s=0===e.length||e.includes(n.id),o=0===t.length||t.includes(n.type);return!(s&&o)}))]))}if(R.length>0&&!R.includes(r.type))return void y.ignored(r.type);if(p(r))return void y.expired(r.id);const t=Date.now(),n=x.current.get(r.id);if(n&&t-n<b)return void y.duplicate(r.id);x.current.set(r.id,t),D((e=>l?[r]:[...e,r]))}catch(e){const r="Error processing broadcast message";S(r),G(r)}}),[L,R,l,G,N,b]),W=s((()=>{const e=_.current;e&&"function"==typeof e.close&&(e.removeEventListener("message",U),e.close(),v.closed(P),_.current=null)}),[U,P]);return o((()=>{if("undefined"==typeof BroadcastChannel){const e="BroadcastChannel is not supported in this browser. Please check browser compatibility.";return S(e),void G(e)}const e=new BroadcastChannel(P);return _.current=e,v.created(P),e.addEventListener("message",U),()=>{W()}}),[P,U,G]),o((()=>{if(i<=0)return;const e=setInterval((()=>{E.started();const e=M.length;k(),E.completed(e-M.length)}),i);return()=>{var r;clearInterval(e),w>0&&(null===(r=k.cancel)||void 0===r||r.call(k))}}),[i,k,w]),o((()=>{const e=setInterval((()=>{const e=Date.now();for(const[r,t]of x.current.entries())e-t>=b&&x.current.delete(r)}),6e4);return()=>clearInterval(e)}),[b]),{channelName:P,messages:M,sentMessages:$,postMessage:j,clearReceivedMessages:O,clearSentMessages:F,getLatestMessage:J,error:B,closeChannel:W}},w=a(void 0);class M 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?i("div",{children:"Error occurred in BroadcastProvider"}):this.props.children}}const D=({children:r,channelName:s})=>{const{messages:a,error:c}=b(s),[l,u]=e.useState(null),d=t(null);o((()=>{if("undefined"==typeof BroadcastChannel)return void u("BroadcastChannel is not supported in this browser. Please check browser compatibility.");const e=new BroadcastChannel(s);return d.current=e,()=>{e.close()}}),[s]);const p=n((()=>({messages:a,error:l||c,channel:d.current})),[a,l,c]);return i(M,{children:i(w.Provider,{value:p,children:r})})},$=()=>{const e=c(w);if(!e)throw new Error("useBroadcastProvider must be used within a BroadcastProvider");return e};export{D as BroadcastProvider,b as useBroadcastChannel,$ as useBroadcastProvider};
|
|
2
2
|
//# sourceMappingURL=index.esm.js.map
|
package/dist/index.esm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.esm.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","createMessage","type","content","options","id","_a","expirationDuration","undefined","getInternalMessageType","baseType","channelName","namespace","hash","
|
|
1
|
+
{"version":3,"file":"index.esm.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","createMessage","type","content","options","id","_a","expirationDuration","undefined","getInternalMessageType","baseType","channelName","namespace","hash","isValidInternalClearMessage","startsWith","log","level","data","process","env","REACT_APP_DEBUG_BROADCAST","prefix","toISOString","console","warn","error","debug","created","name","closed","sent","received","cleared","messageId","expired","duplicate","allSentCleared","allReceivedCleared","ignored","messageType","started","completed","removedCount","INTERNAL_MESSAGE_TYPES","useBroadcastChannel","sourceName","cleaningInterval","keepLatestMessage","registeredTypes","deduplicationTTL","cleanupDebounceMs","messages","setMessages","useState","sentMessages","setSentMessages","setError","channel","useRef","receivedMessageIds","Map","useMemo","internalTypes","CLEAR_SENT_MESSAGES","resolvedChannelName","stableRegisteredTypes","JSON","stringify","performCleanup","useCallback","prev","filter","msg","debouncedCleanup","debounce","fn","wait","result","timeoutId","lastArgs","debounced","args","clearTimeout","setTimeout","cancel","flush","Object","assign","current","setErrorMessage","postMessage","messageContent","channelCurrent","e","clearReceivedMessages","hasFilters","Boolean","ids","length","types","sources","includes","clearSentMessages","sync","idMatches","typeMatches","_b","_c","getLatestMessage","i","sourceMatches","handleMessage","event","isValidMessage","receivedAt","get","set","closeChannel","bc","close","removeEventListener","useEffect","BroadcastChannel","addEventListener","interval","setInterval","before","clearInterval","call","entries","delete","BroadcastChannelContext","createContext","ErrorBoundary","React","Component","constructor","this","state","hasError","getDerivedStateFromError","componentDidCatch","render","_jsx","props","children","BroadcastProvider","hookError","value","Provider","useBroadcastProvider","context","useContext","Error"],"mappings":"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"}
|
package/dist/types/types.d.ts
CHANGED
|
@@ -53,6 +53,10 @@ export interface ClearReceivedMessagesOptions extends ClearOptions {
|
|
|
53
53
|
export interface ClearSentMessagesOptions extends ClearOptions {
|
|
54
54
|
sync?: boolean;
|
|
55
55
|
}
|
|
56
|
+
export interface GetLatestMessageOptions {
|
|
57
|
+
source?: string;
|
|
58
|
+
type?: string;
|
|
59
|
+
}
|
|
56
60
|
/**
|
|
57
61
|
* Actions and state provided by the broadcast channel hook
|
|
58
62
|
*/
|
|
@@ -69,6 +73,10 @@ export interface BroadcastActions {
|
|
|
69
73
|
clearReceivedMessages: (options?: ClearReceivedMessagesOptions) => void;
|
|
70
74
|
/** Function to clear sent messages */
|
|
71
75
|
clearSentMessages: (options?: ClearSentMessagesOptions) => void;
|
|
76
|
+
/** Function to get the latest message by source and type */
|
|
77
|
+
getLatestMessage: (options?: GetLatestMessageOptions) => BroadcastMessage | null;
|
|
78
|
+
/** Explicitly close the broadcast channel and remove listeners */
|
|
79
|
+
closeChannel: () => void;
|
|
72
80
|
/** Current error state */
|
|
73
81
|
error: string | null;
|
|
74
82
|
}
|
package/dist/utils/debug.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
ClearMessage,
|
|
7
7
|
ClearReceivedMessagesOptions,
|
|
8
8
|
ClearSentMessagesOptions,
|
|
9
|
+
GetLatestMessageOptions,
|
|
9
10
|
SendMessageOptions,
|
|
10
11
|
} from '../types/types';
|
|
11
12
|
import {
|
|
@@ -14,7 +15,6 @@ import {
|
|
|
14
15
|
isMessageExpired,
|
|
15
16
|
createMessage,
|
|
16
17
|
getInternalMessageType,
|
|
17
|
-
isInternalType,
|
|
18
18
|
isValidInternalClearMessage,
|
|
19
19
|
debounce,
|
|
20
20
|
} from '../utils/messageUtils';
|
|
@@ -46,19 +46,24 @@ export const useBroadcastChannel = (
|
|
|
46
46
|
// Refs
|
|
47
47
|
const channel = useRef<BroadcastChannel | null>(null);
|
|
48
48
|
const receivedMessageIds = useRef(new Map<string, number>());
|
|
49
|
-
const source =
|
|
50
|
-
const internalTypes =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
const source = useMemo(() => sourceName || generateSourceName(), [sourceName]);
|
|
50
|
+
const internalTypes = useMemo(
|
|
51
|
+
() => ({
|
|
52
|
+
CLEAR_SENT_MESSAGES: getInternalMessageType(
|
|
53
|
+
INTERNAL_MESSAGE_TYPES.CLEAR_SENT_MESSAGES,
|
|
54
|
+
channelName,
|
|
55
|
+
namespace
|
|
56
|
+
),
|
|
57
|
+
}),
|
|
58
|
+
[channelName, namespace]
|
|
59
|
+
);
|
|
57
60
|
|
|
58
61
|
const resolvedChannelName = useMemo(() => {
|
|
59
62
|
return `${channelName}-${namespace}`;
|
|
60
63
|
}, [channelName, namespace]);
|
|
61
64
|
|
|
65
|
+
const stableRegisteredTypes = useMemo(() => registeredTypes, [JSON.stringify(registeredTypes)]);
|
|
66
|
+
|
|
62
67
|
const performCleanup = useCallback(() => {
|
|
63
68
|
setMessages(prev => prev.filter(msg => !isMessageExpired(msg)));
|
|
64
69
|
}, []);
|
|
@@ -81,7 +86,8 @@ export const useBroadcastChannel = (
|
|
|
81
86
|
(messageType: string, messageContent: any, options: SendMessageOptions = {}) => {
|
|
82
87
|
const channelCurrent = channel.current;
|
|
83
88
|
if (!channelCurrent) {
|
|
84
|
-
const error =
|
|
89
|
+
const error =
|
|
90
|
+
'BroadcastChannel is not supported in this browser. Please check browser compatibility.';
|
|
85
91
|
debug.error(error);
|
|
86
92
|
setErrorMessage(error);
|
|
87
93
|
return;
|
|
@@ -150,6 +156,24 @@ export const useBroadcastChannel = (
|
|
|
150
156
|
}
|
|
151
157
|
}, []);
|
|
152
158
|
|
|
159
|
+
const getLatestMessage = useCallback(
|
|
160
|
+
(options: GetLatestMessageOptions = {}) => {
|
|
161
|
+
const { source, type } = options;
|
|
162
|
+
|
|
163
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
164
|
+
const msg = messages[i];
|
|
165
|
+
const sourceMatches = !source || msg.source === source;
|
|
166
|
+
const typeMatches = !type || msg.type === type;
|
|
167
|
+
if (sourceMatches && typeMatches) {
|
|
168
|
+
return msg;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return null;
|
|
173
|
+
},
|
|
174
|
+
[messages]
|
|
175
|
+
);
|
|
176
|
+
|
|
153
177
|
const handleMessage = useCallback(
|
|
154
178
|
(event: MessageEvent) => {
|
|
155
179
|
try {
|
|
@@ -158,23 +182,12 @@ export const useBroadcastChannel = (
|
|
|
158
182
|
|
|
159
183
|
if (!isValidMessage(message)) return;
|
|
160
184
|
if (message.source === source) return;
|
|
161
|
-
if (
|
|
162
|
-
!isInternalType(message.type) &&
|
|
163
|
-
registeredTypes.length > 0 &&
|
|
164
|
-
!registeredTypes.includes(message.type)
|
|
165
|
-
)
|
|
166
|
-
return;
|
|
167
|
-
if (isMessageExpired(message)) {
|
|
168
|
-
debug.message.expired(message.id);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
185
|
if (isValidInternalClearMessage(message)) {
|
|
173
186
|
if (message.type === internalTypes.CLEAR_SENT_MESSAGES) {
|
|
174
187
|
const { ids = [], types = [] } = (message as any).message || {};
|
|
175
188
|
|
|
176
|
-
setMessages(prev =>
|
|
177
|
-
prev.filter(msg => {
|
|
189
|
+
setMessages(prev => [
|
|
190
|
+
...prev.filter(msg => {
|
|
178
191
|
// Only consider messages from the same sender
|
|
179
192
|
if (msg.source !== message.source) return true;
|
|
180
193
|
|
|
@@ -186,13 +199,21 @@ export const useBroadcastChannel = (
|
|
|
186
199
|
|
|
187
200
|
// Remove when BOTH criteria match (wildcards included)
|
|
188
201
|
return !(idMatches && typeMatches);
|
|
189
|
-
})
|
|
190
|
-
);
|
|
202
|
+
}),
|
|
203
|
+
]);
|
|
191
204
|
|
|
192
205
|
// After handling we don't want to process this internal control packet further
|
|
193
206
|
return;
|
|
194
207
|
}
|
|
195
208
|
}
|
|
209
|
+
if (stableRegisteredTypes.length > 0 && !stableRegisteredTypes.includes(message.type)) {
|
|
210
|
+
debug.message.ignored(message.type);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (isMessageExpired(message)) {
|
|
214
|
+
debug.message.expired(message.id);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
196
217
|
|
|
197
218
|
const now = Date.now();
|
|
198
219
|
const receivedAt = receivedMessageIds.current.get(message.id);
|
|
@@ -209,9 +230,26 @@ export const useBroadcastChannel = (
|
|
|
209
230
|
setErrorMessage(error);
|
|
210
231
|
}
|
|
211
232
|
},
|
|
212
|
-
[
|
|
233
|
+
[
|
|
234
|
+
source,
|
|
235
|
+
stableRegisteredTypes,
|
|
236
|
+
keepLatestMessage,
|
|
237
|
+
setErrorMessage,
|
|
238
|
+
internalTypes,
|
|
239
|
+
deduplicationTTL,
|
|
240
|
+
]
|
|
213
241
|
);
|
|
214
242
|
|
|
243
|
+
const closeChannel = useCallback(() => {
|
|
244
|
+
const bc = channel.current;
|
|
245
|
+
if (bc && typeof bc.close === 'function') {
|
|
246
|
+
bc.removeEventListener('message', handleMessage);
|
|
247
|
+
bc.close();
|
|
248
|
+
debug.channel.closed(resolvedChannelName);
|
|
249
|
+
channel.current = null;
|
|
250
|
+
}
|
|
251
|
+
}, [handleMessage, resolvedChannelName]);
|
|
252
|
+
|
|
215
253
|
useEffect(() => {
|
|
216
254
|
if (typeof BroadcastChannel === 'undefined') {
|
|
217
255
|
const error =
|
|
@@ -227,9 +265,7 @@ export const useBroadcastChannel = (
|
|
|
227
265
|
|
|
228
266
|
bc.addEventListener('message', handleMessage);
|
|
229
267
|
return () => {
|
|
230
|
-
|
|
231
|
-
bc.close();
|
|
232
|
-
debug.channel.closed(resolvedChannelName);
|
|
268
|
+
closeChannel();
|
|
233
269
|
};
|
|
234
270
|
}, [resolvedChannelName, handleMessage, setErrorMessage]);
|
|
235
271
|
|
|
@@ -272,7 +308,9 @@ export const useBroadcastChannel = (
|
|
|
272
308
|
postMessage,
|
|
273
309
|
clearReceivedMessages,
|
|
274
310
|
clearSentMessages,
|
|
311
|
+
getLatestMessage,
|
|
275
312
|
error,
|
|
313
|
+
closeChannel,
|
|
276
314
|
};
|
|
277
315
|
};
|
|
278
316
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderHook, act } from '@testing-library/react';
|
|
1
|
+
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
2
2
|
import { useBroadcastChannel } from '../hooks/useBroadcastChannel';
|
|
3
3
|
import { getInternalMessageType } from '../utils/messageUtils';
|
|
4
4
|
|
|
@@ -6,15 +6,30 @@ let mockChannels: any[] = [];
|
|
|
6
6
|
|
|
7
7
|
class MockBroadcastChannel {
|
|
8
8
|
name: string;
|
|
9
|
-
|
|
9
|
+
_onmessage: ((event: MessageEvent) => void) | null = null;
|
|
10
|
+
closed = false;
|
|
10
11
|
postMessage = jest.fn();
|
|
11
|
-
close = jest.fn()
|
|
12
|
+
close = jest.fn(() => {
|
|
13
|
+
this.closed = true;
|
|
14
|
+
this._onmessage = null;
|
|
15
|
+
});
|
|
12
16
|
addEventListener = jest.fn((event: string, callback: (event: MessageEvent) => void) => {
|
|
13
17
|
if (event === 'message') {
|
|
14
|
-
this.
|
|
18
|
+
this._onmessage = callback;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
removeEventListener = jest.fn((event: string, callback: (event: MessageEvent) => void) => {
|
|
22
|
+
if (event === 'message' && this._onmessage === callback) {
|
|
23
|
+
this._onmessage = null;
|
|
15
24
|
}
|
|
16
25
|
});
|
|
17
|
-
|
|
26
|
+
|
|
27
|
+
get onmessage() {
|
|
28
|
+
return this._onmessage;
|
|
29
|
+
}
|
|
30
|
+
set onmessage(fn) {
|
|
31
|
+
this._onmessage = fn;
|
|
32
|
+
}
|
|
18
33
|
|
|
19
34
|
constructor(name: string) {
|
|
20
35
|
this.name = name;
|
|
@@ -22,9 +37,8 @@ class MockBroadcastChannel {
|
|
|
22
37
|
}
|
|
23
38
|
|
|
24
39
|
simulateMessage(data: any) {
|
|
25
|
-
if (this.
|
|
26
|
-
|
|
27
|
-
}
|
|
40
|
+
if (this.closed || !this._onmessage) return;
|
|
41
|
+
this._onmessage({ data } as MessageEvent);
|
|
28
42
|
}
|
|
29
43
|
}
|
|
30
44
|
|
|
@@ -65,22 +79,17 @@ describe('useBroadcastChannel', () => {
|
|
|
65
79
|
await waitForChannel();
|
|
66
80
|
const channel = mockChannels[0];
|
|
67
81
|
|
|
68
|
-
act(() => {
|
|
82
|
+
await act(async () => {
|
|
69
83
|
result.current.postMessage('greeting', { text: 'hi' });
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
timestamp: Date.now(),
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
act(() => {
|
|
84
|
+
const incomingMessage = {
|
|
85
|
+
id: 'external-1',
|
|
86
|
+
type: 'greeting',
|
|
87
|
+
message: { text: 'hi' },
|
|
88
|
+
source: 'another-tab',
|
|
89
|
+
timestamp: Date.now(),
|
|
90
|
+
};
|
|
81
91
|
channel.simulateMessage(incomingMessage);
|
|
82
92
|
});
|
|
83
|
-
|
|
84
93
|
expect(result.current.messages.length).toBe(1);
|
|
85
94
|
expect(result.current.messages[0].message).toEqual({ text: 'hi' });
|
|
86
95
|
});
|
|
@@ -265,30 +274,38 @@ describe('useBroadcastChannel', () => {
|
|
|
265
274
|
|
|
266
275
|
it('handles internal clear message (single id) from other source', async () => {
|
|
267
276
|
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
277
|
+
|
|
268
278
|
await waitForChannel();
|
|
279
|
+
|
|
269
280
|
const channel = mockChannels[0];
|
|
281
|
+
const externalSource = 'external-source';
|
|
282
|
+
|
|
283
|
+
const msg = {
|
|
284
|
+
id: 'to-clear',
|
|
285
|
+
type: 'test',
|
|
286
|
+
message: 'should be removed',
|
|
287
|
+
timestamp: Date.now(),
|
|
288
|
+
source: externalSource,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const internalClear = {
|
|
292
|
+
id: 'internal-id',
|
|
293
|
+
type: getInternalMessageType('CLEAR_SENT_MESSAGES', 'test-channel'),
|
|
294
|
+
message: { ids: ['to-clear'], types: [] },
|
|
295
|
+
timestamp: Date.now(),
|
|
296
|
+
source: externalSource,
|
|
297
|
+
};
|
|
270
298
|
|
|
271
299
|
act(() => {
|
|
272
|
-
channel.simulateMessage(
|
|
273
|
-
id: 'to-clear',
|
|
274
|
-
type: 'test',
|
|
275
|
-
message: 'will be cleared',
|
|
276
|
-
source: 'another-tab',
|
|
277
|
-
timestamp: Date.now(),
|
|
278
|
-
});
|
|
300
|
+
channel.simulateMessage(msg);
|
|
279
301
|
});
|
|
280
302
|
|
|
303
|
+
// await waitFor(() => {
|
|
281
304
|
expect(result.current.messages.length).toBe(1);
|
|
305
|
+
// });
|
|
282
306
|
|
|
283
|
-
const internalType = getInternalMessageType('CLEAR_SENT_MESSAGES', 'test-channel');
|
|
284
307
|
act(() => {
|
|
285
|
-
channel.simulateMessage(
|
|
286
|
-
id: 'internal-1',
|
|
287
|
-
type: internalType,
|
|
288
|
-
message: { ids: ['to-clear'], types: [] },
|
|
289
|
-
source: 'another-tab',
|
|
290
|
-
timestamp: Date.now(),
|
|
291
|
-
});
|
|
308
|
+
channel.simulateMessage(internalClear);
|
|
292
309
|
});
|
|
293
310
|
|
|
294
311
|
expect(result.current.messages.length).toBe(0);
|
|
@@ -296,40 +313,58 @@ describe('useBroadcastChannel', () => {
|
|
|
296
313
|
|
|
297
314
|
it('handles internal clear all messages (wildcard) from other source', async () => {
|
|
298
315
|
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
316
|
+
|
|
299
317
|
await waitForChannel();
|
|
318
|
+
|
|
300
319
|
const channel = mockChannels[0];
|
|
320
|
+
const externalSource = 'external-source';
|
|
301
321
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
322
|
+
const msg1 = {
|
|
323
|
+
id: 'msg1',
|
|
324
|
+
type: 'type1',
|
|
325
|
+
message: 'first',
|
|
326
|
+
timestamp: Date.now(),
|
|
327
|
+
source: externalSource,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const msg2 = {
|
|
331
|
+
id: 'msg2',
|
|
332
|
+
type: 'type2',
|
|
333
|
+
message: 'second',
|
|
334
|
+
timestamp: Date.now(),
|
|
335
|
+
source: externalSource,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const internalClear = {
|
|
339
|
+
id: 'internal-id',
|
|
340
|
+
type: getInternalMessageType('CLEAR_SENT_MESSAGES', 'test-channel'),
|
|
341
|
+
message: { ids: [], types: [] }, // wildcard
|
|
342
|
+
timestamp: Date.now(),
|
|
343
|
+
source: externalSource, // ✅ MUST match msg1/msg2
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
await act(async () => {
|
|
347
|
+
channel.simulateMessage(msg1);
|
|
348
|
+
});
|
|
349
|
+
await act(async () => {
|
|
350
|
+
channel.simulateMessage(msg2);
|
|
317
351
|
});
|
|
318
352
|
|
|
319
|
-
|
|
353
|
+
await act(() => Promise.resolve());
|
|
320
354
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
channel.simulateMessage({
|
|
324
|
-
id: 'internal-2',
|
|
325
|
-
type: internalTypeAll,
|
|
326
|
-
message: { ids: [], types: [] },
|
|
327
|
-
source: 'another-tab',
|
|
328
|
-
timestamp: Date.now(),
|
|
329
|
-
});
|
|
355
|
+
await waitFor(() => {
|
|
356
|
+
expect(result.current.messages.length).toBe(2);
|
|
330
357
|
});
|
|
331
358
|
|
|
332
|
-
|
|
359
|
+
await act(async () => {
|
|
360
|
+
channel.simulateMessage(internalClear);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
await act(() => Promise.resolve());
|
|
364
|
+
|
|
365
|
+
await waitFor(() => {
|
|
366
|
+
expect(result.current.messages.length).toBe(0); // ✅ Both cleared
|
|
367
|
+
});
|
|
333
368
|
});
|
|
334
369
|
|
|
335
370
|
it('performs cleanup of expired messages', async () => {
|
|
@@ -361,12 +396,14 @@ describe('useBroadcastChannel', () => {
|
|
|
361
396
|
|
|
362
397
|
it('handles registered types filtering', async () => {
|
|
363
398
|
const { result } = renderHook(() =>
|
|
364
|
-
useBroadcastChannel('test-channel', {
|
|
399
|
+
useBroadcastChannel('test-channel', {
|
|
400
|
+
registeredTypes: ['allowed-type'],
|
|
401
|
+
})
|
|
365
402
|
);
|
|
366
403
|
await waitForChannel();
|
|
367
404
|
const channel = mockChannels[0];
|
|
368
405
|
|
|
369
|
-
act(() => {
|
|
406
|
+
await act(async () => {
|
|
370
407
|
channel.simulateMessage({
|
|
371
408
|
id: 'unregistered',
|
|
372
409
|
type: 'unregistered-type',
|
|
@@ -378,7 +415,7 @@ describe('useBroadcastChannel', () => {
|
|
|
378
415
|
|
|
379
416
|
expect(result.current.messages.length).toBe(0);
|
|
380
417
|
|
|
381
|
-
act(() => {
|
|
418
|
+
await act(async () => {
|
|
382
419
|
channel.simulateMessage({
|
|
383
420
|
id: 'registered',
|
|
384
421
|
type: 'allowed-type',
|
|
@@ -388,7 +425,9 @@ describe('useBroadcastChannel', () => {
|
|
|
388
425
|
});
|
|
389
426
|
});
|
|
390
427
|
|
|
391
|
-
|
|
428
|
+
await waitFor(() => {
|
|
429
|
+
expect(result.current.messages.length).toBe(1);
|
|
430
|
+
});
|
|
392
431
|
});
|
|
393
432
|
|
|
394
433
|
it('handles keepLatestMessage option', async () => {
|
|
@@ -455,8 +494,10 @@ describe('useBroadcastChannel', () => {
|
|
|
455
494
|
await waitForChannel();
|
|
456
495
|
const channel = mockChannels[0];
|
|
457
496
|
|
|
458
|
-
|
|
459
|
-
|
|
497
|
+
act(() => {
|
|
498
|
+
channel.postMessage.mockImplementationOnce(() => {
|
|
499
|
+
throw new Error('Failed to post message');
|
|
500
|
+
});
|
|
460
501
|
});
|
|
461
502
|
|
|
462
503
|
act(() => {
|
|
@@ -516,4 +557,354 @@ describe('useBroadcastChannel', () => {
|
|
|
516
557
|
expect(sentMessage.expirationDate).toBeDefined();
|
|
517
558
|
expect(sentMessage.expirationDate).toBeGreaterThan(Date.now());
|
|
518
559
|
});
|
|
560
|
+
|
|
561
|
+
it('returns null if there are no messages', async () => {
|
|
562
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
563
|
+
await waitForChannel();
|
|
564
|
+
expect(result.current.getLatestMessage()).toBeNull();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('returns the latest message if no options are provided', async () => {
|
|
568
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
569
|
+
await waitForChannel();
|
|
570
|
+
const channel = mockChannels[0];
|
|
571
|
+
const msg1 = {
|
|
572
|
+
id: '1',
|
|
573
|
+
type: 'typeA',
|
|
574
|
+
message: 'first',
|
|
575
|
+
source: 'source1',
|
|
576
|
+
timestamp: Date.now(),
|
|
577
|
+
};
|
|
578
|
+
const msg2 = {
|
|
579
|
+
id: '2',
|
|
580
|
+
type: 'typeB',
|
|
581
|
+
message: 'second',
|
|
582
|
+
source: 'source2',
|
|
583
|
+
timestamp: Date.now() + 1,
|
|
584
|
+
};
|
|
585
|
+
act(() => {
|
|
586
|
+
channel.simulateMessage(msg1);
|
|
587
|
+
channel.simulateMessage(msg2);
|
|
588
|
+
});
|
|
589
|
+
expect(result.current.getLatestMessage()).toEqual(expect.objectContaining(msg2));
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('returns the latest message matching type', async () => {
|
|
593
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
594
|
+
await waitForChannel();
|
|
595
|
+
const channel = mockChannels[0];
|
|
596
|
+
const msg1 = {
|
|
597
|
+
id: '1',
|
|
598
|
+
type: 'typeA',
|
|
599
|
+
message: 'first',
|
|
600
|
+
source: 'source1',
|
|
601
|
+
timestamp: Date.now(),
|
|
602
|
+
};
|
|
603
|
+
const msg2 = {
|
|
604
|
+
id: '2',
|
|
605
|
+
type: 'typeB',
|
|
606
|
+
message: 'second',
|
|
607
|
+
source: 'source2',
|
|
608
|
+
timestamp: Date.now() + 1,
|
|
609
|
+
};
|
|
610
|
+
const msg3 = {
|
|
611
|
+
id: '3',
|
|
612
|
+
type: 'typeA',
|
|
613
|
+
message: 'third',
|
|
614
|
+
source: 'source3',
|
|
615
|
+
timestamp: Date.now() + 2,
|
|
616
|
+
};
|
|
617
|
+
act(() => {
|
|
618
|
+
channel.simulateMessage(msg1);
|
|
619
|
+
channel.simulateMessage(msg2);
|
|
620
|
+
channel.simulateMessage(msg3);
|
|
621
|
+
});
|
|
622
|
+
expect(result.current.getLatestMessage({ type: 'typeA' })).toEqual(
|
|
623
|
+
expect.objectContaining(msg3)
|
|
624
|
+
);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('returns the latest message matching source', async () => {
|
|
628
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
629
|
+
await waitForChannel();
|
|
630
|
+
const channel = mockChannels[0];
|
|
631
|
+
const msg1 = {
|
|
632
|
+
id: '1',
|
|
633
|
+
type: 'typeA',
|
|
634
|
+
message: 'first',
|
|
635
|
+
source: 'source1',
|
|
636
|
+
timestamp: Date.now(),
|
|
637
|
+
};
|
|
638
|
+
const msg2 = {
|
|
639
|
+
id: '2',
|
|
640
|
+
type: 'typeB',
|
|
641
|
+
message: 'second',
|
|
642
|
+
source: 'source2',
|
|
643
|
+
timestamp: Date.now() + 1,
|
|
644
|
+
};
|
|
645
|
+
const msg3 = {
|
|
646
|
+
id: '3',
|
|
647
|
+
type: 'typeA',
|
|
648
|
+
message: 'third',
|
|
649
|
+
source: 'source1',
|
|
650
|
+
timestamp: Date.now() + 2,
|
|
651
|
+
};
|
|
652
|
+
act(() => {
|
|
653
|
+
channel.simulateMessage(msg1);
|
|
654
|
+
channel.simulateMessage(msg2);
|
|
655
|
+
channel.simulateMessage(msg3);
|
|
656
|
+
});
|
|
657
|
+
expect(result.current.getLatestMessage({ source: 'source1' })).toEqual(
|
|
658
|
+
expect.objectContaining(msg3)
|
|
659
|
+
);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('returns the latest message matching both type and source', async () => {
|
|
663
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
664
|
+
await waitForChannel();
|
|
665
|
+
const channel = mockChannels[0];
|
|
666
|
+
const msg1 = {
|
|
667
|
+
id: '1',
|
|
668
|
+
type: 'typeA',
|
|
669
|
+
message: 'first',
|
|
670
|
+
source: 'source1',
|
|
671
|
+
timestamp: Date.now(),
|
|
672
|
+
};
|
|
673
|
+
const msg2 = {
|
|
674
|
+
id: '2',
|
|
675
|
+
type: 'typeB',
|
|
676
|
+
message: 'second',
|
|
677
|
+
source: 'source2',
|
|
678
|
+
timestamp: Date.now() + 1,
|
|
679
|
+
};
|
|
680
|
+
const msg3 = {
|
|
681
|
+
id: '3',
|
|
682
|
+
type: 'typeA',
|
|
683
|
+
message: 'third',
|
|
684
|
+
source: 'source1',
|
|
685
|
+
timestamp: Date.now() + 2,
|
|
686
|
+
};
|
|
687
|
+
act(() => {
|
|
688
|
+
channel.simulateMessage(msg1);
|
|
689
|
+
channel.simulateMessage(msg2);
|
|
690
|
+
channel.simulateMessage(msg3);
|
|
691
|
+
});
|
|
692
|
+
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
693
|
+
expect.objectContaining(msg3)
|
|
694
|
+
);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('returns null if no message matches the filter', async () => {
|
|
698
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
699
|
+
await waitForChannel();
|
|
700
|
+
const channel = mockChannels[0];
|
|
701
|
+
const msg1 = {
|
|
702
|
+
id: '1',
|
|
703
|
+
type: 'typeA',
|
|
704
|
+
message: 'first',
|
|
705
|
+
source: 'source1',
|
|
706
|
+
timestamp: Date.now(),
|
|
707
|
+
};
|
|
708
|
+
act(() => {
|
|
709
|
+
channel.simulateMessage(msg1);
|
|
710
|
+
});
|
|
711
|
+
expect(result.current.getLatestMessage({ type: 'typeB' })).toBeNull();
|
|
712
|
+
expect(result.current.getLatestMessage({ source: 'source2' })).toBeNull();
|
|
713
|
+
expect(result.current.getLatestMessage({ type: 'typeB', source: 'source2' })).toBeNull();
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('works with keepLatestMessage=true (only one message)', async () => {
|
|
717
|
+
const { result } = renderHook(() =>
|
|
718
|
+
useBroadcastChannel('test-channel', { keepLatestMessage: true })
|
|
719
|
+
);
|
|
720
|
+
await waitForChannel();
|
|
721
|
+
const channel = mockChannels[0];
|
|
722
|
+
const msg1 = {
|
|
723
|
+
id: '1',
|
|
724
|
+
type: 'typeA',
|
|
725
|
+
message: 'first',
|
|
726
|
+
source: 'source1',
|
|
727
|
+
timestamp: Date.now(),
|
|
728
|
+
};
|
|
729
|
+
act(() => {
|
|
730
|
+
channel.simulateMessage(msg1);
|
|
731
|
+
});
|
|
732
|
+
expect(result.current.getLatestMessage()).toEqual(expect.objectContaining(msg1));
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it('returns the latest message in correct order (last one)', async () => {
|
|
736
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
737
|
+
await waitForChannel();
|
|
738
|
+
const channel = mockChannels[0];
|
|
739
|
+
const msg1 = {
|
|
740
|
+
id: '1',
|
|
741
|
+
type: 'typeA',
|
|
742
|
+
message: 'first',
|
|
743
|
+
source: 'source1',
|
|
744
|
+
timestamp: Date.now(),
|
|
745
|
+
};
|
|
746
|
+
const msg2 = {
|
|
747
|
+
id: '2',
|
|
748
|
+
type: 'typeA',
|
|
749
|
+
message: 'second',
|
|
750
|
+
source: 'source1',
|
|
751
|
+
timestamp: Date.now() + 1,
|
|
752
|
+
};
|
|
753
|
+
act(() => {
|
|
754
|
+
channel.simulateMessage(msg1);
|
|
755
|
+
channel.simulateMessage(msg2);
|
|
756
|
+
});
|
|
757
|
+
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
758
|
+
expect.objectContaining(msg2)
|
|
759
|
+
);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('treats empty string filters as no filter (returns latest message)', async () => {
|
|
763
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
764
|
+
await waitForChannel();
|
|
765
|
+
const channel = mockChannels[0];
|
|
766
|
+
const msg1 = {
|
|
767
|
+
id: '1',
|
|
768
|
+
type: 'typeA',
|
|
769
|
+
message: 'first',
|
|
770
|
+
source: 'source1',
|
|
771
|
+
timestamp: 1000,
|
|
772
|
+
};
|
|
773
|
+
const msg2 = {
|
|
774
|
+
id: '2',
|
|
775
|
+
type: 'typeB',
|
|
776
|
+
message: 'second',
|
|
777
|
+
source: 'source2',
|
|
778
|
+
timestamp: 2000,
|
|
779
|
+
};
|
|
780
|
+
act(() => {
|
|
781
|
+
channel.simulateMessage(msg1);
|
|
782
|
+
channel.simulateMessage(msg2);
|
|
783
|
+
});
|
|
784
|
+
expect(result.current.getLatestMessage({ type: '', source: '' })).toEqual(
|
|
785
|
+
expect.objectContaining(msg2)
|
|
786
|
+
);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it('returns the last message in array if timestamps are the same', async () => {
|
|
790
|
+
jest.spyOn(Date, 'now').mockReturnValue(1000);
|
|
791
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
792
|
+
await waitForChannel();
|
|
793
|
+
const channel = mockChannels[0];
|
|
794
|
+
const msg1 = {
|
|
795
|
+
id: '1',
|
|
796
|
+
type: 'typeA',
|
|
797
|
+
message: 'first',
|
|
798
|
+
source: 'source1',
|
|
799
|
+
timestamp: 1000,
|
|
800
|
+
};
|
|
801
|
+
const msg2 = {
|
|
802
|
+
id: '2',
|
|
803
|
+
type: 'typeA',
|
|
804
|
+
message: 'second',
|
|
805
|
+
source: 'source1',
|
|
806
|
+
timestamp: 1000,
|
|
807
|
+
};
|
|
808
|
+
act(() => {
|
|
809
|
+
channel.simulateMessage(msg1);
|
|
810
|
+
channel.simulateMessage(msg2);
|
|
811
|
+
});
|
|
812
|
+
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
813
|
+
expect.objectContaining(msg2)
|
|
814
|
+
);
|
|
815
|
+
jest.spyOn(Date, 'now').mockRestore?.();
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it('returns the last matching message if multiple match the filters', async () => {
|
|
819
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
820
|
+
await waitForChannel();
|
|
821
|
+
const channel = mockChannels[0];
|
|
822
|
+
const msg1 = {
|
|
823
|
+
id: '1',
|
|
824
|
+
type: 'typeA',
|
|
825
|
+
message: 'first',
|
|
826
|
+
source: 'source1',
|
|
827
|
+
timestamp: 1000,
|
|
828
|
+
};
|
|
829
|
+
const msg2 = {
|
|
830
|
+
id: '2',
|
|
831
|
+
type: 'typeB',
|
|
832
|
+
message: 'second',
|
|
833
|
+
source: 'source2',
|
|
834
|
+
timestamp: 2000,
|
|
835
|
+
};
|
|
836
|
+
const msg3 = {
|
|
837
|
+
id: '3',
|
|
838
|
+
type: 'typeA',
|
|
839
|
+
message: 'third',
|
|
840
|
+
source: 'source1',
|
|
841
|
+
timestamp: 3000,
|
|
842
|
+
};
|
|
843
|
+
act(() => {
|
|
844
|
+
channel.simulateMessage(msg1);
|
|
845
|
+
channel.simulateMessage(msg2);
|
|
846
|
+
channel.simulateMessage(msg3);
|
|
847
|
+
});
|
|
848
|
+
// msg1 and msg3 both match typeA/source1 — expect msg3
|
|
849
|
+
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
850
|
+
expect.objectContaining(msg3)
|
|
851
|
+
);
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
it('closes the BroadcastChannel and removes event listener', async () => {
|
|
855
|
+
const { result } = renderHook(() =>
|
|
856
|
+
useBroadcastChannel('test-channel', { sourceName: 'another-tab' })
|
|
857
|
+
);
|
|
858
|
+
await waitForChannel();
|
|
859
|
+
const channel = mockChannels[0];
|
|
860
|
+
expect(channel.close).not.toHaveBeenCalled();
|
|
861
|
+
result.current.closeChannel();
|
|
862
|
+
expect(channel.close).toHaveBeenCalled();
|
|
863
|
+
expect(channel.removeEventListener).toHaveBeenCalledWith('message', expect.any(Function));
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it('is idempotent (safe to call multiple times)', async () => {
|
|
867
|
+
const { result } = renderHook(() =>
|
|
868
|
+
useBroadcastChannel('test-channel', { sourceName: 'another-tab' })
|
|
869
|
+
);
|
|
870
|
+
await waitForChannel();
|
|
871
|
+
const channel = mockChannels[0];
|
|
872
|
+
result.current.closeChannel();
|
|
873
|
+
expect(channel.close).toHaveBeenCalledTimes(1);
|
|
874
|
+
result.current.closeChannel();
|
|
875
|
+
// Should not throw or call close again
|
|
876
|
+
expect(channel.close).toHaveBeenCalledTimes(1);
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it('does not send messages after closeChannel is called', async () => {
|
|
880
|
+
const { result } = renderHook(() =>
|
|
881
|
+
useBroadcastChannel('test-channel', { sourceName: 'another-tab' })
|
|
882
|
+
);
|
|
883
|
+
await waitForChannel();
|
|
884
|
+
const channel = mockChannels[0];
|
|
885
|
+
result.current.closeChannel();
|
|
886
|
+
act(() => {
|
|
887
|
+
result.current.postMessage('test', { data: 'should not send' });
|
|
888
|
+
});
|
|
889
|
+
expect(channel.postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'test' }));
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
it('does not receive messages after closeChannel is called', async () => {
|
|
893
|
+
const { result } = renderHook(() =>
|
|
894
|
+
useBroadcastChannel('test-channel', { sourceName: 'another-tab' })
|
|
895
|
+
);
|
|
896
|
+
await waitForChannel();
|
|
897
|
+
const channel = mockChannels[0];
|
|
898
|
+
result.current.closeChannel();
|
|
899
|
+
act(() => {
|
|
900
|
+
channel.simulateMessage({
|
|
901
|
+
id: 'should-not-receive',
|
|
902
|
+
type: 'test',
|
|
903
|
+
message: 'no',
|
|
904
|
+
source: 'external',
|
|
905
|
+
timestamp: Date.now(),
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
expect(result.current.messages).toHaveLength(0);
|
|
909
|
+
});
|
|
519
910
|
});
|
package/src/types/types.ts
CHANGED
|
@@ -70,6 +70,11 @@ export interface ClearSentMessagesOptions extends ClearOptions {
|
|
|
70
70
|
sync?: boolean;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export interface GetLatestMessageOptions {
|
|
74
|
+
source?: string;
|
|
75
|
+
type?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
/**
|
|
74
79
|
* Actions and state provided by the broadcast channel hook
|
|
75
80
|
*/
|
|
@@ -92,6 +97,12 @@ export interface BroadcastActions {
|
|
|
92
97
|
/** Function to clear sent messages */
|
|
93
98
|
clearSentMessages: (options?: ClearSentMessagesOptions) => void;
|
|
94
99
|
|
|
100
|
+
/** Function to get the latest message by source and type */
|
|
101
|
+
getLatestMessage: (options?: GetLatestMessageOptions) => BroadcastMessage | null;
|
|
102
|
+
|
|
103
|
+
/** Explicitly close the broadcast channel and remove listeners */
|
|
104
|
+
closeChannel: () => void;
|
|
105
|
+
|
|
95
106
|
/** Current error state */
|
|
96
107
|
error: string | null;
|
|
97
108
|
}
|
package/src/utils/debug.ts
CHANGED
|
@@ -40,6 +40,8 @@ export const debug = {
|
|
|
40
40
|
duplicate: (messageId: string) => log('warn', 'Duplicate message ignored:', messageId),
|
|
41
41
|
allSentCleared: () => log('info', 'All sent messages cleared'),
|
|
42
42
|
allReceivedCleared: () => log('info', 'All received messages cleared'),
|
|
43
|
+
ignored: (messageType: string) =>
|
|
44
|
+
log('info', 'Message ignored due to type filter:', messageType),
|
|
43
45
|
},
|
|
44
46
|
cleanup: {
|
|
45
47
|
started: () => log('info', 'Cleanup started'),
|