From 19898b05907a5fcc36ac32e1618294d08eaabce7 Mon Sep 17 00:00:00 2001 From: Guillaume FORTAINE Date: Wed, 14 Dec 2022 09:42:17 +0100 Subject: [PATCH] [web-pubsub-client/react] Add React hook --- common/config/rush/pnpm-lock.yaml | 31 +++- sdk/web-pubsub/web-pubsub-client/package.json | 21 ++- .../review/web-pubsub-client.api.md | 16 ++ .../src/frameworks/react/index.ts | 1 + .../frameworks/react/useWebPubSubClient.ts | 138 ++++++++++++++++++ .../web-pubsub-client/src/webPubSubClient.ts | 4 +- 6 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 sdk/web-pubsub/web-pubsub-client/src/frameworks/react/index.ts create mode 100644 sdk/web-pubsub/web-pubsub-client/src/frameworks/react/useWebPubSubClient.ts diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 26d12ca9cb7e..d3c91302ec93 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -2396,6 +2396,10 @@ packages: resolution: {integrity: sha512-rt2GvuoXcYb+R4X8SF4jlTSXDWoUmkZf7OB8iTRRfE5dmqHn47rY8CRIEPDD5lY28cU86+xXYQ5RsXq/9nydvQ==} dev: false + /@types/prop-types/15.7.5: + resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + dev: false + /@types/qs/6.9.7: resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} dev: false @@ -2404,12 +2408,24 @@ packages: resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} dev: false + /@types/react/18.0.26: + resolution: {integrity: sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==} + dependencies: + '@types/prop-types': 15.7.5 + '@types/scheduler': 0.16.2 + csstype: 3.1.1 + dev: false + /@types/resolve/1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: '@types/node': 18.11.9 dev: false + /@types/scheduler/0.16.2: + resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} + dev: false + /@types/semaphore/1.1.1: resolution: {integrity: sha512-jmFpMslMtBGOXY2s7x6O8vBebcj6zhkwl0Pd/viZApo1uZaPk733P8doPvaiBbCG+R7201OLOl4QP7l1mFyuyw==} dev: false @@ -3515,6 +3531,10 @@ packages: resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} dev: false + /csstype/3.1.1: + resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} + dev: false + /csv-parse/5.3.3: resolution: {integrity: sha512-kEWkAPleNEdhFNkHQpFHu9RYPogsFj3dx6bCxL847fsiLgidzWg0z/O0B1kVWMJUc5ky64zGp18LX2T3DQrOfw==} dev: false @@ -3736,7 +3756,7 @@ packages: dependencies: semver: 7.3.8 shelljs: 0.8.5 - typescript: 5.0.0-dev.20221222 + typescript: 5.0.0-dev.20221223 dev: false /downlevel-dts/0.7.0: @@ -8440,8 +8460,8 @@ packages: hasBin: true dev: false - /typescript/5.0.0-dev.20221222: - resolution: {integrity: sha512-9YjbBpzM8BrO3WcbFEE63qQtipPlBP+WFmQDpI/j1W1DInACLJESbWsVAeYBC6PUvIBMzKIVO33cJUUv2l/pmw==} + /typescript/5.0.0-dev.20221223: + resolution: {integrity: sha512-wdv6oGuCZ1Uhp0VWufyM2PmVOhUNtg8VAT3NeAsYT8m4D0ZEe1+oZXnJaPmv8icwmKFrzTF7eKaX543gC5by5g==} engines: {node: '>=4.2.0'} hasBin: true dev: false @@ -8565,7 +8585,7 @@ packages: dev: false /utils-merge/1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + resolution: {integrity: sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=} engines: {node: '>= 0.4.0'} dev: false @@ -19487,7 +19507,7 @@ packages: dev: false file:projects/web-pubsub-client.tgz: - resolution: {integrity: sha512-6PPAGRU137SPwjZ7bhod+glarcGPxbbvYE8M65zJ/mWIRTjiDqCuSrpfd5u72Q8ztPOGSXCpUdikAMi4N+Av4w==, tarball: file:projects/web-pubsub-client.tgz} + resolution: {integrity: sha512-QVwFrpq0gyaGiUXVjR6uVjUJbcTfxDiz7SRdMGXlLQBKPmIZIBL/WCWFJICEALVu3EZCMQL8Ft37xbIkG4nPOA==, tarball: file:projects/web-pubsub-client.tgz} name: '@rush-temp/web-pubsub-client' version: 0.0.0 dependencies: @@ -19501,6 +19521,7 @@ packages: '@types/jsonwebtoken': 8.5.9 '@types/mocha': 7.0.2 '@types/node': 12.20.55 + '@types/react': 18.0.26 '@types/sinon': 9.0.11 '@types/ws': 7.4.7 buffer: 6.0.3 diff --git a/sdk/web-pubsub/web-pubsub-client/package.json b/sdk/web-pubsub/web-pubsub-client/package.json index f8cf50b16253..dd45502a20aa 100644 --- a/sdk/web-pubsub/web-pubsub-client/package.json +++ b/sdk/web-pubsub/web-pubsub-client/package.json @@ -10,6 +10,24 @@ "ws": "./dist-esm/src/ws.browser.js", "./dist-esm/src/websocket/websocketClient.js": "./dist-esm/src/websocket/websocketClient.browser.js" }, + "exports": { + ".": { + "types": "./types/web-pubsub-client.d.ts", + "import": "./dist-esm/src/index.js", + "require": "./dist/index.js" + }, + "./react": { + "types": "./types/src/frameworks/react/index.d.ts", + "import": "./dist-esm/src/frameworks/react/index.js" + } + }, + "typesVersions": { + "*": { + "react": [ + "types/src/frameworks/react/index.d.ts" + ] + } + }, "types": "types/web-pubsub-client.d.ts", "scripts": { "audit": "node ../../../common/scripts/rush-audit.js && rimraf node_modules package-lock.json && npm i --package-lock-only 2>&1 && npm audit", @@ -20,7 +38,7 @@ "build": "npm run clean && tsc -p . && dev-tool run bundle && api-extractor run --local", "check-format": "prettier --list-different --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", "format": "prettier --write --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", - "clean": "rimraf dist dist-esm test-dist temp types *.tgz *.log", + "clean": "rimraf dist dist-esm dist-test temp types *.tgz *.log", "execute:samples": "dev-tool samples run samples-dev", "extract-api": "tsc -p . && api-extractor run --local", "integration-test:browser": "echo skipped", @@ -79,6 +97,7 @@ "@types/jsonwebtoken": "~8.5.0", "@types/mocha": "^7.0.2", "@types/node": "^12.0.0", + "@types/react": "^18.0.26", "@types/sinon": "^9.0.4", "chai": "^4.2.0", "cross-env": "^7.0.2", diff --git a/sdk/web-pubsub/web-pubsub-client/review/web-pubsub-client.api.md b/sdk/web-pubsub/web-pubsub-client/review/web-pubsub-client.api.md index 27cb74a3a259..556e206a7231 100644 --- a/sdk/web-pubsub/web-pubsub-client/review/web-pubsub-client.api.md +++ b/sdk/web-pubsub/web-pubsub-client/review/web-pubsub-client.api.md @@ -249,6 +249,8 @@ export class WebPubSubClient { sendEvent(eventName: string, content: JSONTypes | ArrayBuffer, dataType: WebPubSubDataType, options?: SendEventOptions): Promise; sendToGroup(groupName: string, content: JSONTypes | ArrayBuffer, dataType: WebPubSubDataType, options?: SendToGroupOptions): Promise; start(options?: StartOptions): Promise; + // (undocumented) + _state: WebPubSubClientState; stop(): void; } @@ -274,6 +276,20 @@ export interface WebPubSubClientProtocol { writeMessage(message: WebPubSubMessage): string | ArrayBuffer; } +// @public (undocumented) +export enum WebPubSubClientState { + // (undocumented) + Connected = "Connected", + // (undocumented) + Connecting = "Connecting", + // (undocumented) + Disconnected = "Disconnected", + // (undocumented) + Recovering = "Recovering", + // (undocumented) + Stopped = "Stopped" +} + // @public export type WebPubSubDataType = /** diff --git a/sdk/web-pubsub/web-pubsub-client/src/frameworks/react/index.ts b/sdk/web-pubsub/web-pubsub-client/src/frameworks/react/index.ts new file mode 100644 index 000000000000..4c2c69ba40e2 --- /dev/null +++ b/sdk/web-pubsub/web-pubsub-client/src/frameworks/react/index.ts @@ -0,0 +1 @@ +export { useWebPubSubClient } from "./useWebPubSubClient"; diff --git a/sdk/web-pubsub/web-pubsub-client/src/frameworks/react/useWebPubSubClient.ts b/sdk/web-pubsub/web-pubsub-client/src/frameworks/react/useWebPubSubClient.ts new file mode 100644 index 000000000000..cf3cc0c96cdd --- /dev/null +++ b/sdk/web-pubsub/web-pubsub-client/src/frameworks/react/useWebPubSubClient.ts @@ -0,0 +1,138 @@ +import { + GroupDataMessage, + JSONTypes, + OnConnectedArgs, + OnDisconnectedArgs, + OnGroupDataMessageArgs, + OnRestoreGroupFailedArgs as OnRejoinGroupFailedArgs, + OnServerDataMessageArgs, + OnStoppedArgs, + SendToGroupOptions, + ServerDataMessage, + WebPubSubClient, + WebPubSubClientProtocol, + WebPubSubClientState, + WebPubSubDataType, + WebPubSubResult, + WebPubSubRetryOptions, +} from "@azure/web-pubsub-client"; +import { useEffect, useRef, useState } from "react"; + +export interface Options { + groupName: string; + protocol?: WebPubSubClientProtocol; + autoReconnect?: boolean; + autoRestoreGroups?: boolean; + messageRetryOptions?: WebPubSubRetryOptions; + reconnectRetryOptions?: WebPubSubRetryOptions; + manual?: boolean; + onConnected?: (args: OnConnectedArgs) => void; + onDisconnected?: (args: OnDisconnectedArgs) => void; + onStopped?: (args: OnStoppedArgs) => void; + onServerMessage?: (args: OnServerDataMessageArgs) => void; + onGroupMessage?: (args: OnGroupDataMessageArgs) => void; + onRejoinGroupFailed?: (args: OnRejoinGroupFailedArgs) => void; +} + +export function useWebPubSubClient( + url: string, + { + groupName, + protocol, + autoReconnect, + autoRestoreGroups, + messageRetryOptions, + reconnectRetryOptions, + manual = false, + onConnected, + onDisconnected, + onStopped, + onServerMessage, + onGroupMessage, + onRejoinGroupFailed, + }: Options +) { + const webpubsubclientRef = useRef(null); + const [latestGroupMessage, setLatestGroupMessage] = useState(); + const [latestServerMessage, setLatestServerMessage] = useState(); + const [connectionStatus, setConnectionStatus] = useState(WebPubSubClientState.Stopped); + + async function startWebSocket() { + webpubsubclientRef.current = new WebPubSubClient(url, { + protocol, + autoReconnect, + autoRestoreGroups, + messageRetryOptions, + reconnectRetryOptions, + }); + + webpubsubclientRef.current.on("connected", (e: OnConnectedArgs) => { + if (typeof onConnected === "function") onConnected(e); + setConnectionStatus(webpubsubclientRef.current?._state!); + }); + + webpubsubclientRef.current.on("disconnected", (e: OnDisconnectedArgs) => { + if (typeof onDisconnected === "function") onDisconnected(e); + setConnectionStatus(webpubsubclientRef.current?._state!); + }); + + webpubsubclientRef.current.on("stopped", (e: OnStoppedArgs) => { + if (typeof onStopped === "function") onStopped(e); + setConnectionStatus(webpubsubclientRef.current?._state!); + }); + + webpubsubclientRef.current.on("server-message", (e: OnServerDataMessageArgs) => { + if (typeof onServerMessage === "function") onServerMessage(e); + setLatestServerMessage(e.message); + }); + + webpubsubclientRef.current.on("group-message", (e: OnGroupDataMessageArgs) => { + if (typeof onGroupMessage === "function") onGroupMessage(e); + setLatestGroupMessage(e.message); + }); + + webpubsubclientRef.current.on("rejoin-group-failed", (e: OnRejoinGroupFailedArgs) => { + if (typeof onRejoinGroupFailed === "function") onRejoinGroupFailed(e); + }); + + await webpubsubclientRef.current.start(); + await webpubsubclientRef.current.joinGroup(groupName); + } + + const sendMessage = async ( + groupName: string, + content: JSONTypes | ArrayBuffer, + dataType: WebPubSubDataType, + options?: SendToGroupOptions + ): Promise => { + await webpubsubclientRef.current?.sendToGroup(groupName, content, dataType, options); + }; + + const connect = () => { + startWebSocket(); + + setConnectionStatus(webpubsubclientRef.current?._state!); + }; + + const disconnect = () => webpubsubclientRef.current?.stop(); + + useEffect(() => { + if (!manual) { + connect(); + } + + return () => { + disconnect(); + }; + }, [manual]); + + return { + latestGroupMessage, + latestServerMessage, + sendMessage, + connect, + disconnect, + connectionStatus, + ws: webpubsubclientRef.current, + }; +} diff --git a/sdk/web-pubsub/web-pubsub-client/src/webPubSubClient.ts b/sdk/web-pubsub/web-pubsub-client/src/webPubSubClient.ts index f8d29d16272c..7722df780ce4 100644 --- a/sdk/web-pubsub/web-pubsub-client/src/webPubSubClient.ts +++ b/sdk/web-pubsub/web-pubsub-client/src/webPubSubClient.ts @@ -42,7 +42,7 @@ import { WebPubSubClientCredential } from "./webPubSubClientCredential"; import { WebSocketClientFactory } from "./websocket/websocketClient"; import { WebSocketClientFactoryLike, WebSocketClientLike } from "./websocket/websocketClientLike"; -enum WebPubSubClientState { +export enum WebPubSubClientState { Stopped = "Stopped", Disconnected = "Disconnected", Connecting = "Connecting", @@ -69,7 +69,7 @@ export class WebPubSubClient { private readonly _reconnectRetryPolicy: RetryPolicy; private readonly _emitter: EventEmitter = new EventEmitter(); - private _state: WebPubSubClientState; + public _state: WebPubSubClientState; private _isStopping: boolean = false; private _ackId: number;