From f2d003c3055b0afb1d12c6348b9537c9bb780ec1 Mon Sep 17 00:00:00 2001 From: Jaden Date: Sat, 12 Aug 2023 01:08:41 -0700 Subject: [PATCH] Initial commit --- .gitattributes | 2 + .gitignore | 1 + index.ts | 0 package-lock.json | 93 +++++++++++++++++++++++++++++++++++++++++ package.json | 21 ++++++++++ src/PhoenixProvider.tsx | 58 +++++++++++++++++++++++++ src/useChannel.ts | 47 +++++++++++++++++++++ 7 files changed, 222 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 index.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/PhoenixProvider.tsx create mode 100644 src/useChannel.ts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..15b5744 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,93 @@ +{ + "name": "use-phoenix", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "use-phoenix", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "phoenix": "^1.7.7" + }, + "devDependencies": { + "@types/phoenix": "^1.6.0", + "@types/react": "^18.2.20" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.0.tgz", + "integrity": "sha512-qwfpsHmFuhAS/dVd4uBIraMxRd56vwBUYQGZ6GpXnFuM2XMRFJbIyruFKKlW2daQliuYZwe0qfn/UjFCDKic5g==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.20", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz", + "integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, + "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==", + "peer": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/phoenix": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/phoenix/-/phoenix-1.7.7.tgz", + "integrity": "sha512-moAN6e4Z16x/x1nswUpnTR2v5gm7HsI7eluZ2YnYUUsBNzi3cY/5frmiJfXIEi877IQAafzTfp8hd6vEUMme+w==" + }, + "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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f5750c8 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "use-phoenix", + "version": "0.0.1", + "description": "React hooks for the Phoenix Framework", + "main": "index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Jaden Moore", + "license": "MIT", + "dependencies": { + "phoenix": "^1.7.7" + }, + "peerDependencies": { + "react": "^18.0.0" + }, + "devDependencies": { + "@types/phoenix": "^1.6.0", + "@types/react": "^18.2.20" + } +} diff --git a/src/PhoenixProvider.tsx b/src/PhoenixProvider.tsx new file mode 100644 index 0000000..a2dd91f --- /dev/null +++ b/src/PhoenixProvider.tsx @@ -0,0 +1,58 @@ +import React, { useEffect } from "react"; +import { Socket, SocketConnectOption } from "phoenix"; + +const PhoenixContext = React.createContext(null); + +export const useSocket = () => React.useContext(PhoenixContext); + +export type SocketProviderProps = { + children?: React.ReactNode; + options?: SocketConnectOption; + url: string; + connect?: boolean; + onOpen?: () => void; + onClose?: () => void; + onError?: () => void; +}; + +export function PhoenixProvider(props: SocketProviderProps) { + const { children, connect, url, options } = props; + const socketRef = React.useRef(null); + + function closeSocket() { + socketRef.current?.disconnect(); + socketRef.current = null; + } + + useEffect(() => { + const { onOpen, onClose, onError } = props; + if (socketRef.current) { + closeSocket(); + } + + if (!connect) return; + + const socket = new Socket(url, options); + socket.connect(); + socketRef.current = socket; + + if (onOpen) socket.onOpen(onOpen); + if (onClose) socket.onClose(onClose); + if (onError) socket.onError(onError); + + return () => { + closeSocket(); + }; + }, [connect, url, options]); + + return {children}; +}; + +PhoenixProvider.defaultProps = { + options: {}, + onOpen: null, + onClose: null, + onError: null, + connect: true, + children: null, +}; diff --git a/src/useChannel.ts b/src/useChannel.ts new file mode 100644 index 0000000..de32755 --- /dev/null +++ b/src/useChannel.ts @@ -0,0 +1,47 @@ +import { Channel, Push } from "phoenix"; +import { useSocket } from "./PhoenixProvider"; +import { useEffect, useRef } from "react"; + +type PushFunction = }>( + event: PushAction["type"], + payload: PushAction["payload"] +) => Promise; + +export function useChannel = {}, JoinPayload = null>( + topic: string, + options?: { params?: Params; onJoin?: (payload: JoinPayload) => void } +): [Channel | null, PushFunction] { + const { params, onJoin } = options || {}; + const socket = useSocket(); + const channelRef = useRef(null); + const onJoinRef = useRef(onJoin); + + onJoinRef.current = onJoin; + + useEffect(() => { + if (socket === null) return; + + const channel = socket.channel(topic, params); + channel.join().receive("ok", (payload) => onJoinRef.current && onJoinRef.current(payload)); + channelRef.current = channel; + + return () => { + channel.leave(); + channelRef.current = null; + }; + }, [socket, topic, params]); + + const push: PushFunction = (event, payload) => { + return pushPromise(channelRef.current?.push(event, payload ?? {})); + }; + + return [channelRef.current, push]; +} + +const pushPromise = (push: Push | undefined): Promise => + new Promise((resolve, reject) => { + if (!push) return reject("no push"); + + push.receive("ok", resolve).receive("error", reject); + // .receive('timeout', reject('timeout')); + });