diff --git a/src/queryClient.ts b/src/queryClient.ts index 1cebd3fa..30cb2cbf 100644 --- a/src/queryClient.ts +++ b/src/queryClient.ts @@ -75,15 +75,14 @@ export default (config: Partial) => { const hooks = configureHooks(queryClient, queryConfig); // set up websocket client and hooks given config - if (queryConfig.enableWebsocket) { - configureWebsockets(queryClient, queryConfig); - } + const ws = (queryConfig.enableWebsocket) ? { ws: configureWebsockets(queryClient, queryConfig) } : {}; // returns the queryClient and relative instances return { queryClient, QueryClientProvider, hooks, + ...ws, useMutation, ReactQueryDevtools, }; diff --git a/src/ws/hooks.ts b/src/ws/hooks.ts index e69de29b..2d0e63b7 100644 --- a/src/ws/hooks.ts +++ b/src/ws/hooks.ts @@ -0,0 +1,103 @@ +/** + * Graasp websocket client + * React effect hooks to subscribe to real-time updates and mutate query client + * + * @author Alexandre CHAU + */ + +import { ServerMessage } from "graasp-websockets/src/interfaces/message"; +import { List } from "immutable"; +import { useEffect } from "react"; +import { QueryClient } from "react-query"; +import { buildItemChildrenKey, buildItemKey, SHARED_ITEMS_KEY } from "../config/keys"; +import { Item, UUID } from "../types"; +import { Channel, GraaspWebsocketClient } from "./ws-client"; + +const ITEM_ENTITY_TYPE = "item" +const MEMBER_ENTITY_TYPE = "member" + +export default (websocketClient: GraaspWebsocketClient, queryClient: QueryClient) => ({ + /** + * React hook to subscribe to the children updates of the give parent item ID + * + * @param parentId The ID of the parent on which to observe children updates + */ + useChildrenUpdates: (parentId: UUID) => { + useEffect(() => { + if (!parentId) { + return; + } + + const channel: Channel = { name: parentId, entity: ITEM_ENTITY_TYPE }; + const parentChildrenKey = buildItemChildrenKey(parentId); + + const handler = (data: ServerMessage) => { + if (data.type === "update" && data.body.kind === "childItem" && data.body.entity === ITEM_ENTITY_TYPE) { + const current: List | undefined = queryClient.getQueryData(parentChildrenKey); + const value = data.body.value; + let mutation; + switch (data.body.op) { + case "create": { + if (current && !current.find(i => i.id === value.id)) { + mutation = current.push(value); + queryClient.setQueryData(parentChildrenKey, mutation); + queryClient.setQueryData(buildItemKey(value.id), value); + } + break; + } + case "delete": { + mutation = current?.filter(i => i.id !== value.id); + queryClient.setQueryData(parentChildrenKey, mutation); + break; + } + } + } + }; + + websocketClient.subscribe(channel, handler); + + return function cleanup() { + websocketClient.unsubscribe(channel, handler); + }; + }, [parentId]); + }, + + useSharedItemsUpdates: (userId: UUID) => { + useEffect(() => { + if (!userId) { + return; + } + + const channel: Channel = { name: userId, entity: MEMBER_ENTITY_TYPE }; + + const handler = (data: ServerMessage) => { + if (data.type === "update" && data.body.kind === "sharedWith" && data.body.entity === MEMBER_ENTITY_TYPE) { + const current: List | undefined = queryClient.getQueryData(SHARED_ITEMS_KEY); + const value = data.body.value; + let mutation; + switch (data.body.op) { + case "create": { + if (current && !current.find(i => i.id === value.id)) { + mutation = current.push(value); + queryClient.setQueryData(SHARED_ITEMS_KEY, mutation); + queryClient.setQueryData(buildItemKey(value.id), value); + } + break; + } + case "delete": { + mutation = current?.filter(i => i.id !== value.id); + queryClient.setQueryData(SHARED_ITEMS_KEY, mutation); + break; + } + } + } + }; + + websocketClient.subscribe(channel, handler); + + return function cleanup() { + websocketClient.unsubscribe(channel, handler); + } + }, [userId]); + }, +}); \ No newline at end of file diff --git a/src/ws/index.ts b/src/ws/index.ts index 0d0a4611..1417a5b9 100644 --- a/src/ws/index.ts +++ b/src/ws/index.ts @@ -1,11 +1,23 @@ +/** + * Graasp websocket client top-level file + * Entry point to use the Graasp WebSocket client in front-end applications + * + * @author Alexandre CHAU + */ + import { QueryClient } from "react-query"; import { QueryClientConfig } from "../types"; +import configureWebsocketHooks from "./hooks"; +import { configureWebsocketClient } from "./ws-client"; -const configureWebsockets = ( +// to be called by the main query client configurator +export default ( queryClient: QueryClient, queryConfig: QueryClientConfig, -) => ({ - -}); +) => { + const websocketClient = configureWebsocketClient(queryConfig); -export default configureWebsockets; \ No newline at end of file + return { + hooks: configureWebsocketHooks(websocketClient, queryClient), + }; +}; \ No newline at end of file diff --git a/src/ws/ws-client.ts b/src/ws/ws-client.ts index 581c2572..29f22cb2 100644 --- a/src/ws/ws-client.ts +++ b/src/ws/ws-client.ts @@ -9,7 +9,7 @@ import { ClientMessage, ServerMessage } from "graasp-websockets/src/interfaces/message"; import { QueryClientConfig } from "../types"; -type Channel = { +export type Channel = { entity: "item" | "member"; name: string, }; @@ -38,7 +38,26 @@ function addToMappedArray(map: Map>, key: S, value: T) { } } -export const configureWebsocketClient = (config: QueryClientConfig) => { +/** + * Websocket client for the graasp-websockets protocol + */ +export interface GraaspWebsocketClient { + /** + * Subscribe a handler to a given channel + * @param channel Channel to which to subscribe to + * @param handler Handler function to register + */ + subscribe(channel: Channel, handler: UpdateHandlerFn): void + + /** + * Unsubscribe a handler from a channel, THE HANDLER MUST === THE ONE PASSED TO SUBSCRIBE + * @param channel Channel from wihch to unsubscribe the provided handler from + * @param handler Handler function to unregster, MUST BE EQUAL (===) TO PREVIOUSLY REGISTERED HANDLE WITH @see subscribe ! + */ + unsubscribe(channel: Channel, handler: UpdateHandlerFn): void +} + +export const configureWebsocketClient = (config: QueryClientConfig): GraaspWebsocketClient => { // native client WebSocket instance const ws = new WebSocket(config.WS_HOST);