diff --git a/package-lock.json b/package-lock.json index 26b834e..c6f7ff9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6500,7 +6500,8 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "node_modules/js-yaml": { "version": "3.14.1", @@ -6789,6 +6790,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -7865,31 +7867,6 @@ } ] }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -8328,15 +8305,6 @@ "node": ">=v12.22.7" } }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -9799,6 +9767,9 @@ "name": "@r2wc/emotion", "version": "0.1.0", "license": "MIT", + "dependencies": { + "@r2wc/core": "^1.0.1" + }, "devDependencies": { "@emotion/cache": "^11.11.0", "@emotion/react": "^11.11.1", diff --git a/pub-sub/index.ts b/pub-sub/index.ts new file mode 100644 index 0000000..baa7df4 --- /dev/null +++ b/pub-sub/index.ts @@ -0,0 +1,3 @@ +export * from "./pub-sub"; +export * from "./usePubSub"; +export * from "./usePubSubState"; diff --git a/pub-sub/pub-sub.ts b/pub-sub/pub-sub.ts new file mode 100644 index 0000000..63126ec --- /dev/null +++ b/pub-sub/pub-sub.ts @@ -0,0 +1,72 @@ +type ChannelListeners = { __listeners: Array> } +type ChannelFragments = { [topic: string]: Channel | undefined } +type Channel = ChannelListeners & ChannelFragments + +type Unsubscribe = () => void + +export type Topic = string[] +export type Listener = (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 = ( + topic: Topic, + listener?: Listener, +): 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) + + return () => { + const index = channel.__listeners.indexOf(listener as Listener) + if (index > -1) { + channel.__listeners.splice(index, 1) + } + } +} + +export const publish = ( + topic: Topic, + message: Message, +): void => { + if (!window[symbol]) { + window[symbol] = createChannel() + } + + const listeners: Array> = [] + + 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)) +} diff --git a/pub-sub/usePubSub.ts b/pub-sub/usePubSub.ts new file mode 100644 index 0000000..3aada93 --- /dev/null +++ b/pub-sub/usePubSub.ts @@ -0,0 +1,24 @@ +import { useEffect, useCallback, useRef } from "react"; + +import { BaseMessage, publish, subscribe } from "./pub-sub"; + +const _internalTopicStabilizer = "-||-||-"; + +export const usePubSub = ( + 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)] + ); +}; diff --git a/pub-sub/usePubSubState.ts b/pub-sub/usePubSubState.ts new file mode 100644 index 0000000..4c3e85e --- /dev/null +++ b/pub-sub/usePubSubState.ts @@ -0,0 +1,32 @@ +import type { BaseMessage } from "./pub-sub"; + +import { useState, useCallback } from "react"; + +import { usePubSub } from "./usePubSub"; + +export const usePubSubState = ( + 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]; +};