-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
42da60e
commit cc91466
Showing
5 changed files
with
137 additions
and
35 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from "./pub-sub"; | ||
export * from "./usePubSub"; | ||
export * from "./usePubSubState"; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
type ChannelListeners = { __listeners: Array<Listener<BaseMessage>> } | ||
type ChannelFragments = { [topic: string]: Channel | undefined } | ||
type Channel = ChannelListeners & ChannelFragments | ||
|
||
type Unsubscribe = () => void | ||
|
||
export type Topic = string[] | ||
export type Listener<Message extends BaseMessage> = (message: Message) => void | ||
export type BaseMessage = { type: string } | ||
|
||
const symbol = Symbol.for("r2wc.pubsub") | ||
|
||
declare global { | ||
interface Window { | ||
[symbol]: Channel | ||
} | ||
} | ||
|
||
function createChannel(): Channel { | ||
return { __listeners: [] } as unknown as Channel | ||
} | ||
|
||
export const subscribe = <Message extends BaseMessage>( | ||
topic: Topic, | ||
listener?: Listener<Message>, | ||
): Unsubscribe | undefined => { | ||
if (!listener) return | ||
|
||
if (!window[symbol]) { | ||
window[symbol] = createChannel() | ||
} | ||
|
||
let channel: Channel = window[symbol] | ||
|
||
for (const fragment of topic) { | ||
if (!channel[fragment]) channel[fragment] = createChannel() | ||
channel = channel[fragment] as Channel | ||
} | ||
|
||
channel.__listeners.push(listener as Listener<BaseMessage>) | ||
|
||
return () => { | ||
const index = channel.__listeners.indexOf(listener as Listener<BaseMessage>) | ||
if (index > -1) { | ||
channel.__listeners.splice(index, 1) | ||
} | ||
} | ||
} | ||
|
||
export const publish = <Message extends BaseMessage>( | ||
topic: Topic, | ||
message: Message, | ||
): void => { | ||
if (!window[symbol]) { | ||
window[symbol] = createChannel() | ||
} | ||
|
||
const listeners: Array<Listener<BaseMessage>> = [] | ||
|
||
let channel: Channel = window[symbol] | ||
|
||
for (const fragment of topic) { | ||
listeners.unshift(...channel.__listeners) | ||
|
||
if (!channel[fragment]) channel[fragment] = createChannel() | ||
|
||
channel = channel[fragment] as Channel | ||
} | ||
|
||
channel.__listeners.forEach((subscriber) => subscriber(message)) | ||
listeners.forEach((subscriber) => subscriber(message)) | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { useEffect, useCallback, useRef } from "react"; | ||
|
||
import { BaseMessage, publish, subscribe } from "./pub-sub"; | ||
|
||
const _internalTopicStabilizer = "-||-||-"; | ||
|
||
export const usePubSub = <TMessage extends BaseMessage>( | ||
topics: string[], | ||
handler?: (data: TMessage) => void | ||
): ((data: TMessage) => void) => { | ||
const stableHandler = useRef(handler); | ||
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
useEffect( | ||
() => subscribe(topics, stableHandler?.current), | ||
[topics.join(_internalTopicStabilizer)] | ||
); | ||
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
return useCallback( | ||
(data: TMessage) => publish(topics, data), | ||
[topics.join(_internalTopicStabilizer)] | ||
); | ||
}; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import type { BaseMessage } from "./pub-sub"; | ||
|
||
import { useState, useCallback } from "react"; | ||
|
||
import { usePubSub } from "./usePubSub"; | ||
|
||
export const usePubSubState = <TMessage extends BaseMessage>( | ||
topics: string[], | ||
initialValue?: TMessage, | ||
filter?: (data: TMessage) => boolean | ||
): [TMessage | undefined, (data: TMessage) => void] => { | ||
const [state, setState] = useState(initialValue); | ||
|
||
const filteredSetter = useCallback( | ||
(data: TMessage) => { | ||
if (!filter) { | ||
setState(data); | ||
return; | ||
} | ||
|
||
if (filter(data)) { | ||
setState(data); | ||
return; | ||
} | ||
}, | ||
[filter] | ||
); | ||
|
||
const publish = usePubSub(topics, filteredSetter); | ||
|
||
return [state, publish]; | ||
}; |