diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml
new file mode 100644
index 0000000..a336723
--- /dev/null
+++ b/.github/workflows/workflow.yml
@@ -0,0 +1,31 @@
+name: "🚀 CI Workflow"
+
+on:
+ push:
+ branches:
+ - master
+ workflow_call: {}
+ pull_request: {}
+
+jobs:
+ test:
+ name: 🃏 Test
+ runs-on: ubuntu-latest
+ steps:
+ - name: 🛑 Cancel Previous Runs
+ uses: styfle/cancel-workflow-action@0.9.1
+
+ - name: ⬇️ Checkout repo
+ uses: actions/checkout@v3
+
+ - name: ⎔ Setup node
+ uses: actions/setup-node@v3
+ with:
+ cache: "yarn"
+ node-version: 16
+
+ - name: 📥 Download deps
+ run: yarn install
+
+ - name: 🃏 Test
+ run: yarn test
diff --git a/package.json b/package.json
index eafd0de..3e3317d 100644
--- a/package.json
+++ b/package.json
@@ -42,10 +42,10 @@
"dependencies": {
"dequal": "^2.0.1",
"invariant": "^2.2.4",
- "pusher-js": "^7.0.0"
+ "pusher-js": "^7.3.0"
},
"peerDependencies": {
- "react": "^16.9.0"
+ "react": ">=16.9.0"
},
"devDependencies": {
"@babel/core": "^7.8.6",
diff --git a/src/__tests__/useChannel.tsx b/src/__tests__/useChannel.tsx
index 8efe361..4281804 100644
--- a/src/__tests__/useChannel.tsx
+++ b/src/__tests__/useChannel.tsx
@@ -1,24 +1,30 @@
-import { PusherChannelMock } from "pusher-js-mock";
+import { PusherChannelMock, PusherMock } from "pusher-js-mock";
import React from "react";
import { renderHook } from "@testing-library/react-hooks";
import { renderHookWithProvider } from "../testUtils";
-import { useChannel, NO_CHANNEL_NAME_WARNING } from "../core/useChannel";
+import { useChannel } from "../core/useChannel";
import { __PusherContext } from "../core/PusherProvider";
+import { ChannelsProvider } from "../web";
describe("useChannel()", () => {
- test("should throw an error when no channelName present", () => {
- const wrapper: React.FC = (props) => (
- <__PusherContext.Provider value={{ client: {} as any }} {...props} />
+ test("should return undefined when channelName is falsy", () => {
+ const wrapper: React.FC = ({ children }) => (
+ <__PusherContext.Provider value={{ client: {} as any }}>
+ {children}
+
);
+ const { result } = renderHook(() => useChannel(""), {
+ wrapper,
+ });
- jest.spyOn(console, "warn");
- renderHook(() => useChannel(undefined), { wrapper });
- expect(console.warn).toHaveBeenCalledWith(NO_CHANNEL_NAME_WARNING);
+ expect(result.current).toBeUndefined();
});
test("should return undefined if no pusher client present", () => {
- const wrapper: React.FC = (props) => (
- <__PusherContext.Provider value={{ client: undefined }} {...props} />
+ const wrapper: React.FC = ({ children }) => (
+ <__PusherContext.Provider value={{ client: undefined }}>
+ {children}
+
);
const { result } = renderHook(() => useChannel("public-channel"), {
wrapper,
@@ -35,19 +41,18 @@ describe("useChannel()", () => {
});
test("should unsubscribe on unmount", async () => {
- const mockUnsubscribe = jest.fn();
- const client = {
- subscribe: jest.fn(),
- unsubscribe: mockUnsubscribe,
- };
- const wrapper: React.FC = (props) => (
- <__PusherContext.Provider value={{ client: client as any }} {...props} />
+ const client = new PusherMock("key");
+ client.unsubscribe = jest.fn();
+ const wrapper: React.FC = ({ children, ...props }) => (
+ <__PusherContext.Provider value={{ client: client as any }} {...props}>
+ {children}
+
);
- const { unmount } = await renderHook(() => useChannel("public-channel"), {
+ const { unmount } = renderHook(() => useChannel("public-channel"), {
wrapper,
});
unmount();
- expect(mockUnsubscribe).toHaveBeenCalled();
+ expect(client.unsubscribe).toHaveBeenCalled();
});
});
diff --git a/src/core/ChannelsProvider.tsx b/src/core/ChannelsProvider.tsx
new file mode 100644
index 0000000..ddf1656
--- /dev/null
+++ b/src/core/ChannelsProvider.tsx
@@ -0,0 +1,88 @@
+import { Channel, PresenceChannel } from "pusher-js";
+import React, { useCallback, useRef } from "react";
+import { ChannelsContextValues } from "./types";
+
+import { usePusher } from "./usePusher";
+
+// context setup
+const ChannelsContext = React.createContext({});
+export const __ChannelsContext = ChannelsContext;
+
+type AcceptedChannels = Channel | PresenceChannel;
+type ConnectedChannels = {
+ [channelName: string]: AcceptedChannels[];
+};
+
+/**
+ * Provider that creates your channels instances and provides it to child hooks throughout your app.
+ */
+
+export const ChannelsProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const { client } = usePusher();
+ const connectedChannels = useRef({});
+
+ const subscribe = useCallback(
+ (channelName: string) => {
+ /** Return early if there's no client */
+ if (!client || !channelName) return;
+
+ /** Subscribe to channel and set it in state */
+ const pusherChannel = client.subscribe(channelName);
+ connectedChannels.current[channelName] = [
+ ...(connectedChannels.current[channelName] || []),
+ pusherChannel,
+ ];
+ return pusherChannel as T;
+ },
+ [client, connectedChannels]
+ );
+
+ const unsubscribe = useCallback(
+ (channelName: string) => {
+ /** Return early if there's no props */
+ if (
+ !client ||
+ !channelName ||
+ !(channelName in connectedChannels.current)
+ )
+ return;
+ /** If just one connection, unsubscribe totally*/
+ if (connectedChannels.current[channelName].length === 1) {
+ client.unsubscribe(channelName);
+ delete connectedChannels.current[channelName];
+ } else {
+ connectedChannels.current[channelName].pop();
+ }
+ },
+ [connectedChannels, client]
+ );
+
+ const getChannel = useCallback(
+ (channelName: string) => {
+ /** Return early if there's no client */
+ if (
+ !client ||
+ !channelName ||
+ !(channelName in connectedChannels.current)
+ )
+ return;
+ /** Return channel */
+ return connectedChannels.current[channelName][0] as T;
+ },
+ [connectedChannels, client]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/core/PusherProvider.tsx b/src/core/PusherProvider.tsx
index f7acba2..ef9dbb0 100644
--- a/src/core/PusherProvider.tsx
+++ b/src/core/PusherProvider.tsx
@@ -1,8 +1,8 @@
import { Options } from "pusher-js";
-import { PusherContextValues, PusherProviderProps } from "./types";
-import React, { useEffect, useRef, useState } from "react";
-
+import React, { useEffect, useMemo, useRef, useState } from "react";
import { dequal } from "dequal";
+import { PusherContextValues, PusherProviderProps } from "./types";
+import { ChannelsProvider } from "./ChannelsProvider";
// context setup
const PusherContext = React.createContext({});
@@ -30,7 +30,10 @@ export const CorePusherProvider: React.FC = ({
if (!cluster) console.error("A cluster is required for pusher");
}, [clientKey, cluster]);
- const config: Options = { cluster, ...props };
+ const config: Options = useMemo(
+ () => ({ cluster, ...props }),
+ [cluster, props]
+ );
// track config for comparison
const previousConfig = useRef(props);
@@ -44,15 +47,15 @@ export const CorePusherProvider: React.FC = ({
if (
!_PusherRuntime ||
defer ||
+ !clientKey ||
props.value ||
(dequal(previousConfig.current, props) && client !== undefined)
) {
return;
}
- // @ts-ignore
setClient(new _PusherRuntime(clientKey, config));
- }, [client, clientKey, props, defer]);
+ }, [client, clientKey, props, defer, _PusherRuntime, config]);
return (
= ({
client,
triggerEndpoint,
}}
- children={children}
{...props}
- />
+ >
+ {children}
+
);
};
diff --git a/src/core/types.d.ts b/src/core/types.d.ts
index d28a1d6..1df03bd 100644
--- a/src/core/types.d.ts
+++ b/src/core/types.d.ts
@@ -1,4 +1,9 @@
-import { default as Pusher, Options } from "pusher-js";
+import {
+ Channel,
+ default as Pusher,
+ Options,
+ PresenceChannel,
+} from "pusher-js";
import * as React from "react";
import "jest-fetch-mock";
@@ -8,8 +13,21 @@ export interface PusherContextValues {
triggerEndpoint?: string;
}
+export interface ChannelsContextValues {
+ subscribe?: (
+ channelName: string
+ ) => T | undefined;
+ unsubscribe?: (
+ channelName: string
+ ) => void;
+ getChannel?: (
+ channelName: string
+ ) => T | undefined;
+}
+
export interface PusherProviderProps extends Options {
_PusherRuntime?: typeof Pusher;
+ children: React.ReactNode;
clientKey: string | undefined;
cluster:
| "mt1"
diff --git a/src/core/useChannel.ts b/src/core/useChannel.ts
index 750cf6e..9624645 100644
--- a/src/core/useChannel.ts
+++ b/src/core/useChannel.ts
@@ -1,6 +1,6 @@
import { Channel, PresenceChannel } from "pusher-js";
import { useEffect, useState } from "react";
-import { usePusher } from "./usePusher";
+import { useChannels } from "./useChannels";
/**
* Subscribe to a channel
@@ -16,31 +16,19 @@ import { usePusher } from "./usePusher";
* ```
*/
-export const NO_CHANNEL_NAME_WARNING =
- "No channel name passed to useChannel. No channel has been subscribed to.";
-
export function useChannel(
channelName: string | undefined
) {
- const { client } = usePusher();
- const [channel, setChannel] = useState();
- useEffect(() => {
- /** Return early if there's no client */
- if (!client) return;
+ const [channel, setChannel] = useState();
+ const { subscribe, unsubscribe } = useChannels();
- /** Return early and warn if there's no channel */
- if (!channelName) {
- console.warn(NO_CHANNEL_NAME_WARNING);
- return;
- }
-
- /** Subscribe to channel and set it in state */
- const pusherChannel = client.subscribe(channelName);
- setChannel(pusherChannel as T);
+ useEffect(() => {
+ if (!channelName || !subscribe || !unsubscribe) return;
- /** Cleanup on unmount/re-render */
- return () => client?.unsubscribe(channelName);
- }, [channelName, client]);
+ const _channel = subscribe(channelName);
+ setChannel(_channel);
+ return () => unsubscribe(channelName);
+ }, [channelName, subscribe, unsubscribe]);
/** Return the channel for use. */
return channel;
diff --git a/src/core/useChannels.tsx b/src/core/useChannels.tsx
new file mode 100644
index 0000000..0d7e551
--- /dev/null
+++ b/src/core/useChannels.tsx
@@ -0,0 +1,19 @@
+import { useContext, useEffect } from "react";
+import { __ChannelsContext } from "./ChannelsProvider";
+import { ChannelsContextValues } from "./types";
+
+/**
+ * Provides access to the channels global provider.
+ */
+
+export function useChannels() {
+ const context = useContext(__ChannelsContext);
+ useEffect(() => {
+ if (!context || !Object.keys(context).length)
+ console.warn(NOT_IN_CONTEXT_WARNING);
+ }, [context]);
+ return context;
+}
+
+const NOT_IN_CONTEXT_WARNING =
+ "No Channels context. Did you forget to wrap your app in a ?";
diff --git a/src/core/useTrigger.ts b/src/core/useTrigger.ts
index 07fbb0d..bf40fad 100644
--- a/src/core/useTrigger.ts
+++ b/src/core/useTrigger.ts
@@ -33,10 +33,13 @@ export function useTrigger(channelName: string) {
(eventName: string, data?: TData) => {
const fetchOptions: RequestInit = {
method: "POST",
- body: JSON.stringify({ channelName, eventName, data })
+ body: JSON.stringify({ channelName, eventName, data }),
};
+ // @ts-expect-error deprecated since 7.1.0, but still supported for backwards compatibility
+ // now it should use channelAuthorization instead
if (client && client.config?.auth) {
+ // @ts-expect-error deprecated
fetchOptions.headers = client.config.auth.headers;
} else {
console.warn(NO_AUTH_HEADERS_WARNING);
diff --git a/src/native/index.ts b/src/native/index.ts
index 4ecf420..cb0cea1 100644
--- a/src/native/index.ts
+++ b/src/native/index.ts
@@ -5,5 +5,7 @@ export * from "../core/usePresenceChannel";
export * from "../core/useEvent";
export * from "../core/useClientTrigger";
export * from "../core/useTrigger";
+export * from "../core/useChannels";
+export * from "../core/ChannelsProvider";
export * from "./PusherProvider";
export { __PusherContext } from "../core/PusherProvider";
diff --git a/src/testUtils.tsx b/src/testUtils.tsx
index cef4da3..1d3dc91 100644
--- a/src/testUtils.tsx
+++ b/src/testUtils.tsx
@@ -4,6 +4,7 @@ import React from "react";
import Pusher from "pusher-js";
import { PusherMock } from "pusher-js-mock";
import { __PusherContext } from "./core/PusherProvider";
+import { ChannelsProvider } from "./core/ChannelsProvider";
/**
* Flushes async promises in mocks
@@ -21,9 +22,9 @@ export async function renderHookWithProvider(
clientConfig: Record = {}
) {
const client = new PusherMock("key", clientConfig) as unknown;
- const wrapper: React.FC = ({ children }) => (
- <__PusherContext.Provider value={{ client: client as Pusher }}>
- {children}
+ const wrapper: React.FC = ({ children, ...props }) => (
+ <__PusherContext.Provider value={{ client: client as Pusher }} {...props}>
+ {children}
);
const result = renderHook(hook, { wrapper });
diff --git a/src/web/index.ts b/src/web/index.ts
index 4ecf420..82151dc 100644
--- a/src/web/index.ts
+++ b/src/web/index.ts
@@ -4,6 +4,8 @@ export * from "../core/useChannel";
export * from "../core/usePresenceChannel";
export * from "../core/useEvent";
export * from "../core/useClientTrigger";
+export * from "../core/useChannels";
export * from "../core/useTrigger";
+export * from "../core/ChannelsProvider";
export * from "./PusherProvider";
export { __PusherContext } from "../core/PusherProvider";
diff --git a/yarn.lock b/yarn.lock
index 6fcb36d..5ebcddd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6309,10 +6309,10 @@ pusher-js-mock@mayteio/pusher-js-mock#feature/presence-channels-release:
version "0.2.1"
resolved "https://codeload.github.com/mayteio/pusher-js-mock/tar.gz/1b426161901b736b729efeb256f573426eae56a2"
-pusher-js@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/pusher-js/-/pusher-js-7.0.0.tgz#9716ae3f79ca7f87a269764f278a8e14494a4e20"
- integrity sha512-2ZSw8msMe6EKNTebQSthRInrWUK9bo3zXPmQx0bfeDFJdSnTWUROhdAhmpRQREHzqrL+l4imv/3uwgIQHUO0oQ==
+pusher-js@^7.3.0:
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/pusher-js/-/pusher-js-7.3.0.tgz#0d4334bc67fb16b599cff7a1464016dfbd6d5658"
+ integrity sha512-N7uFRZGK6PKFfKd8e1aKsQ81OrilATGNhIbj42xJKmK+3zhstGBCQ110ZiF2nIngPTEKxQQqf15SJsVnQfNuqQ==
dependencies:
tweetnacl "^1.0.3"