From e836e7c7e0913b624b07efa8971b907228754ed0 Mon Sep 17 00:00:00 2001 From: Alexandre Chau Date: Tue, 3 Aug 2021 17:42:56 +0200 Subject: [PATCH] feat: refactor websocket client, add new real-time behaviours --- package.json | 1 - src/api/chat.ts | 30 +++ src/api/index.ts | 1 + src/api/routes.ts | 3 + src/config/keys.ts | 3 + src/hooks/chat.ts | 24 +++ src/hooks/index.ts | 2 + src/mutations/chat.ts | 12 ++ src/mutations/index.ts | 2 + src/types.ts | 17 ++ src/ws/constants.ts | 27 +++ src/ws/hooks.ts | 134 ------------ src/ws/hooks/chat.ts | 54 +++++ src/ws/hooks/index.ts | 16 ++ src/ws/hooks/item.ts | 245 ++++++++++++++++++++++ src/ws/hooks/membership.ts | 73 +++++++ src/ws/index.ts | 2 - src/ws/protocol.ts | 102 +++++++++ src/ws/ws-client.ts | 209 ++++++++----------- yarn.lock | 411 ++----------------------------------- 20 files changed, 717 insertions(+), 651 deletions(-) create mode 100644 src/api/chat.ts create mode 100644 src/hooks/chat.ts create mode 100644 src/mutations/chat.ts create mode 100644 src/ws/constants.ts delete mode 100644 src/ws/hooks.ts create mode 100644 src/ws/hooks/chat.ts create mode 100644 src/ws/hooks/index.ts create mode 100644 src/ws/hooks/item.ts create mode 100644 src/ws/hooks/membership.ts create mode 100644 src/ws/protocol.ts diff --git a/package.json b/package.json index 3e4cd7b4..15d3c71a 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@babel/preset-typescript": "7.14.5", "@commitlint/cli": "12.1.4", "@commitlint/config-conventional": "12.1.4", - "@graasp/websockets": "git://github.com/graasp/graasp-websockets.git#master", "@testing-library/jest-dom": "5.11.6", "@testing-library/react": "11.2.2", "@testing-library/react-hooks": "7.0.0", diff --git a/src/api/chat.ts b/src/api/chat.ts new file mode 100644 index 00000000..5b7567a9 --- /dev/null +++ b/src/api/chat.ts @@ -0,0 +1,30 @@ +import { PartialChatMessage, QueryClientConfig, UUID } from '../types'; +import { buildGetItemChatRoute, buildPostItemChatMessageRoute } from './routes'; +import { DEFAULT_GET, DEFAULT_POST, failOnError } from './utils'; + +export const getItemChat = async ( + id: UUID, + { API_HOST }: QueryClientConfig, +) => { + const res = await fetch( + `${API_HOST}/${buildGetItemChatRoute(id)}`, + DEFAULT_GET, + ).then(failOnError); + const itemChat = await res.json(); + return itemChat; +}; + +export const postItemChatMessage = async ( + { chatId, body }: PartialChatMessage, + { API_HOST }: QueryClientConfig, +) => { + const res = await fetch( + `${API_HOST}/${buildPostItemChatMessageRoute(chatId)}`, + { + ...DEFAULT_POST, + body: JSON.stringify({ body }), + }, + ).then(failOnError); + const publishedMessage = await res.json(); + return publishedMessage; +}; diff --git a/src/api/index.ts b/src/api/index.ts index d56d9e0b..cbc6efe1 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -5,3 +5,4 @@ export * from './authentication'; export * from './itemTag'; export * from './itemLogin'; export * from './itemFlag'; +export * from './chat'; \ No newline at end of file diff --git a/src/api/routes.ts b/src/api/routes.ts index 9b739634..fd941aed 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -37,6 +37,9 @@ export const buildShareItemWithRoute = (id: UUID) => `item-memberships?itemId=${id}`; export const buildGetItemMembershipsForItemRoute = (id: UUID) => `item-memberships?itemId=${id}`; +export const buildGetItemChatRoute = (id: UUID) => `${ITEMS_ROUTE}/${id}/chat`; +export const buildPostItemChatMessageRoute = (id: UUID) => + `${ITEMS_ROUTE}/${id}/chat`; export const buildGetMemberBy = (email: string) => `${MEMBERS_ROUTE}?email=${email}`; diff --git a/src/config/keys.ts b/src/config/keys.ts index 979d6a29..919413bc 100644 --- a/src/config/keys.ts +++ b/src/config/keys.ts @@ -11,6 +11,8 @@ export const CURRENT_MEMBER_KEY = 'currentMember'; export const MEMBERS_KEY = 'members'; export const buildMemberKey = (id: UUID) => [MEMBERS_KEY, id]; export const buildItemParentsKey = (id: UUID) => [ITEMS_KEY, id, 'parents']; +export const CHATS_KEY = 'chats'; +export const buildItemChatKey = (id: UUID) => [CHATS_KEY, id]; export const getKeyForParentId = (parentId: UUID | null) => parentId ? buildItemChildrenKey(parentId) : OWN_ITEMS_KEY; @@ -48,4 +50,5 @@ export const MUTATION_KEYS = { POST_ITEM_FLAG: 'postItemFlag', EDIT_ITEM_MEMBERSHIP: 'editItemMembership', DELETE_ITEM_MEMBERSHIP: 'deleteItemMembership', + POST_ITEM_CHAT_MESSAGE: 'postChatMessage', }; diff --git a/src/hooks/chat.ts b/src/hooks/chat.ts new file mode 100644 index 00000000..aedc9564 --- /dev/null +++ b/src/hooks/chat.ts @@ -0,0 +1,24 @@ +import { useQuery } from 'react-query'; +import * as Api from '../api'; +import { buildItemChatKey } from '../config/keys'; +import { QueryClientConfig, UUID } from '../types'; + +export default (queryConfig: QueryClientConfig) => { + const { retry, cacheTime, staleTime } = queryConfig; + const defaultOptions = { + retry, + cacheTime, + staleTime, + }; + + return { + useItemChat: (itemId: UUID) => + useQuery({ + queryKey: buildItemChatKey(itemId), + queryFn: () => + Api.getItemChat(itemId, queryConfig).then((data) => data), + ...defaultOptions, + enabled: Boolean(itemId), + }), + }; +}; \ No newline at end of file diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 5d2ebfd0..fa52c748 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -3,6 +3,7 @@ import configureItemHooks from './item'; import configureMemberHooks from './member'; import configureItemTagHooks from './itemTag'; import configureItemFlagHooks from './itemFlag'; +import configureChatHooks from './chat'; import { QueryClientConfig } from '../types'; export default (queryClient: QueryClient, queryConfig: QueryClientConfig) => ({ @@ -10,4 +11,5 @@ export default (queryClient: QueryClient, queryConfig: QueryClientConfig) => ({ ...configureMemberHooks(queryConfig), ...configureItemTagHooks(queryConfig), ...configureItemFlagHooks(queryConfig), + ...configureChatHooks(queryConfig), }); diff --git a/src/mutations/chat.ts b/src/mutations/chat.ts new file mode 100644 index 00000000..e4bba1db --- /dev/null +++ b/src/mutations/chat.ts @@ -0,0 +1,12 @@ +import { QueryClient } from 'react-query'; +import * as Api from '../api' +import { MUTATION_KEYS } from '../config/keys'; +import { QueryClientConfig } from '../types'; + +const { POST_ITEM_CHAT_MESSAGE } = MUTATION_KEYS; + +export default (queryClient: QueryClient, queryConfig: QueryClientConfig) => { + queryClient.setMutationDefaults(POST_ITEM_CHAT_MESSAGE, { + mutationFn: (chatMsg) => Api.postItemChatMessage(chatMsg, queryConfig), + }); +}; \ No newline at end of file diff --git a/src/mutations/index.ts b/src/mutations/index.ts index 76fbfbcf..160f0e80 100644 --- a/src/mutations/index.ts +++ b/src/mutations/index.ts @@ -4,6 +4,7 @@ import memberMutations from './member'; import tagsMutations from './itemTag'; import flagsMutations from './itemFlag'; import itemMembershipMutations from './membership'; +import chatMutations from './chat'; import { QueryClientConfig } from '../types'; const configureMutations = ( @@ -15,6 +16,7 @@ const configureMutations = ( memberMutations(queryClient, queryConfig); tagsMutations(queryClient, queryConfig); flagsMutations(queryClient, queryConfig); + chatMutations(queryClient, queryConfig); }; export default configureMutations; diff --git a/src/types.ts b/src/types.ts index a15bf42e..ce6b18ba 100644 --- a/src/types.ts +++ b/src/types.ts @@ -77,3 +77,20 @@ export enum PERMISSION_LEVELS { WRITE = 'write', ADMIN = 'admin', } + +export type PartialChatMessage = { + chatId: string; + body: string; +}; + +export type ChatMessage = { + chatId: string; + creator: string; + createdAt: string; + body: string; +}; + +export interface Chat { + id: string; + messages: Array; +} diff --git a/src/ws/constants.ts b/src/ws/constants.ts new file mode 100644 index 00000000..1ba5e7ff --- /dev/null +++ b/src/ws/constants.ts @@ -0,0 +1,27 @@ +/** + * TODO: use types from graasp-websockets + */ + +/** Namespace for notifications realm */ +export const REALM_NOTIF = 'notif'; + +/** Client actions */ +export const CLIENT_ACTION_SUBSCRIBE = 'subscribe'; +export const CLIENT_ACTION_UNSUBSCRIBE = 'unsubscribe'; +export const CLIENT_ACTION_SUBSCRIBE_ONLY = 'subscribeOnly'; +export const CLIENT_ACTION_DISCONNECT = 'disconnect'; + +/** Server message types */ +export const SERVER_TYPE_RESPONSE = 'response'; +export const SERVER_TYPE_UPDATE = 'update'; +export const SERVER_TYPE_INFO = 'info'; + +/** Server response status */ +export const RESPONSE_STATUS_SUCCESS = 'success'; +export const RESPONSE_STATUS_ERROR = 'error'; + +/** Error names */ +export const ERROR_ACCESS_DENIED = 'ACCESS_DENIED'; +export const ERROR_BAD_REQUEST = 'BAD_REQUEST'; +export const ERROR_NOT_FOUND = 'NOT_FOUND'; +export const ERROR_SERVER_ERROR = 'SERVER_ERROR'; diff --git a/src/ws/hooks.ts b/src/ws/hooks.ts deleted file mode 100644 index fff85f1b..00000000 --- a/src/ws/hooks.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Graasp websocket client - * React effect hooks to subscribe to real-time updates and mutate query client - * - * @author Alexandre CHAU - */ - -import { - WS_ENTITY_ITEM, - WS_ENTITY_MEMBER, - WS_SERVER_TYPE_UPDATE, - WS_UPDATE_KIND_CHILD_ITEM, - WS_UPDATE_KIND_SHARED_WITH, - WS_UPDATE_OP_CREATE, - WS_UPDATE_OP_DELETE, -} from '@graasp/websockets/src/interfaces/constants'; -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'; - -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: WS_ENTITY_ITEM }; - const parentChildrenKey = buildItemChildrenKey(parentId); - - const handler = (data: ServerMessage) => { - if ( - data.type === WS_SERVER_TYPE_UPDATE && - data.body.kind === WS_UPDATE_KIND_CHILD_ITEM && - data.body.entity === WS_ENTITY_ITEM - ) { - const current: List | undefined = queryClient.getQueryData( - parentChildrenKey, - ); - const value = data.body.value as Item; - let mutation; - switch (data.body.op) { - case WS_UPDATE_OP_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 WS_UPDATE_OP_DELETE: { - if (current) { - mutation = current.filter((i) => i.id !== value.id); - queryClient.setQueryData(parentChildrenKey, mutation); - } - break; - } - default: - 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: WS_ENTITY_MEMBER }; - - const handler = (data: ServerMessage) => { - if ( - data.type === WS_SERVER_TYPE_UPDATE && - data.body.kind === WS_UPDATE_KIND_SHARED_WITH && - data.body.entity === WS_ENTITY_MEMBER - ) { - const current: List | undefined = queryClient.getQueryData( - SHARED_ITEMS_KEY, - ); - const value = data.body.value as Item; - let mutation; - switch (data.body.op) { - case WS_UPDATE_OP_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 WS_UPDATE_OP_DELETE: { - if (current) { - 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]); - }, -}); diff --git a/src/ws/hooks/chat.ts b/src/ws/hooks/chat.ts new file mode 100644 index 00000000..88af95e9 --- /dev/null +++ b/src/ws/hooks/chat.ts @@ -0,0 +1,54 @@ +import { QueryClient } from 'react-query'; +import { buildItemChatKey } from '../../config/keys'; +import { Chat, ChatMessage, UUID } from '../../types'; +import { Channel, GraaspWebsocketClient } from '../ws-client'; + +// todo: use graasp-types? +interface ChatEvent { + kind: string; + op: string; + message: ChatMessage; +} + +export default ( + websocketClient: GraaspWebsocketClient, + queryClient: QueryClient, +) => ({ + /** + * React hook to subscribe to the updates of the given chat ID + * @param chatId The ID of the chat of which to observves updates + */ + useItemChatUpdates: (chatId: UUID) => { + if (!chatId) { + return; + } + + const channel: Channel = { name: chatId, topic: 'chat/item' }; + + const handler = (event: ChatEvent) => { + if (event.kind === 'item') { + const chatKey = buildItemChatKey(chatId); + const current: Chat | undefined = queryClient.getQueryData(chatKey); + + if (current) { + switch (event.op) { + case 'publish': { + const msg = event.message; + const newChat = Object.assign({}, current); + newChat.messages = [...current.messages]; + newChat.messages.push(msg); + queryClient.setQueryData(chatKey, newChat); + break; + } + } + } + } + }; + + websocketClient.subscribe(channel, handler); + + return function cleanup() { + websocketClient.unsubscribe(channel, handler); + }; + }, +}); diff --git a/src/ws/hooks/index.ts b/src/ws/hooks/index.ts new file mode 100644 index 00000000..6f3ae14d --- /dev/null +++ b/src/ws/hooks/index.ts @@ -0,0 +1,16 @@ +import { QueryClient } from 'react-query'; +import { GraaspWebsocketClient } from '../ws-client'; +import configureChatHooks from './chat'; +import configureItemHooks from './item'; +import configureMembershipHooks from './membership'; + +export default ( + websocketClient: GraaspWebsocketClient, + queryClient: QueryClient, +) => { + return { + ...configureItemHooks(websocketClient, queryClient), + ...configureMembershipHooks(websocketClient, queryClient), + ...configureChatHooks(websocketClient, queryClient), + }; +}; diff --git a/src/ws/hooks/item.ts b/src/ws/hooks/item.ts new file mode 100644 index 00000000..0ac6d6e9 --- /dev/null +++ b/src/ws/hooks/item.ts @@ -0,0 +1,245 @@ +/** + * Graasp websocket client + * React effect hooks to subscribe to real-time updates and mutate query client + */ + +import { List, Record } from 'immutable'; +import { useEffect } from 'react'; +import { QueryClient } from 'react-query'; +import { + buildItemChildrenKey, + buildItemKey, + OWN_ITEMS_KEY, + SHARED_ITEMS_KEY, +} from '../../config/keys'; +import { Item, UUID } from '../../types'; +import { Channel, GraaspWebsocketClient } from '../ws-client'; + +// TODO: use graasp-types? +interface ItemEvent { + kind: string; + op: string; + item: Item; +} + +export default ( + websocketClient: GraaspWebsocketClient, + queryClient: QueryClient, +) => ({ + /** + * React hook to subscribe to the updates of the given item ID + * @param itemId The ID of the item of which to observe updates + */ + useItemUpdates: (itemId: UUID) => { + useEffect(() => { + if (!itemId) { + return; + } + + const channel: Channel = { name: itemId, topic: 'item' }; + const itemKey = buildItemKey(itemId); + + const handler = (event: ItemEvent) => { + if (event.kind === 'self') { + const current: Record | undefined = queryClient.getQueryData( + itemKey, + ); + const item = event.item; + + if (current?.get('id') === item.id) { + switch (event.op) { + case 'update': { + queryClient.setQueryData(itemKey, item); + break; + } + case 'delete': { + queryClient.setQueryData(itemKey, null); + break; + } + } + } + } + }; + + websocketClient.subscribe(channel, handler); + + return function cleanup() { + websocketClient.unsubscribe(channel, handler); + }; + }, [itemId]); + }, + + /** + * React hook to subscribe to the children updates of the given 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, topic: 'item' }; + const parentChildrenKey = buildItemChildrenKey(parentId); + + const handler = (event: ItemEvent) => { + if (event.kind === 'child') { + const current: List | undefined = queryClient.getQueryData( + parentChildrenKey, + ); + + if (current) { + const item = event.item; + let mutation; + + switch (event.op) { + case 'create': { + if (!current.find((i) => i.id === item.id)) { + mutation = current.push(item); + queryClient.setQueryData(parentChildrenKey, mutation); + queryClient.setQueryData(buildItemKey(item.id), item); + } + break; + } + case 'update': { + // replace value if it exists + mutation = current.map((i) => (i.id === item.id ? item : i)); + queryClient.setQueryData(parentChildrenKey, mutation); + queryClient.setQueryData(buildItemKey(item.id), item); + + break; + } + case 'delete': { + mutation = current.filter((i) => i.id !== item.id); + queryClient.setQueryData(parentChildrenKey, mutation); + break; + } + } + } + } + }; + + websocketClient.subscribe(channel, handler); + + return function cleanup() { + websocketClient.unsubscribe(channel, handler); + }; + }, [parentId]); + }, + + /** + * React hook to subscribe to the owned items updates of the given user ID + * @param userId The ID of the user on which to observe owned items updates + */ + useOwnItemsUpdates: (userId: UUID) => { + useEffect(() => { + if (!userId) { + return; + } + + const channel: Channel = { name: userId, topic: 'item/member' }; + + const handler = (event: ItemEvent) => { + if (event.kind === 'own') { + const current: List | undefined = queryClient.getQueryData( + OWN_ITEMS_KEY, + ); + + if (current) { + const item = event.item; + let mutation; + + switch (event.op) { + case 'create': { + if (!current.find((i) => i.id === item.id)) { + mutation = current.push(item); + queryClient.setQueryData(OWN_ITEMS_KEY, mutation); + queryClient.setQueryData(buildItemKey(item.id), item); + } + break; + } + case 'update': { + // replace value if it exists + mutation = current.map((i) => (i.id === item.id ? item : i)); + queryClient.setQueryData(OWN_ITEMS_KEY, mutation); + queryClient.setQueryData(buildItemKey(item.id), item); + + break; + } + case 'delete': { + mutation = current.filter((i) => i.id !== item.id); + queryClient.setQueryData(OWN_ITEMS_KEY, mutation); + + break; + } + } + } + } + }; + + websocketClient.subscribe(channel, handler); + + return function cleanup() { + websocketClient.unsubscribe(channel, handler); + }; + }, [userId]); + }, + + /** + * React hook to subscribe to the shared items updates of the given user ID + * @param parentId The ID of the user on which to observe shared items updates + */ + useSharedItemsUpdates: (userId: UUID) => { + useEffect(() => { + if (!userId) { + return; + } + + const channel: Channel = { name: userId, topic: 'item/member' }; + + const handler = (event: ItemEvent) => { + if (event.kind === 'shared') { + const current: List | undefined = queryClient.getQueryData( + SHARED_ITEMS_KEY, + ); + + if (current) { + const item = event.item; + let mutation; + + switch (event.op) { + case 'create': { + if (!current.find((i) => i.id === item.id)) { + mutation = current.push(item); + queryClient.setQueryData(SHARED_ITEMS_KEY, mutation); + queryClient.setQueryData(buildItemKey(item.id), item); + } + break; + } + case 'update': { + // replace value if it exists + mutation = current.map((i) => (i.id === item.id ? item : i)); + queryClient.setQueryData(SHARED_ITEMS_KEY, mutation); + queryClient.setQueryData(buildItemKey(item.id), item); + + break; + } + case 'delete': { + mutation = current.filter((i) => i.id !== item.id); + queryClient.setQueryData(SHARED_ITEMS_KEY, mutation); + + break; + } + } + } + } + }; + + websocketClient.subscribe(channel, handler); + + return function cleanup() { + websocketClient.unsubscribe(channel, handler); + }; + }, [userId]); + }, +}); diff --git a/src/ws/hooks/membership.ts b/src/ws/hooks/membership.ts new file mode 100644 index 00000000..b4591e86 --- /dev/null +++ b/src/ws/hooks/membership.ts @@ -0,0 +1,73 @@ +import { List } from 'immutable'; +import { useEffect } from 'react'; +import { QueryClient } from 'react-query'; +import { buildItemMembershipsKey } from '../../config/keys'; +import { Membership, UUID } from '../../types'; +import { Channel, GraaspWebsocketClient } from '../ws-client'; + +// todo: use graasp-types? +interface MembershipEvent { + kind: string; + op: string; + membership: Membership; +} + +export default ( + websocketClient: GraaspWebsocketClient, + queryClient: QueryClient, +) => ({ + /** + * React hooks to subscribe to membership updates for a given item ID + * @param itemId The ID of the item of which to observe memberships updates + */ + useItemMembershipsUpdates: (itemId: UUID) => { + useEffect(() => { + if (!itemId) { + return; + } + + const channel: Channel = { name: itemId, topic: 'memberships/item' }; + const itemMembershipsKey = buildItemMembershipsKey(itemId); + + const handler = (event: MembershipEvent) => { + if (event.kind === 'item') { + const current: + | List + | undefined = queryClient.getQueryData(itemMembershipsKey); + const membership = event.membership; + + if (current && membership.itemId === itemId) { + let mutation; + switch (event.op) { + case 'create': { + if (!current.find((m) => m.id === membership.id)) { + mutation = current.push(membership); + queryClient.setQueryData(itemMembershipsKey, mutation); + } + break; + } + case 'update': { + mutation = current.map((m) => + m.id === membership.id ? membership : m, + ); + queryClient.setQueryData(itemMembershipsKey, mutation); + break; + } + case 'delete': { + mutation = current.filter((m) => m.id !== membership.id); + queryClient.setQueryData(itemMembershipsKey, mutation); + break; + } + } + } + } + }; + + websocketClient.subscribe(channel, handler); + + return function cleanup() { + websocketClient.unsubscribe(channel, handler); + }; + }, [itemId]); + }, +}); diff --git a/src/ws/index.ts b/src/ws/index.ts index c65ca75e..e835522b 100644 --- a/src/ws/index.ts +++ b/src/ws/index.ts @@ -1,8 +1,6 @@ /** * 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'; diff --git a/src/ws/protocol.ts b/src/ws/protocol.ts new file mode 100644 index 00000000..b523626b --- /dev/null +++ b/src/ws/protocol.ts @@ -0,0 +1,102 @@ +/** + * TODO: use types from graasp-websockets + */ + +import { + CLIENT_ACTION_DISCONNECT, + CLIENT_ACTION_SUBSCRIBE, + CLIENT_ACTION_SUBSCRIBE_ONLY, + CLIENT_ACTION_UNSUBSCRIBE, + REALM_NOTIF, + RESPONSE_STATUS_ERROR, + RESPONSE_STATUS_SUCCESS, + SERVER_TYPE_INFO, + SERVER_TYPE_RESPONSE, + SERVER_TYPE_UPDATE, +} from './constants'; + +/** + * Default message shape + * Must have the REALM_NOTIF type to allow future message types unrelated to notifications + */ +interface Message { + realm: typeof REALM_NOTIF; +} + +/** + * Message sent by client to disconnect + */ +export interface ClientDisconnect extends Message { + action: typeof CLIENT_ACTION_DISCONNECT; +} + +/** + * Message sent by client to subscribe to some channel + */ +export interface ClientSubscribe extends Message { + action: typeof CLIENT_ACTION_SUBSCRIBE; + topic: string; + channel: string; +} + +/** + * Message sent by client to unsubscribe from some channel + */ +export interface ClientUnsubscribe extends Message { + action: typeof CLIENT_ACTION_UNSUBSCRIBE; + topic: string; + channel: string; +} + +/** + * Message sent by client to subscribe to a single channel + * (i.e. it also unsubscribes it from any other channel) + */ +export interface ClientSubscribeOnly extends Message { + action: typeof CLIENT_ACTION_SUBSCRIBE_ONLY; + topic: string; + channel: string; +} + +/** + * Message sent by server as a response to a {@link ClientMessage} + */ +export interface ServerResponse extends Message { + type: typeof SERVER_TYPE_RESPONSE; + status: typeof RESPONSE_STATUS_SUCCESS | typeof RESPONSE_STATUS_ERROR; + error?: Error; + request?: ClientMessage; +} + +/** + * Message sent by server for misc broadcasts unrelated to a channel + */ +export interface ServerInfo extends Message { + type: typeof SERVER_TYPE_INFO; + message: string; + extra?: unknown; +} + +/** + * Message sent by server for update notifications sent over a channel + */ +export interface ServerUpdate extends Message { + type: typeof SERVER_TYPE_UPDATE; + topic: string; + channel: string; + body: unknown; +} + +/** + * Client message type is union type of all client message subtypes + */ +export type ClientMessage = + | ClientDisconnect + | ClientSubscribe + | ClientUnsubscribe + | ClientSubscribeOnly; + +/** + * Server message type is union type of all server message subtypes + */ +export type ServerMessage = ServerResponse | ServerInfo | ServerUpdate; diff --git a/src/ws/ws-client.ts b/src/ws/ws-client.ts index a7c622ed..57b1077a 100644 --- a/src/ws/ws-client.ts +++ b/src/ws/ws-client.ts @@ -2,35 +2,26 @@ * Graasp websocket client * Provides front-end integration for real-time updates using WebSocket * Implements the client protocol from https://github.com/graasp/graasp-websockets - * - * @author Alexandre CHAU */ -import { - EntityName, - WS_CLIENT_ACTION_SUBSCRIBE, - WS_CLIENT_ACTION_UNSUBSCRIBE, - WS_REALM_NOTIF, - WS_RESPONSE_STATUS_SUCCESS, - WS_SERVER_TYPE_INFO, - WS_SERVER_TYPE_RESPONSE, - WS_SERVER_TYPE_UPDATE, -} from '@graasp/websockets/src/interfaces/constants'; -import { - ClientMessage, - ServerMessage, -} from '@graasp/websockets/src/interfaces/message'; import { QueryClientConfig } from '../types'; +import { + CLIENT_ACTION_SUBSCRIBE, + CLIENT_ACTION_UNSUBSCRIBE, + REALM_NOTIF, + RESPONSE_STATUS_SUCCESS, + SERVER_TYPE_INFO, + SERVER_TYPE_RESPONSE, + SERVER_TYPE_UPDATE, +} from './constants'; +import { ClientMessage, ServerMessage } from './protocol'; export type Channel = { - entity: EntityName; + topic: string; name: string; }; -// client-side representation of unsollicited server info messages channel -export const INFO_CHANNEL_NAME = 'info'; - -type UpdateHandlerFn = (data: ServerMessage) => void; +type UpdateHandlerFn = (data: any) => void; /** * Helper to remove the first element in an array that @@ -69,8 +60,8 @@ function addToMappedArray(map: Map>, key: S, value: T) { function buildChannelKey(channel: Channel): string { // ensure serialized key is always identical (properties + order) const rebuiltChannel: Channel = { + topic: channel.topic, name: channel.name, - entity: channel.entity, }; return JSON.stringify(rebuiltChannel); } @@ -87,14 +78,14 @@ export interface GraaspWebsocketClient { * @param channel Channel to which to subscribe to * @param handler Handler function to register */ - subscribe(channel: Channel, handler: UpdateHandlerFn): void; + subscribe(channel: Channel, handler: (data: T) => void): 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; + unsubscribe(channel: Channel, handler: (data: T) => void): void; } export const configureWebsocketClient = ( @@ -119,17 +110,18 @@ export const configureWebsocketClient = ( const sendSubscribeRequest = (channel: Channel) => { send({ - realm: WS_REALM_NOTIF, - action: WS_CLIENT_ACTION_SUBSCRIBE, - entity: channel.entity, + realm: REALM_NOTIF, + action: CLIENT_ACTION_SUBSCRIBE, + topic: channel.topic, channel: channel.name, }); }; const sendUnsubscribeRequest = (channel: Channel) => { send({ - realm: WS_REALM_NOTIF, - action: WS_CLIENT_ACTION_UNSUBSCRIBE, + realm: REALM_NOTIF, + action: CLIENT_ACTION_UNSUBSCRIBE, + topic: channel.topic, channel: channel.name, }); }; @@ -140,82 +132,65 @@ export const configureWebsocketClient = ( current: new Map>(), info: new Array(), - add: ( - channel: Channel | typeof INFO_CHANNEL_NAME, - handler: UpdateHandlerFn, - ): boolean => { - if (channel === INFO_CHANNEL_NAME) { - // if subscribed to info, no ack to wait for - subscriptions.info.push(handler); + add: (channel: Channel, handler: UpdateHandlerFn): boolean => { + const channelKey = buildChannelKey(channel); + const maybeCurrent = subscriptions.current.get(channelKey); + if (maybeCurrent !== undefined && maybeCurrent.length > 0) { + // if already subscribed, don't subscribe again, simply register handler in current + addToMappedArray(subscriptions.current, channelKey, handler); return false; } else { - const channelKey = buildChannelKey(channel); - const maybeCurrent = subscriptions.current.get(channelKey); - if (maybeCurrent !== undefined && maybeCurrent.length > 0) { - // if already subscribed, don't subscribe again, simply register handler in current - addToMappedArray(subscriptions.current, channelKey, handler); - return false; - } else { - // if WS not ready, add to early, otherwise add to waiting ack - const map = - ws.readyState === ws.OPEN - ? subscriptions.waitingAck - : subscriptions.early; - // create queue if doesn't exist for this channel, otherwise push to it - addToMappedArray(map, channelKey, handler); - return true; - } + // if WS not ready, add to early, otherwise add to waiting ack + const map = + ws.readyState === ws.OPEN + ? subscriptions.waitingAck + : subscriptions.early; + // create queue if doesn't exist for this channel, otherwise push to it + addToMappedArray(map, channelKey, handler); + return true; } }, - remove: ( - channel: Channel | typeof INFO_CHANNEL_NAME, - handler: UpdateHandlerFn, - ): boolean => { - if (channel === INFO_CHANNEL_NAME) { - arrayRemoveFirstEqual(subscriptions.info, handler); - return false; - } else { - // helper to remove from a subscription map - const _remove = ( - map: Map>, - channelKey: string, - handler: UpdateHandlerFn, - ): boolean => { - const queue = map.get(channelKey); - if (queue !== undefined) { - return arrayRemoveFirstEqual(queue, handler); - } else { - return false; - } - }; - // helper to cleanup mapped array if it is empty - const _cleanup = ( - map: Map>, - channelKey: string, - ): boolean => { - const isNowEmpty = map.get(channelKey)?.length === 0; - if (isNowEmpty) { - // cleanup array - map.delete(channelKey); - } - return isNowEmpty; - }; - - const channelKey = buildChannelKey(channel); - // find first map from which to remove from - if (_remove(subscriptions.early, channelKey, handler)) { - // no need to send unsubscribe if still in early - return false; - } else if (_remove(subscriptions.waitingAck, channelKey, handler)) { - // if in waitingAck must send unsubscribe if just got emptied - return _cleanup(subscriptions.waitingAck, channelKey); - } else if (_remove(subscriptions.current, channelKey, handler)) { - // if in current must send unsubscribe if just got emptied - return _cleanup(subscriptions.current, channelKey); + remove: (channel: Channel, handler: UpdateHandlerFn): boolean => { + // helper to remove from a subscription map + const _remove = ( + map: Map>, + channelKey: string, + handler: UpdateHandlerFn, + ): boolean => { + const queue = map.get(channelKey); + if (queue !== undefined) { + return arrayRemoveFirstEqual(queue, handler); } else { return false; } + }; + // helper to cleanup mapped array if it is empty + const _cleanup = ( + map: Map>, + channelKey: string, + ): boolean => { + const isNowEmpty = map.get(channelKey)?.length === 0; + if (isNowEmpty) { + // cleanup array + map.delete(channelKey); + } + return isNowEmpty; + }; + + const channelKey = buildChannelKey(channel); + // find first map from which to remove from + if (_remove(subscriptions.early, channelKey, handler)) { + // no need to send unsubscribe if still in early + return false; + } else if (_remove(subscriptions.waitingAck, channelKey, handler)) { + // if in waitingAck must send unsubscribe if just got emptied + return _cleanup(subscriptions.waitingAck, channelKey); + } else if (_remove(subscriptions.current, channelKey, handler)) { + // if in current must send unsubscribe if just got emptied + return _cleanup(subscriptions.current, channelKey); + } else { + return false; } }, @@ -250,17 +225,19 @@ export const configureWebsocketClient = ( const update = serdes.parse(event.data); switch (update.type) { - case WS_SERVER_TYPE_INFO: { - subscriptions.info.forEach((fn) => fn(update)); + case SERVER_TYPE_INFO: { + subscriptions.info.forEach((fn) => + fn({ message: update.message, extra: update.extra }), + ); break; } - case WS_SERVER_TYPE_RESPONSE: { - if (update.status === WS_RESPONSE_STATUS_SUCCESS) { + case SERVER_TYPE_RESPONSE: { + if (update.status === RESPONSE_STATUS_SUCCESS) { const req = update.request; - if (req?.action === WS_CLIENT_ACTION_SUBSCRIBE) { + if (req?.action === CLIENT_ACTION_SUBSCRIBE) { // when ack, move all from waiting acks to current - subscriptions.ack({ name: req?.channel, entity: req?.entity }); + subscriptions.ack({ name: req?.channel, topic: req?.topic }); } } else { console.debug( @@ -270,12 +247,12 @@ export const configureWebsocketClient = ( break; } - case WS_SERVER_TYPE_UPDATE: { + case SERVER_TYPE_UPDATE: { // send update to all handlers of this channel - const channel = { name: update.channel, entity: update.body.entity }; + const channel = { name: update.channel, topic: update.topic }; const channelKey = buildChannelKey(channel); const handlers = subscriptions.current.get(channelKey); - handlers?.forEach((fn) => fn(update)); + handlers?.forEach((fn) => fn(update.body)); break; } @@ -285,25 +262,13 @@ export const configureWebsocketClient = ( }); return { - subscribe: ( - channel: Channel | typeof INFO_CHANNEL_NAME, - handler: UpdateHandlerFn, - ) => { - if ( - subscriptions.add(channel, handler) && - channel !== INFO_CHANNEL_NAME - ) { + subscribe: (channel: Channel, handler: (data: T) => void) => { + if (subscriptions.add(channel, handler)) { sendSubscribeRequest(channel); } }, - unsubscribe: ( - channel: Channel | typeof INFO_CHANNEL_NAME, - handler: UpdateHandlerFn, - ) => { - if ( - subscriptions.remove(channel, handler) && - channel !== INFO_CHANNEL_NAME - ) { + unsubscribe: (channel: Channel, handler: (data: T) => void) => { + if (subscriptions.remove(channel, handler)) { sendUnsubscribeRequest(channel); } }, diff --git a/yarn.lock b/yarn.lock index c9d8145c..bd32edc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2044,15 +2044,6 @@ __metadata: languageName: node linkType: hard -"@fastify/ajv-compiler@npm:^1.0.0": - version: 1.1.0 - resolution: "@fastify/ajv-compiler@npm:1.1.0" - dependencies: - ajv: ^6.12.6 - checksum: b8a2522ead00a01ab7ff2921f00aa8e4aeb943949191ce2a617c88e4679db1358a70e4099791828a397a50e5d6f6bd75184ad0ac75a12dffeb9df4c089986a32 - languageName: node - linkType: hard - "@graasp/query-client@link:..::locator=graasp-query-client-example%40workspace%3Aexample": version: 0.0.0-use.local resolution: "@graasp/query-client@link:..::locator=graasp-query-client-example%40workspace%3Aexample" @@ -2068,7 +2059,6 @@ __metadata: "@babel/preset-typescript": 7.14.5 "@commitlint/cli": 12.1.4 "@commitlint/config-conventional": 12.1.4 - "@graasp/websockets": "git://github.com/graasp/graasp-websockets.git#master" "@testing-library/jest-dom": 5.11.6 "@testing-library/react": 11.2.2 "@testing-library/react-hooks": 7.0.0 @@ -2115,20 +2105,6 @@ __metadata: languageName: unknown linkType: soft -"@graasp/websockets@git://github.com/graasp/graasp-websockets.git#master": - version: 0.1.0 - resolution: "@graasp/websockets@git://github.com/graasp/graasp-websockets.git#commit=da4b355456dff8cb908e5fc2e5f5cf0d2aa033d5" - dependencies: - ajv-latest: "npm:ajv@^8.6.0" - dotenv: ^9.0.2 - fastify: ^3.18.1 - fastify-plugin: ^3.0.0 - fastify-websocket: ^3.2.0 - ioredis: ^4.27.6 - checksum: 128f7d7e9496d92981a8f2a8073f92bce65fbc65a8b9f02ded6a411eefef2c598a32d2cc513fbe9d61789f58027fbc8880bcbdcb0b885bc8295640be97e983f5 - languageName: node - linkType: hard - "@hapi/address@npm:2.x.x": version: 2.1.4 resolution: "@hapi/address@npm:2.1.4" @@ -4160,13 +4136,6 @@ __metadata: languageName: node linkType: hard -"abstract-logging@npm:^2.0.0": - version: 2.0.1 - resolution: "abstract-logging@npm:2.0.1" - checksum: 6967d15e5abbafd17f56eaf30ba8278c99333586fa4f7935fd80e93cfdc006c37fcc819c5d63ee373a12e6cb2d0417f7c3c6b9e42b957a25af9937d26749415e - languageName: node - linkType: hard - "accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.7": version: 1.3.7 resolution: "accepts@npm:1.3.7" @@ -4302,27 +4271,27 @@ __metadata: languageName: node linkType: hard -"ajv-latest@npm:ajv@^8.6.0, ajv@npm:^8.0.1": - version: 8.6.2 - resolution: "ajv@npm:8.6.2" +"ajv@npm:^6.1.0, ajv@npm:^6.10.0, ajv@npm:^6.10.2, ajv@npm:^6.12.4, ajv@npm:^6.12.5": + version: 6.12.6 + resolution: "ajv@npm:6.12.6" dependencies: fast-deep-equal: ^3.1.1 - json-schema-traverse: ^1.0.0 - require-from-string: ^2.0.2 + fast-json-stable-stringify: ^2.0.0 + json-schema-traverse: ^0.4.1 uri-js: ^4.2.2 - checksum: b86d6cb86c69abbd8ce71ab7d4ff272660bf6d34fa9fbe770f73e54da59d531b2546692e36e2b35bbcfb11d20db774b4c09189671335185b8c799d65194e5169 + checksum: 874972efe5c4202ab0a68379481fbd3d1b5d0a7bd6d3cc21d40d3536ebff3352a2a1fabb632d4fd2cc7fe4cbdcd5ed6782084c9bbf7f32a1536d18f9da5007d4 languageName: node linkType: hard -"ajv@npm:^6.1.0, ajv@npm:^6.10.0, ajv@npm:^6.10.2, ajv@npm:^6.11.0, ajv@npm:^6.12.2, ajv@npm:^6.12.4, ajv@npm:^6.12.5, ajv@npm:^6.12.6": - version: 6.12.6 - resolution: "ajv@npm:6.12.6" +"ajv@npm:^8.0.1": + version: 8.6.2 + resolution: "ajv@npm:8.6.2" dependencies: fast-deep-equal: ^3.1.1 - fast-json-stable-stringify: ^2.0.0 - json-schema-traverse: ^0.4.1 + json-schema-traverse: ^1.0.0 + require-from-string: ^2.0.2 uri-js: ^4.2.2 - checksum: 874972efe5c4202ab0a68379481fbd3d1b5d0a7bd6d3cc21d40d3536ebff3352a2a1fabb632d4fd2cc7fe4cbdcd5ed6782084c9bbf7f32a1536d18f9da5007d4 + checksum: b86d6cb86c69abbd8ce71ab7d4ff272660bf6d34fa9fbe770f73e54da59d531b2546692e36e2b35bbcfb11d20db774b4c09189671335185b8c799d65194e5169 languageName: node linkType: hard @@ -4452,13 +4421,6 @@ __metadata: languageName: node linkType: hard -"archy@npm:^1.0.0": - version: 1.0.0 - resolution: "archy@npm:1.0.0" - checksum: 504ae7af655130bab9f471343cfdb054feaec7d8e300e13348bc9fe9e660f83d422e473069584f73233c701ae37d1c8452ff2522f2a20c38849e0f406f1732ac - languageName: node - linkType: hard - "are-we-there-yet@npm:~1.1.2": version: 1.1.5 resolution: "are-we-there-yet@npm:1.1.5" @@ -4741,13 +4703,6 @@ __metadata: languageName: node linkType: hard -"atomic-sleep@npm:^1.0.0": - version: 1.0.0 - resolution: "atomic-sleep@npm:1.0.0" - checksum: b95275afb2f80732f22f43a60178430c468906a415a7ff18bcd0feeebc8eec3930b51250aeda91a476062a90e07132b43a1794e8d8ffcf9b650e8139be75fa36 - languageName: node - linkType: hard - "autoprefixer@npm:^9.6.1, autoprefixer@npm:^9.7.3": version: 9.8.6 resolution: "autoprefixer@npm:9.8.6" @@ -4765,18 +4720,6 @@ __metadata: languageName: node linkType: hard -"avvio@npm:^7.1.2": - version: 7.2.2 - resolution: "avvio@npm:7.2.2" - dependencies: - archy: ^1.0.0 - debug: ^4.0.0 - fastq: ^1.6.1 - queue-microtask: ^1.1.2 - checksum: ece793dd148dbb50e24f40dacf4852b804405fc1cd34ce794659ffc020c6de41695d87999edc0bb2573c802eaa7766493859dbc432d5dc0079381f318c2705a1 - languageName: node - linkType: hard - "axe-core@npm:^4.0.2": version: 4.3.2 resolution: "axe-core@npm:4.3.2" @@ -5939,13 +5882,6 @@ __metadata: languageName: node linkType: hard -"cluster-key-slot@npm:^1.1.0": - version: 1.1.0 - resolution: "cluster-key-slot@npm:1.1.0" - checksum: fc953c75209b1ef9088081bab4e40a0b2586491c974ab93460569c014515ca5a2e31c043f185285e177007162fc353d07836d98f570c171dbe055775430e495b - languageName: node - linkType: hard - "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -6486,13 +6422,6 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^0.4.0": - version: 0.4.1 - resolution: "cookie@npm:0.4.1" - checksum: bd7c47f5d94ab70ccdfe8210cde7d725880d2fcda06d8e375afbdd82de0c8d3b73541996e9ce57d35f67f672c4ee6d60208adec06b3c5fc94cebb85196084cf8 - languageName: node - linkType: hard - "copy-concurrently@npm:^1.0.0": version: 1.0.5 resolution: "copy-concurrently@npm:1.0.5" @@ -7114,7 +7043,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1": +"debug@npm:4, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1": version: 4.3.2 resolution: "debug@npm:4.3.2" dependencies: @@ -7277,13 +7206,6 @@ __metadata: languageName: node linkType: hard -"denque@npm:^1.1.0": - version: 1.5.0 - resolution: "denque@npm:1.5.0" - checksum: 9c0d07a3a6789bccc24f7023a54c83b8850b36c8fbc3aff4bf43b01b76a93ae11c88139502913534fe913bac1b0418dbc30e487ce3d176cbbc001a7a18627c56 - languageName: node - linkType: hard - "depd@npm:^1.1.2, depd@npm:~1.1.2": version: 1.1.2 resolution: "depd@npm:1.1.2" @@ -7577,13 +7499,6 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^9.0.2": - version: 9.0.2 - resolution: "dotenv@npm:9.0.2" - checksum: 6b7980330a653089bc9b83362248547791151ee74f9881eb223ac2f4d641b174b708f77315d88708b551d45b4177afd3ba71bca4832f8807e003f71c2a0f83e7 - languageName: node - linkType: hard - "dotgitignore@npm:^2.1.0": version: 2.1.0 resolution: "dotgitignore@npm:2.1.0" @@ -8612,13 +8527,6 @@ __metadata: languageName: node linkType: hard -"fast-decode-uri-component@npm:^1.0.1": - version: 1.0.1 - resolution: "fast-decode-uri-component@npm:1.0.1" - checksum: 427a48fe0907e76f0e9a2c228e253b4d8a8ab21d130ee9e4bb8339c5ba4086235cf9576831f7b20955a752eae4b525a177ff9d5825dd8d416e7726939194fbee - languageName: node - linkType: hard - "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -8646,18 +8554,6 @@ __metadata: languageName: node linkType: hard -"fast-json-stringify@npm:^2.5.2": - version: 2.7.8 - resolution: "fast-json-stringify@npm:2.7.8" - dependencies: - ajv: ^6.11.0 - deepmerge: ^4.2.2 - rfdc: ^1.2.0 - string-similarity: ^4.0.1 - checksum: 78699673bf2852aa3d0984d697de745fca7c5515a79ee13cfc0c754e579807e1fc0a9aa0c18e7b1e8b3378facf6045bc67a4b28b59c6f6048256043a3fbd2c68 - languageName: node - linkType: hard - "fast-levenshtein@npm:^2.0.6, fast-levenshtein@npm:~2.0.6": version: 2.0.6 resolution: "fast-levenshtein@npm:2.0.6" @@ -8665,75 +8561,6 @@ __metadata: languageName: node linkType: hard -"fast-redact@npm:^3.0.0": - version: 3.0.1 - resolution: "fast-redact@npm:3.0.1" - checksum: 89de97ea5cdb32c8ba127c48cf789dd9011dcca72513aa5bb268262fdc6402834b8060782821559c5891bd049c53572f3c6fdb1f4cd26d788614f1dcb01ba39a - languageName: node - linkType: hard - -"fast-safe-stringify@npm:^2.0.8": - version: 2.0.8 - resolution: "fast-safe-stringify@npm:2.0.8" - checksum: be8a07f342817e80c37286509355f91170bd89be9c1df9512ba0c5a61ce20ccf9bdae42ccf65e1fa3834734c78fc524121f709303ebbb97d22df3cb03ff9d7a5 - languageName: node - linkType: hard - -"fastify-error@npm:^0.3.0": - version: 0.3.1 - resolution: "fastify-error@npm:0.3.1" - checksum: fd6a0f6f87b5e4ab59a4d3d66124bd13830a1cf85cf1987259dfb1175fc6a4bcae68b076ad78f6bb06654f72af133b44813090e9ce5502d2ef56ddcb2b0fa867 - languageName: node - linkType: hard - -"fastify-plugin@npm:^3.0.0": - version: 3.0.0 - resolution: "fastify-plugin@npm:3.0.0" - checksum: 00324690789ee02f977ab58ba70d937e2e420a6813a6aa7ae3ba47e34022dc8ac40e15a36ab14a67d94474de984398b1c8b30279fb51d92966f5d3d3ebd1b6fa - languageName: node - linkType: hard - -"fastify-warning@npm:^0.2.0": - version: 0.2.0 - resolution: "fastify-warning@npm:0.2.0" - checksum: c19ebccf54a3122877d2248400772ca98bacbabdf97826211ede29246c640d47431a2eebed1f52f9421139ed5e52e42d3bd4aefc46e27b6f34add3507529fd97 - languageName: node - linkType: hard - -"fastify-websocket@npm:^3.2.0": - version: 3.2.0 - resolution: "fastify-websocket@npm:3.2.0" - dependencies: - fastify-plugin: ^3.0.0 - ws: ^7.4.2 - checksum: 1aa0d5d797616daad626cc0bef3b47b0113137f97f0414249970ddb8c98e6e08570f9cf4257322e5aa23aabe44e9147997a91f906a0f79e65e8cf26d062bd0d9 - languageName: node - linkType: hard - -"fastify@npm:^3.18.1": - version: 3.20.1 - resolution: "fastify@npm:3.20.1" - dependencies: - "@fastify/ajv-compiler": ^1.0.0 - abstract-logging: ^2.0.0 - avvio: ^7.1.2 - fast-json-stringify: ^2.5.2 - fastify-error: ^0.3.0 - fastify-warning: ^0.2.0 - find-my-way: ^4.1.0 - flatstr: ^1.0.12 - light-my-request: ^4.2.0 - pino: ^6.13.0 - proxy-addr: ^2.0.7 - readable-stream: ^3.4.0 - rfdc: ^1.1.4 - secure-json-parse: ^2.0.0 - semver: ^7.3.2 - tiny-lru: ^7.0.0 - checksum: e6742daf0ff7aa95c21ee5ca3dc46c39f270d708981c8952b0e7cb35285c41f3b53ff59933e90169a6c813bbe65e34d0eed722b734dcad1681c1f9c5e691248e - languageName: node - linkType: hard - "fastparse@npm:^1.1.2": version: 1.1.2 resolution: "fastparse@npm:1.1.2" @@ -8741,7 +8568,7 @@ __metadata: languageName: node linkType: hard -"fastq@npm:^1.6.0, fastq@npm:^1.6.1": +"fastq@npm:^1.6.0": version: 1.11.1 resolution: "fastq@npm:1.11.1" dependencies: @@ -8894,18 +8721,6 @@ __metadata: languageName: node linkType: hard -"find-my-way@npm:^4.1.0": - version: 4.3.3 - resolution: "find-my-way@npm:4.3.3" - dependencies: - fast-decode-uri-component: ^1.0.1 - fast-deep-equal: ^3.1.3 - safe-regex2: ^2.0.0 - semver-store: ^0.3.0 - checksum: c5f212d2d2a5e76be91a86409b2de9391a1c3cf5210d357facf0fe91d1e46608865aa2396a98a8d4772116bfae2eb0b05abe4efa725d5cf3cc9b9554bb34e048 - languageName: node - linkType: hard - "find-up@npm:4.1.0, find-up@npm:^4.0.0, find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" @@ -8954,13 +8769,6 @@ __metadata: languageName: node linkType: hard -"flatstr@npm:^1.0.12": - version: 1.0.12 - resolution: "flatstr@npm:1.0.12" - checksum: e1bb562c94b119e958bf37e55738b172b5f8aaae6532b9660ecd877779f8559dbbc89613ba6b29ccc13447e14c59277d41450f785cf75c30df9fce62f459e9a8 - languageName: node - linkType: hard - "flatted@npm:^3.1.0": version: 3.2.2 resolution: "flatted@npm:3.2.2" @@ -10274,25 +10082,6 @@ fsevents@^1.2.7: languageName: node linkType: hard -"ioredis@npm:^4.27.6": - version: 4.27.7 - resolution: "ioredis@npm:4.27.7" - dependencies: - cluster-key-slot: ^1.1.0 - debug: ^4.3.1 - denque: ^1.1.0 - lodash.defaults: ^4.2.0 - lodash.flatten: ^4.4.0 - lodash.isarguments: ^3.1.0 - p-map: ^2.1.0 - redis-commands: 1.7.0 - redis-errors: ^1.2.0 - redis-parser: ^3.0.0 - standard-as-callback: ^2.1.0 - checksum: 42c2f242b3c91202578415dc39fbc9e9ecff58f19acbd91b654cbe12877181e18dbeb6c12b3e71225a7cc928cc4e34f536953375978093c7c1a6f7417a9dfd84 - languageName: node - linkType: hard - "ip-regex@npm:^2.1.0": version: 2.1.0 resolution: "ip-regex@npm:2.1.0" @@ -12308,19 +12097,6 @@ fsevents@^1.2.7: languageName: node linkType: hard -"light-my-request@npm:^4.2.0": - version: 4.4.1 - resolution: "light-my-request@npm:4.4.1" - dependencies: - ajv: ^6.12.2 - cookie: ^0.4.0 - fastify-warning: ^0.2.0 - readable-stream: ^3.6.0 - set-cookie-parser: ^2.4.1 - checksum: f701296c0ec67c400ee3b6dcb6320fdca2816eed64abd7a6a20a12afa70b0d8843737eda3c548f64838399ea30f1f938462815158854c83f9ff8266dee40d129 - languageName: node - linkType: hard - "lines-and-columns@npm:^1.1.6": version: 1.1.6 resolution: "lines-and-columns@npm:1.1.6" @@ -12458,27 +12234,6 @@ fsevents@^1.2.7: languageName: node linkType: hard -"lodash.defaults@npm:^4.2.0": - version: 4.2.0 - resolution: "lodash.defaults@npm:4.2.0" - checksum: 84923258235592c8886e29de5491946ff8c2ae5c82a7ac5cddd2e3cb697e6fbdfbbb6efcca015795c86eec2bb953a5a2ee4016e3735a3f02720428a40efbb8f1 - languageName: node - linkType: hard - -"lodash.flatten@npm:^4.4.0": - version: 4.4.0 - resolution: "lodash.flatten@npm:4.4.0" - checksum: 0ac34a393d4b795d4b7421153d27c13ae67e08786c9cbb60ff5b732210d46f833598eee3fb3844bb10070e8488efe390ea53bb567377e0cb47e9e630bf0811cb - languageName: node - linkType: hard - -"lodash.isarguments@npm:^3.1.0": - version: 3.1.0 - resolution: "lodash.isarguments@npm:3.1.0" - checksum: ae1526f3eb5c61c77944b101b1f655f846ecbedcb9e6b073526eba6890dc0f13f09f72e11ffbf6540b602caee319af9ac363d6cdd6be41f4ee453436f04f13b5 - languageName: node - linkType: hard - "lodash.ismatch@npm:^4.4.0": version: 4.4.0 resolution: "lodash.ismatch@npm:4.4.0" @@ -14025,7 +13780,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"p-map@npm:^2.0.0, p-map@npm:^2.1.0": +"p-map@npm:^2.0.0": version: 2.1.0 resolution: "p-map@npm:2.1.0" checksum: 9e3ad3c9f6d75a5b5661bcad78c91f3a63849189737cd75e4f1225bf9ac205194e5c44aac2ef6f09562b1facdb9bd1425584d7ac375bfaa17b3f1a142dab936d @@ -14370,29 +14125,6 @@ fsevents@^1.2.7: languageName: node linkType: hard -"pino-std-serializers@npm:^3.1.0": - version: 3.2.0 - resolution: "pino-std-serializers@npm:3.2.0" - checksum: 77e29675b116e42ae9fe6d4ef52ef3a082ffc54922b122d85935f93ddcc20277f0b0c873c5c6c5274a67b0409c672aaae3de6bcea10a2d84699718dda55ba95b - languageName: node - linkType: hard - -"pino@npm:^6.13.0": - version: 6.13.0 - resolution: "pino@npm:6.13.0" - dependencies: - fast-redact: ^3.0.0 - fast-safe-stringify: ^2.0.8 - flatstr: ^1.0.12 - pino-std-serializers: ^3.1.0 - quick-format-unescaped: ^4.0.3 - sonic-boom: ^1.0.2 - bin: - pino: bin.js - checksum: 7145de4287f03bc3f7ff4de0cdacfcfea9d843e5759c90f95f6a245b6898964387f6bd24813749333c23684186bc79d91a2d03f28512f2c4a3f8bc6773316a1c - languageName: node - linkType: hard - "pirates@npm:^4.0.1": version: 4.0.1 resolution: "pirates@npm:4.0.1" @@ -15555,7 +15287,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"proxy-addr@npm:^2.0.7, proxy-addr@npm:~2.0.5": +"proxy-addr@npm:~2.0.5": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" dependencies: @@ -15706,20 +15438,13 @@ fsevents@^1.2.7: languageName: node linkType: hard -"queue-microtask@npm:^1.1.2, queue-microtask@npm:^1.2.2": +"queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" checksum: b676f8c040cdc5b12723ad2f91414d267605b26419d5c821ff03befa817ddd10e238d22b25d604920340fd73efd8ba795465a0377c4adf45a4a41e4234e42dc4 languageName: node linkType: hard -"quick-format-unescaped@npm:^4.0.3": - version: 4.0.3 - resolution: "quick-format-unescaped@npm:4.0.3" - checksum: 28dd3f3fbfec385cdca779e905d48c1a4623ee1f8071f060c7b38625eded5b5472433ae38ec09b8f8d968b443dfda7aa156811c59c4dfe0b52b73c3bc6d714ed - languageName: node - linkType: hard - "quick-lru@npm:^1.0.0": version: 1.1.0 resolution: "quick-lru@npm:1.1.0" @@ -16108,7 +15833,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.2, readable-stream@npm:^3.0.6, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": +"readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.2, readable-stream@npm:^3.0.6, readable-stream@npm:^3.6.0": version: 3.6.0 resolution: "readable-stream@npm:3.6.0" dependencies: @@ -16168,29 +15893,6 @@ fsevents@^1.2.7: languageName: node linkType: hard -"redis-commands@npm:1.7.0": - version: 1.7.0 - resolution: "redis-commands@npm:1.7.0" - checksum: d1ff7fbcb5e54768c77f731f1d49679d2a62c3899522c28addb4e2e5813aea8bcac3f22519d71d330224c3f2937f935dfc3d8dc65e90db0f5fe22dc2c1515aa7 - languageName: node - linkType: hard - -"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0": - version: 1.2.0 - resolution: "redis-errors@npm:1.2.0" - checksum: f28ac2692113f6f9c222670735aa58aeae413464fd58ccf3fce3f700cae7262606300840c802c64f2b53f19f65993da24dc918afc277e9e33ac1ff09edb394f4 - languageName: node - linkType: hard - -"redis-parser@npm:^3.0.0": - version: 3.0.0 - resolution: "redis-parser@npm:3.0.0" - dependencies: - redis-errors: ^1.0.0 - checksum: 89290ae530332f2ae37577647fa18208d10308a1a6ba750b9d9a093e7398f5e5253f19855b64c98757f7129cccce958e4af2573fdc33bad41405f87f1943459a - languageName: node - linkType: hard - "redux@npm:^3.6.0 || ^4.0.0": version: 4.1.0 resolution: "redux@npm:4.1.0" @@ -16539,13 +16241,6 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"ret@npm:~0.2.0": - version: 0.2.2 - resolution: "ret@npm:0.2.2" - checksum: 774964bb413a3525e687bca92d81c1cd75555ec33147c32ecca22f3d06409e35df87952cfe3d57afff7650a0f7e42139cf60cb44e94c29dde390243bc1941f16 - languageName: node - linkType: hard - "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -16577,13 +16272,6 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"rfdc@npm:^1.1.4, rfdc@npm:^1.2.0": - version: 1.3.0 - resolution: "rfdc@npm:1.3.0" - checksum: fb2ba8512e43519983b4c61bd3fa77c0f410eff6bae68b08614437bc3f35f91362215f7b4a73cbda6f67330b5746ce07db5dd9850ad3edc91271ad6deea0df32 - languageName: node - linkType: hard - "rgb-regex@npm:^1.0.1": version: 1.0.1 resolution: "rgb-regex@npm:1.0.1" @@ -16825,15 +16513,6 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"safe-regex2@npm:^2.0.0": - version: 2.0.0 - resolution: "safe-regex2@npm:2.0.0" - dependencies: - ret: ~0.2.0 - checksum: f5e182fca040dedd50ae052ea0eb035d9903b2db71243d5d8b43299735857288ef2ab52546a368d9c6fd1333b2a0d039297925e78ffc14845354f3f6158af7c2 - languageName: node - linkType: hard - "safe-regex@npm:^1.1.0": version: 1.1.0 resolution: "safe-regex@npm:1.1.0" @@ -16970,13 +16649,6 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"secure-json-parse@npm:^2.0.0": - version: 2.4.0 - resolution: "secure-json-parse@npm:2.4.0" - checksum: efaafcaa08a4646ca829b29168474f57fb289a0ca7a1d77b66b55a0292785bc6eb9143b21cfc50b37dd12a823c25b12aa1771f18314ed5a616a1f8f12a318533 - languageName: node - linkType: hard - "select-hose@npm:^2.0.0": version: 2.0.0 resolution: "select-hose@npm:2.0.0" @@ -16993,13 +16665,6 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"semver-store@npm:^0.3.0": - version: 0.3.0 - resolution: "semver-store@npm:0.3.0" - checksum: b38f747123e850191526a912657c653c7e5963d164a8daf99e52aa30bc8c5bdac176dc6dab714e17a1a8489ac138c18ff7161b1961f1882888bce637990442dd - languageName: node - linkType: hard - "semver@npm:2 || 3 || 4 || 5, semver@npm:^5.4.1, semver@npm:^5.5.0, semver@npm:^5.5.1, semver@npm:^5.6.0": version: 5.7.1 resolution: "semver@npm:5.7.1" @@ -17120,13 +16785,6 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"set-cookie-parser@npm:^2.4.1": - version: 2.4.8 - resolution: "set-cookie-parser@npm:2.4.8" - checksum: e15b5df9a56ab06d4895286033a6aff7b318ad024310df058b5821b3539cc06f716ef529618cac0dd78df40e37830de715f388c0f97f84062dd9be2326efcd0c - languageName: node - linkType: hard - "set-value@npm:^2.0.0, set-value@npm:^2.0.1": version: 2.0.1 resolution: "set-value@npm:2.0.1" @@ -17359,16 +17017,6 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"sonic-boom@npm:^1.0.2": - version: 1.4.1 - resolution: "sonic-boom@npm:1.4.1" - dependencies: - atomic-sleep: ^1.0.0 - flatstr: ^1.0.12 - checksum: 189fa8fe5c2dc05d3513fc1a4926a2f16f132fa6fa0b511745a436010cdcd9c1d3b3cb6a9d7c05bd32a965dc77673a5ac0eb0992e920bdedd16330d95323124f - languageName: node - linkType: hard - "sort-keys@npm:^1.0.0": version: 1.1.2 resolution: "sort-keys@npm:1.1.2" @@ -17605,13 +17253,6 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"standard-as-callback@npm:^2.1.0": - version: 2.1.0 - resolution: "standard-as-callback@npm:2.1.0" - checksum: 88bec83ee220687c72d94fd86a98d5272c91d37ec64b66d830dbc0d79b62bfa6e47f53b71646011835fc9ce7fae62739545d13124262b53be4fbb3e2ebad551c - languageName: node - linkType: hard - "standard-version@npm:9.1.0": version: 9.1.0 resolution: "standard-version@npm:9.1.0" @@ -17725,13 +17366,6 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"string-similarity@npm:^4.0.1": - version: 4.0.4 - resolution: "string-similarity@npm:4.0.4" - checksum: 797b41b24e1eb6b3b0ab896950b58c295a19a82933479c75f7b5279ffb63e0b456a8c8d10329c02f607ca1a50370e961e83d552aa468ff3b0fa15809abc9eff7 - languageName: node - linkType: hard - "string-width@npm:^1.0.1": version: 1.0.2 resolution: "string-width@npm:1.0.2" @@ -18309,13 +17943,6 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"tiny-lru@npm:^7.0.0": - version: 7.0.6 - resolution: "tiny-lru@npm:7.0.6" - checksum: 36a786a9111a3a358a698c9404783ad3f5396f6ed891672ffa4cd7dec4617abb8dfed6af7611bbaf8a3942a9475534c8b80f9bd58d7efa9aa594f395c9805f09 - languageName: node - linkType: hard - "tiny-warning@npm:^1.0.2": version: 1.0.3 resolution: "tiny-warning@npm:1.0.3" @@ -19692,7 +19319,7 @@ typescript@^3.8.3: languageName: node linkType: hard -"ws@npm:^7.4.2, ws@npm:^7.4.6": +"ws@npm:^7.4.6": version: 7.5.3 resolution: "ws@npm:7.5.3" peerDependencies: