Created
September 19, 2024 06:40
-
-
Save LiniovasDovydas/67f3fadd207d4375c6f8777711458d07 to your computer and use it in GitHub Desktop.
Broadcast channel state sync with react external store API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type BroadcastStateSyncMessage = { type: "STATE_SYNC" }; | |
type BroadcastStateUpdate<S> = { type: "UPDATE"; data: S }; | |
type BroadcastCheckOwnerShipMessage = { type: "CHECK_OWNERSHIP" }; | |
type BroadcastTransferOwnershipMessage = { type: "TRANSFER_OWNERSHIP" }; | |
type BroadcastOwnershipTakenMessage = { type: "OWNERSHIP_TAKEN"; id: string }; | |
type BroadcastDefaultMessages = | |
| BroadcastStateSyncMessage | |
| BroadcastOwnershipTakenMessage | |
| BroadcastTransferOwnershipMessage | |
| BroadcastCheckOwnerShipMessage; | |
type BroadcastMessage<S> = BroadcastStateUpdate<S> | BroadcastDefaultMessages; | |
const OWNERSHIP_TAKEOVER_DELAY = 24; // ms | |
const channels = new Map< | |
string, | |
ReturnType<typeof createBroadcastChannelStore> | |
>(); | |
function createBroadcastChannelStore<S>(name: string, initialState: S) { | |
const id = `${Date.now()}`; | |
const listeners = new Set<() => void>(); | |
const channel = new BroadcastChannel(name); | |
let isOwner = false; | |
let data = initialState; | |
let ownershipTimeout: number | undefined; | |
// Internal functions | |
const notifyListeners = () => { | |
listeners.forEach((callback) => callback()); | |
}; | |
const postDefaultMessage = (message: BroadcastMessage<S>) => { | |
channel.postMessage(message); | |
}; | |
const giveUpOwnership = () => { | |
window.removeEventListener("beforeunload", giveUpOwnership); | |
if (!isOwner) { | |
return; | |
} | |
postDefaultMessage({ type: "TRANSFER_OWNERSHIP" }); | |
}; | |
const takeOwnership = () => { | |
window.addEventListener("beforeunload", giveUpOwnership); | |
if (ownershipTimeout) { | |
clearTimeout(ownershipTimeout); | |
} | |
ownershipTimeout = window.setTimeout(() => { | |
isOwner = true; | |
postDefaultMessage({ type: "OWNERSHIP_TAKEN", id }); | |
}, OWNERSHIP_TAKEOVER_DELAY); | |
}; | |
const onMessageReceived = (event: MessageEvent<BroadcastMessage<S>>) => { | |
if (!isBroadCastMessage<S>(event.data)) { | |
return; | |
} | |
switch (event.data.type) { | |
case "OWNERSHIP_TAKEN": | |
isOwner = event.data.id === id; | |
if (!isOwner) { | |
clearTimeout(ownershipTimeout); | |
} | |
break; | |
case "STATE_SYNC": | |
if (isOwner) { | |
postDefaultMessage({ type: "UPDATE", data }); | |
} | |
break; | |
case "TRANSFER_OWNERSHIP": | |
takeOwnership(); | |
break; | |
case "CHECK_OWNERSHIP": | |
if (isOwner) { | |
postDefaultMessage({ type: "OWNERSHIP_TAKEN", id }); | |
} | |
break; | |
case "UPDATE": | |
data = event.data.data; | |
notifyListeners(); | |
break; | |
} | |
}; | |
const setData = (newData: S) => { | |
data = newData; | |
postDefaultMessage({ type: "UPDATE", data }); | |
notifyListeners(); | |
}; | |
const getSnapshot = () => data; | |
const syncInitialValue = () => postDefaultMessage({ type: "STATE_SYNC" }); | |
channel.onmessage = onMessageReceived; | |
syncInitialValue(); | |
postDefaultMessage({ type: "CHECK_OWNERSHIP" }); | |
takeOwnership(); | |
return { | |
subscribe: (onStateChange: () => void) => { | |
listeners.add(onStateChange); | |
return () => { | |
listeners.delete(onStateChange); | |
if (listeners.size === 0) { | |
requestAnimationFrame(() => { | |
if (listeners.size === 0) { | |
channels.delete(name); | |
giveUpOwnership(); | |
channel.close(); | |
} | |
}); | |
} | |
}; | |
}, | |
setData, | |
getSnapshot, | |
}; | |
} | |
function getBroadcastStore<S>(name: string, initialState: S) { | |
const channel = channels.get(name); | |
if (!channel) { | |
const instance = createBroadcastChannelStore<S>(name, initialState); | |
channels.set( | |
name, | |
instance as ReturnType<typeof createBroadcastChannelStore> | |
); | |
return instance; | |
} | |
return channel as ReturnType<typeof createBroadcastChannelStore<S>>; | |
} | |
function isBroadCastMessage<S>( | |
message: unknown | |
): message is BroadcastMessage<S> { | |
if (!message || typeof message !== "object") { | |
return false; | |
} | |
return "type" in message; | |
} | |
export { getBroadcastStore }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState, useSyncExternalStore } from "react"; | |
import { getBroadcastStore } from "./broadcastStore"; | |
function useBroadcastStore<S>(name: string, initialState: S) { | |
const [channel] = useState(getBroadcastStore<S>(name, initialState)); | |
const state = useSyncExternalStore( | |
channel.subscribe, | |
channel.getSnapshot, | |
undefined | |
); | |
return [state, channel.setData] as const; | |
} | |
export { useBroadcastStore }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment