diff --git a/.eslintrc.js b/.eslintrc.js index 1c321c08..0a832a43 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,8 @@ module.exports = { // security/detect-object-injection just gives a lot of false positives // see https://github.com/nodesecurity/eslint-plugin-security/issues/21 'security/detect-object-injection': 'off', + // the code problem checked by this ESLint rule is automatically checked by the TypeScript compiler + 'no-redeclare': 'off', }, overrides: [ { diff --git a/README.md b/README.md index 4d2ecc33..54dfbb00 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,10 @@ const client = new Ably.Realtime.Promise({ key: "", clientId: " mockHistory; })(), subscribe: () => {}, + unsubscribe: () => {}, + on: () => {}, + off: () => {}, publish: () => {}, subscriptions: createMockEmitter(), }; diff --git a/demo/package-lock.json b/demo/package-lock.json index 59db66d1..cf8846eb 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -8,9 +8,8 @@ "name": "demo", "version": "1.0.0", "dependencies": { - "@ably-labs/react-hooks": "^3.0.0-canary.1", - "@ably/spaces": "0.1.3", - "ably": "^1.2.44", + "@ably/spaces": "file:..", + "ably": "^1.2.45", "classnames": "^2.3.2", "dayjs": "^1.11.9", "nanoid": "^4.0.2", @@ -40,6 +39,49 @@ "vite": "^4.4.5" } }, + "..": { + "name": "@ably/spaces", + "version": "0.1.3", + "license": "ISC", + "dependencies": { + "nanoid": "^4.0.2" + }, + "devDependencies": { + "@rollup/plugin-node-resolve": "^15.2.1", + "@rollup/plugin-terser": "^0.4.3", + "@testing-library/react": "^14.0.0", + "@types/react": "^18.2.23", + "@types/react-dom": "^18.2.8", + "@typescript-eslint/eslint-plugin": "^5.51.0", + "@typescript-eslint/parser": "^5.51.0", + "@vitest/coverage-c8": "^0.33.0", + "eslint": "^8.33.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsdoc": "^46.7.0", + "eslint-plugin-security": "^1.7.1", + "jsdom": "^22.1.0", + "prettier": "^2.8.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rollup": "^3.28.0", + "ts-node": "^10.9.1", + "typescript": "^4.9.5", + "vitest": "^0.34.3" + }, + "peerDependencies": { + "ably": "^1.2.45", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -49,18 +91,6 @@ "node": ">=0.10.0" } }, - "node_modules/@ably-labs/react-hooks": { - "version": "3.0.0-canary.1", - "resolved": "https://registry.npmjs.org/@ably-labs/react-hooks/-/react-hooks-3.0.0-canary.1.tgz", - "integrity": "sha512-ln2XHNwiZof3xU0jZBd1tb4wzsntmbfonqX9M40V2nlNKW7OkzSSW0e2CbAyy8LtsZPEm6vsHZHVgTcdrBxKBg==", - "dependencies": { - "ably": "^1.2.27" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, "node_modules/@ably/msgpack-js": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.4.0.tgz", @@ -70,15 +100,8 @@ } }, "node_modules/@ably/spaces": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@ably/spaces/-/spaces-0.1.3.tgz", - "integrity": "sha512-8egeUAvl+L6wrBuIIVx17BdQH+bl9rP0VRqEuYpx8+lRt8pQS+t/gyfjE3XQbg5OIaILbeiDbhgkNNDD7OOlRw==", - "dependencies": { - "nanoid": "^4.0.2" - }, - "peerDependencies": { - "ably": "^1.2.43" - } + "resolved": "..", + "link": true }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", @@ -20404,14 +20427,6 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, - "@ably-labs/react-hooks": { - "version": "3.0.0-canary.1", - "resolved": "https://registry.npmjs.org/@ably-labs/react-hooks/-/react-hooks-3.0.0-canary.1.tgz", - "integrity": "sha512-ln2XHNwiZof3xU0jZBd1tb4wzsntmbfonqX9M40V2nlNKW7OkzSSW0e2CbAyy8LtsZPEm6vsHZHVgTcdrBxKBg==", - "requires": { - "ably": "^1.2.27" - } - }, "@ably/msgpack-js": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.4.0.tgz", @@ -20421,11 +20436,29 @@ } }, "@ably/spaces": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@ably/spaces/-/spaces-0.1.3.tgz", - "integrity": "sha512-8egeUAvl+L6wrBuIIVx17BdQH+bl9rP0VRqEuYpx8+lRt8pQS+t/gyfjE3XQbg5OIaILbeiDbhgkNNDD7OOlRw==", + "version": "file:..", "requires": { - "nanoid": "^4.0.2" + "@rollup/plugin-node-resolve": "^15.2.1", + "@rollup/plugin-terser": "^0.4.3", + "@testing-library/react": "^14.0.0", + "@types/react": "^18.2.23", + "@types/react-dom": "^18.2.8", + "@typescript-eslint/eslint-plugin": "^5.51.0", + "@typescript-eslint/parser": "^5.51.0", + "@vitest/coverage-c8": "^0.33.0", + "eslint": "^8.33.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsdoc": "^46.7.0", + "eslint-plugin-security": "^1.7.1", + "jsdom": "^22.1.0", + "nanoid": "^4.0.2", + "prettier": "^2.8.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rollup": "^3.28.0", + "ts-node": "^10.9.1", + "typescript": "^4.9.5", + "vitest": "^0.34.3" } }, "@alloc/quick-lru": { diff --git a/demo/package.json b/demo/package.json index 98bae423..848019ec 100644 --- a/demo/package.json +++ b/demo/package.json @@ -12,9 +12,8 @@ "deploy:production": "npm run build && netlify deploy --prod" }, "dependencies": { - "@ably-labs/react-hooks": "^3.0.0-canary.1", - "@ably/spaces": "0.1.3", - "ably": "^1.2.44", + "@ably/spaces": "file:..", + "ably": "^1.2.45", "classnames": "^2.3.2", "dayjs": "^1.11.9", "nanoid": "^4.0.2", diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 9c79e7b8..cfd7dce7 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -1,33 +1,36 @@ -import { useContext, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; +import { useMembers, useSpace, useLocations } from '@ably/spaces/react'; -import { Header, SlideMenu, SpacesContext, CurrentSlide, AblySvg, slides, Modal } from './components'; +import { Header, SlideMenu, CurrentSlide, AblySvg, slides, Modal } from './components'; import { getRandomName, getRandomColor } from './utils'; -import { useMembers } from './hooks'; import { PreviewProvider } from './components/PreviewContext.tsx'; +import { type Member } from './utils/types'; + const App = () => { - const space = useContext(SpacesContext); + const { space, enter } = useSpace(); const { self, others } = useMembers(); + const { update } = useLocations(); const [isModalVisible, setModalIsVisible] = useState(false); useEffect(() => { if (!space || self?.profileData.name) return; - const enter = async () => { + const init = async () => { const name = getRandomName(); - await space.enter({ name, color: getRandomColor() }); - await space.locations.set({ slide: `${0}`, element: null }); + await enter({ name, color: getRandomColor() }); + await update({ slide: `${0}`, element: null }); setModalIsVisible(true); }; - enter(); + init(); }, [space, self?.profileData.name]); return (
@@ -50,7 +53,7 @@ const App = () => {
diff --git a/demo/src/components/CurrentSlide.tsx b/demo/src/components/CurrentSlide.tsx index ecda595b..7a146a91 100644 --- a/demo/src/components/CurrentSlide.tsx +++ b/demo/src/components/CurrentSlide.tsx @@ -1,7 +1,8 @@ import { useRef } from 'react'; import { Cursors } from '.'; -import { useMembers, useTrackCursor } from '../hooks'; +import { useTrackCursor } from '../hooks'; import { SlidePreviewProps } from './SlidePreview'; +import { useMembers } from '@ably/spaces/react'; interface Props { slides: Omit[]; diff --git a/demo/src/components/Cursors.tsx b/demo/src/components/Cursors.tsx index 0f726c4a..c64d2adc 100644 --- a/demo/src/components/Cursors.tsx +++ b/demo/src/components/Cursors.tsx @@ -1,65 +1,27 @@ -import { useContext, useEffect, useState } from 'react'; -import type { CursorUpdate as _CursorUpdate } from '@ably/spaces'; - +import { useCursors } from '@ably/spaces/react'; import cn from 'classnames'; -import { CursorSvg, SpacesContext } from '.'; -import { useMembers, CURSOR_ENTER, CURSOR_LEAVE, CURSOR_MOVE } from '../hooks'; - -type state = typeof CURSOR_ENTER | typeof CURSOR_LEAVE | typeof CURSOR_MOVE; -type CursorUpdate = Omit<_CursorUpdate, 'data'> & { data: { state: state } }; +import { CursorSvg } from '.'; +import { CURSOR_LEAVE } from '../hooks'; export const Cursors = () => { - const space = useContext(SpacesContext); - const { self, others } = useMembers(); - const [cursors, setCursors] = useState<{ - [connectionId: string]: { position: CursorUpdate['position']; state: CursorUpdate['data']['state'] }; - }>({}); - - useEffect(() => { - if (!space || !others) return; - - space.cursors.subscribe('update', (cursorUpdate) => { - const { connectionId, position, data } = cursorUpdate as CursorUpdate; - - if (cursorUpdate.connectionId === self?.connectionId) return; - - setCursors((currentCursors) => ({ - ...currentCursors, - [connectionId]: { position, state: data.state }, - })); + const { space, cursors } = useCursors({ returnCursors: true }); + + const activeCursors = Object.keys(cursors) + .filter((connectionId) => { + const { member, cursorUpdate } = cursors[connectionId]!!; + return ( + member.connectionId !== space.connectionId && member.isConnected && cursorUpdate.data.state !== CURSOR_LEAVE + ); + }) + .map((connectionId) => { + const { member, cursorUpdate } = cursors[connectionId]!!; + return { + connectionId: member.connectionId, + profileData: member.profileData, + position: cursorUpdate.position, + }; }); - return () => { - space.cursors.unsubscribe('update'); - }; - }, [space, others, self?.connectionId]); - - useEffect(() => { - const handler = async (member: { connectionId: string }) => { - setCursors((currentCursors) => ({ - ...currentCursors, - [member.connectionId]: { position: { x: 0, y: 0 }, state: CURSOR_LEAVE }, - })); - }; - - space?.members.subscribe('leave', handler); - - return () => { - space?.members.unsubscribe('leave', handler); - }; - }, [space]); - - const activeCursors = others - .filter( - (member) => - member.isConnected && cursors[member.connectionId] && cursors[member.connectionId].state !== CURSOR_LEAVE, - ) - .map((member) => ({ - connectionId: member.connectionId, - profileData: member.profileData, - position: cursors[member.connectionId].position, - })); - return (
{activeCursors.map((cursor) => { diff --git a/demo/src/components/Image.tsx b/demo/src/components/Image.tsx index 95d36be0..7be5868c 100644 --- a/demo/src/components/Image.tsx +++ b/demo/src/components/Image.tsx @@ -1,7 +1,8 @@ import cn from 'classnames'; -import { useClickOutside, useElementSelect, useMembers } from '../hooks'; +import { useClickOutside, useElementSelect } from '../hooks'; import { findActiveMembers, getMemberFirstName, getOutlineClasses } from '../utils'; import { useRef } from 'react'; +import { useMembers } from '@ably/spaces/react'; import { usePreview } from './PreviewContext.tsx'; interface Props extends React.HTMLAttributes { diff --git a/demo/src/components/Modal.tsx b/demo/src/components/Modal.tsx index f6a56c3f..2a369aac 100644 --- a/demo/src/components/Modal.tsx +++ b/demo/src/components/Modal.tsx @@ -1,7 +1,6 @@ -import { FormEvent, useContext, useRef } from 'react'; +import { FormEvent, useRef } from 'react'; import cn from 'classnames'; - -import { SpacesContext } from '.'; +import { useSpace } from '@ably/spaces/react'; import { Member } from '../utils/types'; interface Props { @@ -11,7 +10,7 @@ interface Props { } export const Modal = ({ isVisible = false, setIsVisible, self }: Props) => { - const space = useContext(SpacesContext); + const { space, updateProfileData } = useSpace(); const inputRef = useRef(null); const handleSubmit = (e: FormEvent) => { @@ -19,7 +18,7 @@ export const Modal = ({ isVisible = false, setIsVisible, self }: Props) => { if (!space || !setIsVisible) return; - space.updateProfileData((profileData) => ({ ...profileData, name: inputRef.current?.value })); + updateProfileData((profileData) => ({ ...profileData, name: inputRef.current?.value })); setIsVisible(false); }; diff --git a/demo/src/components/SlidePreview.tsx b/demo/src/components/SlidePreview.tsx index b1af292e..7256a11b 100644 --- a/demo/src/components/SlidePreview.tsx +++ b/demo/src/components/SlidePreview.tsx @@ -1,8 +1,7 @@ import cn from 'classnames'; import { AvatarStack, CurrentSelectorSvg } from '.'; -import { useContext } from 'react'; -import { SpacesContext } from '.'; -import { useMembers } from '../hooks'; +import { useLocations, useMembers } from '@ably/spaces/react'; +import { Member } from '../utils/types'; export interface SlidePreviewProps { children: React.ReactNode; @@ -10,14 +9,14 @@ export interface SlidePreviewProps { } export const SlidePreview = ({ children, index }: SlidePreviewProps) => { - const space = useContext(SpacesContext); - const { self, members } = useMembers(); + const { space, self, members } = useMembers(); + const { update } = useLocations(); const membersOnASlide = (members || []).filter(({ location }) => location?.slide === `${index}`); const isActive = self?.location?.slide === `${index}`; const handleSlideClick = async () => { if (!space || !self) return; - space.locations.set({ slide: `${index}`, element: null }); + update({ slide: `${index}`, element: null }); }; return ( @@ -49,7 +48,7 @@ export const SlidePreview = ({ children, index }: SlidePreviewProps) => {
); diff --git a/demo/src/components/SpacesContext.tsx b/demo/src/components/SpacesContext.tsx deleted file mode 100644 index 1ec91831..00000000 --- a/demo/src/components/SpacesContext.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from 'react'; -import { AblyProvider } from '@ably-labs/react-hooks'; - -import Spaces, { type Space } from '@ably/spaces'; -import { Realtime } from 'ably'; -import { nanoid } from 'nanoid'; - -import { getParamValueFromUrl, generateSpaceName } from '../utils'; - -export const SpacesContext = React.createContext(undefined); - -const SpaceContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [space, setSpace] = React.useState(undefined); - const spaceName = getParamValueFromUrl('space', generateSpaceName); - - const [spaces, ably] = React.useMemo(() => { - const clientId = nanoid(); - - const ably = new Realtime.Promise({ - authUrl: `/api/ably-token-request?clientId=${clientId}`, - clientId, - }); - - return [new Spaces(ably), ably]; - }, []); - - React.useEffect(() => { - let ignore = false; - - const init = async () => { - const spaceInstance = await spaces.get(getParamValueFromUrl('space', generateSpaceName), { - offlineTimeout: 10_000, - }); - - if (spaceInstance && !space && !ignore) { - setSpace(spaceInstance); - } - }; - - init(); - - return () => { - ignore = true; - }; - }, [spaceName, spaces]); - - return ( - - {children}{' '} - - ); -}; - -export { SpaceContextProvider }; diff --git a/demo/src/components/index.ts b/demo/src/components/index.ts index 28b16ad4..d5c8529b 100644 --- a/demo/src/components/index.ts +++ b/demo/src/components/index.ts @@ -10,6 +10,5 @@ export * from './Paragraph'; export * from './SlideMenu'; export * from './SlidePreview'; export * from './slides'; -export * from './SpacesContext'; export * from './svg'; export * from './Title'; diff --git a/demo/src/hooks/index.ts b/demo/src/hooks/index.ts index b2ab5dab..e4644a39 100644 --- a/demo/src/hooks/index.ts +++ b/demo/src/hooks/index.ts @@ -1,4 +1,2 @@ -export * from './useMembers'; export * from './useElementSelect'; export * from './useTrackCursor'; -export * from './useLock'; diff --git a/demo/src/hooks/useElementSelect.ts b/demo/src/hooks/useElementSelect.ts index d29adbf3..2ffc8375 100644 --- a/demo/src/hooks/useElementSelect.ts +++ b/demo/src/hooks/useElementSelect.ts @@ -1,13 +1,11 @@ -import { MutableRefObject, useContext, useEffect } from 'react'; -import { SpacesContext } from '../components'; -import { useMembers } from './useMembers'; +import { MutableRefObject, useEffect } from 'react'; import { buildLockId, releaseMyLocks } from '../utils/locking'; import { Member } from '../utils/types'; +import { useMembers, useSpace } from '@ably/spaces/react'; export const useElementSelect = (element?: string, lockable: boolean = true) => { - const space = useContext(SpacesContext); - const { self } = useMembers(); + const { space, self } = useMembers(); const handleSelect = async () => { if (!space || !self) return; @@ -32,7 +30,7 @@ export const useElementSelect = (element?: string, lockable: boolean = true) => }; export const useClickOutside = (ref: MutableRefObject, self?: Member, enabled?: boolean) => { - const space = useContext(SpacesContext); + const { space } = useSpace(); useEffect(() => { if (!enabled) return; @@ -55,7 +53,7 @@ export const useClickOutside = (ref: MutableRefObject, self? }; export const useClearOnFailedLock = (lockConflict: boolean, self?: Member) => { - const space = useContext(SpacesContext); + const { space } = useSpace(); useEffect(() => { if (lockConflict) { diff --git a/demo/src/hooks/useLock.ts b/demo/src/hooks/useLock.ts deleted file mode 100644 index 7a3d2181..00000000 --- a/demo/src/hooks/useLock.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useContext, useEffect, useState } from 'react'; - -import { type Lock, LockStatus } from '@ably/spaces'; - -import { SpacesContext } from '../components'; -import { buildLockId } from '../utils/locking'; -import { isMember } from '../hooks'; - -import { type Member } from '../utils/types'; - -export const useLock = (slide: string, id: string): { status?: string; member?: Member } => { - const space = useContext(SpacesContext); - const locationLockId = buildLockId(slide, id); - const [status, setStatus] = useState(undefined); - const [member, setMember] = useState(undefined); - - useEffect(() => { - if (!space) return; - - const handler = (lock: Lock) => { - if (lock.id !== locationLockId) return; - - setStatus(lock.status); - - if (isMember(lock.member)) { - setMember(lock.member); - } - }; - - space.locks.subscribe('update', handler); - - return () => { - space?.locks.unsubscribe('update', handler); - }; - }, [space, slide, id]); - - useEffect(() => { - if (status !== undefined) return; - const lock = space?.locks.get(locationLockId); - if (lock) { - setMember(lock.member as any); - setStatus(lock.status); - } - }, [status]); - - return { status, member }; -}; - -export const useLockStatus = (slide: string, id: string, selfConnectionId?: string) => { - const { member, status } = useLock(slide, id); - - const locked = status === 'locked'; - const lockedByYou = locked && member?.connectionId === selfConnectionId; - - return { locked, lockedByYou }; -}; diff --git a/demo/src/hooks/useMembers.ts b/demo/src/hooks/useMembers.ts deleted file mode 100644 index 47905d24..00000000 --- a/demo/src/hooks/useMembers.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useEffect, useState, useContext } from 'react'; -import { type SpaceMember } from '@ably/spaces'; -import { SpacesContext } from '../components'; - -import { type Member } from '../utils/types'; - -export const isMember = (obj: unknown): obj is Member => { - return !!(obj as Member)?.profileData?.name && !!(obj as Member)?.profileData?.color; -}; - -const areMembers = (arr: unknown): arr is Member[] => { - return (arr as Member[]).every((m) => isMember(m)); -}; - -const membersToOthers = (members: Member[] = [], self: SpaceMember | null): Member[] => - members.filter((m) => m.connectionId !== self?.connectionId); - -export const useMembers: () => { self?: Member; others: Member[]; members: Member[] } = () => { - const space = useContext(SpacesContext); - const [members, setMembers] = useState([]); - const [others, setOthers] = useState([]); - const [self, setSelf] = useState(undefined); - - useEffect(() => { - if (!space) return; - - const handler = ({ members }: { members: SpaceMember[] }) => - (async () => { - const self = await space.members.getSelf(); - - if (isMember(self)) { - setSelf(self); - } - - if (areMembers(members)) { - setMembers([...members]); - setOthers(membersToOthers([...members], self)); - } - })(); - - const init = async () => { - const initSelf = await space.members.getSelf(); - const initMembers = await space.members.getAll(); - - if (isMember(initSelf)) { - setSelf(initSelf); - } - - if (areMembers(initMembers)) { - setMembers(initMembers); - setOthers(membersToOthers(initMembers, initSelf)); - } - space.subscribe('update', handler); - }; - - init(); - - return () => { - space.unsubscribe('update', handler); - }; - }, [space]); - - return { members, self, others }; -}; diff --git a/demo/src/hooks/useTextComponentLock.ts b/demo/src/hooks/useTextComponentLock.ts index cd837dff..c175d67a 100644 --- a/demo/src/hooks/useTextComponentLock.ts +++ b/demo/src/hooks/useTextComponentLock.ts @@ -1,13 +1,12 @@ import { MutableRefObject, useCallback } from 'react'; -import { useChannel } from '@ably-labs/react-hooks'; +import { useChannel } from 'ably/react'; +import { useMembers, useLock } from '@ably/spaces/react'; +import sanitize from 'sanitize-html'; import { findActiveMember, generateSpaceName, getParamValueFromUrl } from '../utils'; import { buildLockId } from '../utils/locking.ts'; import { usePreview } from '../components/PreviewContext.tsx'; -import { useMembers } from './useMembers.ts'; import { useClearOnFailedLock, useClickOutside, useElementSelect } from './useElementSelect.ts'; -import { useLockStatus } from './useLock.ts'; import { useSlideElementContent } from './useSlideElementContent.ts'; -import sanitize from 'sanitize-html'; interface UseTextComponentLockArgs { id: string; @@ -20,8 +19,10 @@ export const useTextComponentLock = ({ id, slide, defaultText, containerRef }: U const spaceName = getParamValueFromUrl('space', generateSpaceName); const { members, self } = useMembers(); const activeMember = findActiveMember(id, slide, members); - const { locked, lockedByYou } = useLockStatus(slide, id, self?.connectionId); const lockId = buildLockId(slide, id); + const { status, member } = useLock(lockId); + const locked = status === 'locked'; + const lockedByYou = locked && self?.connectionId === member?.connectionId; const channelName = `[?rewind=1]${spaceName}${lockId}`; const [content, updateContent] = useSlideElementContent(lockId, defaultText); const preview = usePreview(); diff --git a/demo/src/hooks/useTrackCursor.ts b/demo/src/hooks/useTrackCursor.ts index 01b277ba..56e26d57 100644 --- a/demo/src/hooks/useTrackCursor.ts +++ b/demo/src/hooks/useTrackCursor.ts @@ -1,15 +1,15 @@ -import { useContext, RefObject, useEffect } from 'react'; -import { SpacesContext } from '../components'; +import { RefObject, useEffect } from 'react'; +import { useCursors } from '@ably/spaces/react'; export const CURSOR_MOVE = 'move'; export const CURSOR_ENTER = 'enter'; export const CURSOR_LEAVE = 'leave'; export const useTrackCursor = (containerRef: RefObject, selfConnectionId?: string) => { - const space = useContext(SpacesContext); + const { set } = useCursors(); useEffect(() => { - if (!containerRef.current || !space) return; + if (!containerRef.current || !set) return; const { current: cursorContainer } = containerRef; @@ -17,7 +17,7 @@ export const useTrackCursor = (containerRef: RefObject, selfConn enter: (event: MouseEvent) => { if (!selfConnectionId) return; const { top, left } = cursorContainer.getBoundingClientRect(); - space.cursors.set({ + set({ position: { x: event.clientX - left, y: event.clientY - top }, data: { state: CURSOR_ENTER }, }); @@ -25,7 +25,7 @@ export const useTrackCursor = (containerRef: RefObject, selfConn move: (event: MouseEvent) => { if (!selfConnectionId) return; const { top, left } = cursorContainer.getBoundingClientRect(); - space.cursors.set({ + set({ position: { x: event.clientX - left, y: event.clientY - top }, data: { state: CURSOR_MOVE }, }); @@ -33,7 +33,7 @@ export const useTrackCursor = (containerRef: RefObject, selfConn leave: (event: MouseEvent) => { if (!selfConnectionId) return; const { top, left } = cursorContainer.getBoundingClientRect(); - space.cursors.set({ + set({ position: { x: event.clientX - left, y: event.clientY - top }, data: { state: CURSOR_LEAVE }, }); @@ -45,10 +45,9 @@ export const useTrackCursor = (containerRef: RefObject, selfConn cursorContainer.addEventListener('mouseleave', cursorHandlers.leave); return () => { - space.cursors.unsubscribe(); cursorContainer.removeEventListener('mouseenter', cursorHandlers.enter); cursorContainer.removeEventListener('mousemove', cursorHandlers.move); cursorContainer.removeEventListener('mouseleave', cursorHandlers.leave); }; - }, [space, containerRef, selfConnectionId]); + }, [set, containerRef, selfConnectionId]); }; diff --git a/demo/src/main.tsx b/demo/src/main.tsx index c264e519..743cbbff 100644 --- a/demo/src/main.tsx +++ b/demo/src/main.tsx @@ -3,17 +3,39 @@ import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; -import { SpaceContextProvider } from './components'; import { SlidesStateContextProvider } from './components/SlidesStateContext.tsx'; +import Spaces from '@ably/spaces'; +import { SpacesProvider, SpaceProvider } from '@ably/spaces/react'; +import { Realtime } from 'ably'; +import { nanoid } from 'nanoid'; +import { generateSpaceName, getParamValueFromUrl } from './utils'; +import { AblyProvider } from 'ably/react'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); +const clientId = nanoid(); + +const client = new Realtime.Promise({ + authUrl: `/api/ably-token-request?clientId=${clientId}`, + clientId, +}); + +const spaces = new Spaces(client); +const spaceName = getParamValueFromUrl('space', generateSpaceName); + root.render( - - - - - + + + + + + + + + , ); diff --git a/docs/react.md b/docs/react.md new file mode 100644 index 00000000..69b320ee --- /dev/null +++ b/docs/react.md @@ -0,0 +1,211 @@ +# React Hooks + +> [!NOTE] +> For more information about React Hooks for Spaces, please see the official [Spaces documentation](https://ably.com/docs/spaces/react). + +Incorporate Spaces into your React application with idiomatic and user-friendly React Hooks. + +Using this module you can: + +- Interact with [Ably Spaces](https://ably.com/docs/spaces) using a React Hook. +- Subscribe to events in a space +- Retrieve the membership of a space +- Set the location of space members +- Acquire locks on components within a space +- Set the position of members' cursors in a space + +--- + +- [Compatible React Versions](#compatible-react-versions) +- [Usage](#usage) + + [useSpace](#usespace) + + [useMembers](#usemembers) + + [useLocation](#uselocation) + + [useLocks](#uselocks) + + [useLock](#uselock) + + [useCursors](#usecursors) + + [Error Handling](#error-handling) + +--- + +## Compatible React Versions + +The hooks are compatible with all versions of React above 16.8.0 + +## Usage + +Start by connecting your app to Ably using the `SpacesProvider` component. + +The `SpacesProvider` should wrap every component that needs to access Spaces. + +```jsx +import { Realtime } from "ably"; +import Spaces from "@ably/spaces"; +import { SpacesProvider, SpaceProvider } from "@ably/spaces/react"; + +const ably = new Realtime.Promise({ key: "your-ably-api-key", clientId: 'me' }); +const spaces = new Spaces(ably); + +root.render( + + + + + +) +``` + +Once you've done this, you can use the `hooks` in your code. The simplest example is as follows: + +```javascript +const { self, others } = useMembers(); +``` + +Our react hooks are designed to run on the client-side, so if you are using server-side rendering, make sure that your components which use Spaces react hooks are only rendered on the client side. + +--- + +### useSpace + +The `useSpace` hook lets you subscribe to the current Space and receive Space state events and get the current Space instance. + +```javascript +const { space } = useSpace((update) => { + console.log(update); +}); +``` + +### useMembers + +The `useMembers` hook is useful in building avatar stacks. By using the `useMembers` hook you can retrieve members of the space. +This includes members that have recently left the space, but have not yet been removed. + +```javascript +const { self, others, members } = useMembers(); +``` + +* `self` - a member’s own member object +* `others` - an array of member objects for all members other than the member themselves +* `members` - an array of all member objects, including the member themselves + +It also lets you subscribe to members entering, leaving, being +removed from the Space (after a timeout) or updating their profile information. + +```javascript +// Subscribe to all member events in a space +useMembers((memberUpdate) => { + console.log(memberUpdate); +}); + +// Subscribe to member enter events only +useMembers('enter', (memberJoined) => { + console.log(memberJoined); +}); + +// Subscribe to member leave events only +useMembers('leave', (memberLeft) => { + console.log(memberLeft); +}); + +// Subscribe to member remove events only +useMembers('remove', (memberRemoved) => { + console.log(memberRemoved); +}); + +// Subscribe to profile updates on members only +useMembers('updateProfile', (memberProfileUpdated) => { + console.log(memberProfileUpdated); +}); + +// Subscribe to all updates to members +useMembers('update', (memberUpdate) => { + console.log(memberUpdate); +}); +``` + +### useLocation + +The `useLocation` hook lets you subscribe to location events. +Location events are emitted whenever a member changes location. + +```javascript +useLocation((locationUpdate) => { + console.log(locationUpdate); +}); +``` + +`useLocation` also enables you to update current member location by using `update` method provided by hook. For example: + +```javascript +const { update } = useLocation((locationUpdate) => { + console.log(locationUpdate); +}); +``` + +### useLocks + +`useLocks` enables you to subscribe to lock events by registering a listener. Lock events are emitted whenever a lock transitions into the `locked` or `unlocked` state. + +```javascript +useLocks((lockUpdate) => { + console.log(lockUpdate); +}); +``` + +### useLock + +`useLock` returns the status of a lock and, if the lock has been acquired, the member holding that lock. + +```javascript +const { status, member } = useLock('my-lock'); +``` + +### useCursors + +`useCursors` enables you to track a member's cursor position and provide a view of all members' cursors within a space. For example: + +```javascript +// Subscribe to events published on "mousemove" by all members +const { set } = useCursors((cursorUpdate) => { + console.log(cursorUpdate); +}); + +useEffect(() => { + // Publish a your cursor position on "mousemove" including optional data + const eventListener = ({ clientX, clientY }) => { + set({ position: { x: clientX, y: clientY }, data: { color: 'red' } }); + } + + window.addEventListener('mousemove', eventListener); + + return () => { + window.removeEventListener('mousemove', eventListener); + }; +}); +``` + +If you provide `{ returnCursors: true }` as an option you can get active members cursors: + +```javascript +const { cursors } = useCursors((cursorUpdate) => { + console.log(cursorUpdate); +}, { returnCursors: true }); +``` + +--- + +### Error Handling + +`useSpace`, `useMembers`, `useLocks` and `useCursors` return connection and channel errors you may encounter, so that you can handle then within your components. This may include when a client doesn't have permission to attach to a channel, or if it loses its connection to Ably. + +```jsx +const { connectionError, channelError } = useMembers(); + +if (connectionError) { + // TODO: handle connection errors +} else if (channelError) { + // TODO: handle channel errors +} else { + return +} +``` diff --git a/package-lock.json b/package-lock.json index 20cae84d..d86e1d56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,10 @@ "@playwright/test": "^1.39.0", "@rollup/plugin-node-resolve": "^15.2.1", "@rollup/plugin-terser": "^0.4.3", + "@testing-library/react": "^14.0.0", "@types/express": "^4.17.18", + "@types/react": "^18.2.23", + "@types/react-dom": "^18.2.8", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitest/coverage-c8": "^0.33.0", @@ -25,7 +28,10 @@ "eslint-plugin-jsdoc": "^46.7.0", "eslint-plugin-security": "^1.7.1", "express": "^4.18.2", + "jsdom": "^22.1.0", "prettier": "^3.0.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", "rollup": "^3.28.0", "ts-node": "^10.9.1", "typedoc": "^0.25.2", @@ -33,7 +39,17 @@ "vitest": "^0.34.3" }, "peerDependencies": { - "ably": "^1.2.45" + "ably": "^1.2.45", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -67,6 +83,196 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", + "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -778,6 +984,84 @@ "node": ">=10" } }, + "node_modules/@testing-library/dom": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", + "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/@testing-library/react": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", + "integrity": "sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -802,6 +1086,12 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/aria-query": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.2.tgz", + "integrity": "sha512-PHKZuMN+K5qgKIWhBodXzQslTo5P+K/6LqeKXS6O/4liIDdZqaX5RXrCK++LAw+y/nptN48YmUMFiQHRSWYwtQ==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.3", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", @@ -928,6 +1218,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, + "node_modules/@types/prop-types": { + "version": "15.7.7", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.7.tgz", + "integrity": "sha512-FbtmBWCcSa2J4zL781Zf1p5YUBXQomPEcep9QZCfRfQgTxz3pJWiDFLebohZ9fFntX5ibzOkSsrJ0TEew8cAog==", + "dev": true + }, "node_modules/@types/qs": { "version": "6.9.8", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", @@ -940,6 +1236,26 @@ "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==", "dev": true }, + "node_modules/@types/react": { + "version": "18.2.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.23.tgz", + "integrity": "sha512-qHLW6n1q2+7KyBEYnrZpcsAmU/iiCh9WGCKgXvMxx89+TYdJWRjZohVIo9XTcoLhfX3+/hP0Pbulu3bCZQ9PSA==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.8.tgz", + "integrity": "sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -955,6 +1271,12 @@ "@types/node": "*" } }, + "node_modules/@types/scheduler": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.4.tgz", + "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -1357,6 +1679,12 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, "node_modules/ably": { "version": "1.2.45", "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.45.tgz", @@ -1426,6 +1754,18 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1493,6 +1833,15 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", @@ -1630,6 +1979,12 @@ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "peer": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -1690,6 +2045,18 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1923,6 +2290,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2006,6 +2385,38 @@ "node": ">= 8" } }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, + "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/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2023,6 +2434,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -2062,6 +2479,35 @@ "node": ">=6" } }, + "node_modules/deep-equal": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", + "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.1", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2102,6 +2548,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2154,6 +2609,24 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2184,6 +2657,18 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", @@ -2237,6 +2722,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -2929,6 +3434,20 @@ "node": ">=8.0.0" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3276,6 +3795,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3304,6 +3835,20 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -3317,13 +3862,26 @@ "node": ">=10.19.0" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -3402,6 +3960,22 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -3528,6 +4102,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -3579,6 +4162,12 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -3595,6 +4184,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", @@ -3656,6 +4254,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -3668,6 +4275,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -3730,6 +4350,12 @@ "node": ">=8" } }, + "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==", + "devOptional": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3751,6 +4377,69 @@ "node": ">=12.0.0" } }, + "node_modules/jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3842,6 +4531,18 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": 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==", + "devOptional": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/loupe": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", @@ -3878,6 +4579,15 @@ "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", "dev": true }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.3", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz", @@ -4106,6 +4816,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -4115,6 +4831,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -4303,6 +5035,18 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -4543,6 +5287,12 @@ "node": ">= 0.10" } }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -4577,6 +5327,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4642,12 +5398,55 @@ "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "devOptional": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "devOptional": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, "node_modules/regexp-tree": { "version": "0.1.24", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", @@ -4695,6 +5494,12 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -4780,6 +5585,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4870,6 +5681,27 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "devOptional": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -5105,6 +5937,18 @@ "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", "dev": true }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5233,6 +6077,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "node_modules/terser": { "version": "5.17.7", "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.7.tgz", @@ -5313,6 +6163,33 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -5588,6 +6465,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5606,6 +6492,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5820,6 +6716,61 @@ "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", "dev": true }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5851,6 +6802,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/which-typed-array": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", @@ -5917,6 +6883,21 @@ "async-limiter": "~1.0.0" } }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -6007,6 +6988,158 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/runtime": { + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", + "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, "@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -6413,9 +7546,67 @@ "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "peer": true, "requires": { - "defer-to-connect": "^2.0.0" + "defer-to-connect": "^2.0.0" + } + }, + "@testing-library/dom": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", + "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + } + } + }, + "@testing-library/react": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", + "integrity": "sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" } }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -6440,6 +7631,12 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "@types/aria-query": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.2.tgz", + "integrity": "sha512-PHKZuMN+K5qgKIWhBodXzQslTo5P+K/6LqeKXS6O/4liIDdZqaX5RXrCK++LAw+y/nptN48YmUMFiQHRSWYwtQ==", + "dev": true + }, "@types/body-parser": { "version": "1.19.3", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", @@ -6566,6 +7763,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, + "@types/prop-types": { + "version": "15.7.7", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.7.tgz", + "integrity": "sha512-FbtmBWCcSa2J4zL781Zf1p5YUBXQomPEcep9QZCfRfQgTxz3pJWiDFLebohZ9fFntX5ibzOkSsrJ0TEew8cAog==", + "dev": true + }, "@types/qs": { "version": "6.9.8", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", @@ -6578,6 +7781,26 @@ "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==", "dev": true }, + "@types/react": { + "version": "18.2.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.23.tgz", + "integrity": "sha512-qHLW6n1q2+7KyBEYnrZpcsAmU/iiCh9WGCKgXvMxx89+TYdJWRjZohVIo9XTcoLhfX3+/hP0Pbulu3bCZQ9PSA==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.8.tgz", + "integrity": "sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -6593,6 +7816,12 @@ "@types/node": "*" } }, + "@types/scheduler": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.4.tgz", + "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==", + "dev": true + }, "@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -6847,6 +8076,12 @@ "pretty-format": "^29.5.0" } }, + "abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, "ably": { "version": "1.2.45", "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.45.tgz", @@ -6887,6 +8122,15 @@ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -6938,6 +8182,15 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "requires": { + "deep-equal": "^2.0.5" + } + }, "array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", @@ -7036,6 +8289,12 @@ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "peer": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -7083,6 +8342,15 @@ "ms": "2.0.0" } }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -7267,6 +8535,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -7335,6 +8612,32 @@ "which": "^2.0.1" } }, + "cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "requires": { + "rrweb-cssom": "^0.6.0" + } + }, + "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 + }, + "data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "requires": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -7344,6 +8647,12 @@ "ms": "2.1.2" } }, + "decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -7370,6 +8679,32 @@ "type-detect": "^4.0.0" } }, + "deep-equal": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", + "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.1", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + } + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -7398,6 +8733,12 @@ "object-keys": "^1.1.1" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7434,6 +8775,21 @@ "esutils": "^2.0.2" } }, + "dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "requires": { + "webidl-conversions": "^7.0.0" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7461,6 +8817,12 @@ "once": "^1.4.0" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, "es-abstract": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", @@ -7508,6 +8870,23 @@ "which-typed-array": "^1.1.10" } }, + "es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + } + }, "es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -8073,6 +9452,17 @@ "signal-exit": "^3.0.2" } }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -8311,6 +9701,15 @@ "has-symbols": "^1.0.2" } }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -8336,6 +9735,17 @@ "toidentifier": "1.0.1" } }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, "http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -8346,13 +9756,23 @@ "resolve-alpn": "^1.0.0" } }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "ignore": { @@ -8410,6 +9830,16 @@ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, "is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -8494,6 +9924,12 @@ "is-extglob": "^2.1.1" } }, + "is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true + }, "is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -8527,6 +9963,12 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -8537,6 +9979,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true + }, "is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", @@ -8577,6 +10025,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true + }, "is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -8586,6 +10040,16 @@ "call-bind": "^1.0.2" } }, + "is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, "isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -8636,6 +10100,12 @@ "istanbul-lib-report": "^3.0.0" } }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "devOptional": true + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -8651,6 +10121,46 @@ "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", "dev": true }, + "jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "requires": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "dependencies": { + "ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "dev": true, + "requires": {} + } + } + }, "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -8724,6 +10234,15 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "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==", + "devOptional": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "loupe": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", @@ -8754,6 +10273,12 @@ "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", "dev": true }, + "lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true + }, "magic-string": { "version": "0.30.3", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz", @@ -8915,12 +10440,28 @@ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "peer": true }, + "nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", "dev": true }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -9054,6 +10595,15 @@ "callsites": "^3.0.0" } }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -9207,6 +10757,12 @@ "ipaddr.js": "1.9.1" } }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -9232,6 +10788,12 @@ "side-channel": "^1.0.4" } }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9269,6 +10831,36 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "devOptional": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "devOptional": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" } }, "react-is": { @@ -9277,6 +10869,12 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, "regexp-tree": { "version": "0.1.24", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", @@ -9306,6 +10904,12 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -9362,6 +10966,12 @@ "fsevents": "~2.3.2" } }, + "rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9415,6 +11025,24 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "devOptional": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -9621,6 +11249,15 @@ "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", "dev": true }, + "stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "requires": { + "internal-slot": "^1.0.4" + } + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -9710,6 +11347,12 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "terser": { "version": "5.17.7", "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.7.tgz", @@ -9772,6 +11415,27 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + } + }, + "tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "requires": { + "punycode": "^2.3.0" + } + }, "ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -9962,6 +11626,12 @@ "which-boxed-primitive": "^1.0.2" } }, + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -9977,6 +11647,16 @@ "punycode": "^2.1.0" } }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -10084,6 +11764,46 @@ "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", "dev": true }, + "w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "requires": { + "xml-name-validator": "^4.0.0" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + } + }, + "whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true + }, + "whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "requires": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -10106,6 +11826,18 @@ "is-symbol": "^1.0.3" } }, + "which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "requires": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + } + }, "which-typed-array": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", @@ -10154,6 +11886,18 @@ "async-limiter": "~1.0.0" } }, + "xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index eb8e1430..66381f88 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,6 @@ "examples:locks": "ts-node ./examples/locks.ts", "docs": "typedoc" }, - "exports": { - "import": "./dist/mjs/index.js", - "require": "./dist/cjs/index.js" - }, "repository": { "type": "git", "url": "git+https://github.com/ably/spaces.git" @@ -45,7 +41,10 @@ "@playwright/test": "^1.39.0", "@rollup/plugin-node-resolve": "^15.2.1", "@rollup/plugin-terser": "^0.4.3", + "@testing-library/react": "^14.0.0", "@types/express": "^4.17.18", + "@types/react": "^18.2.23", + "@types/react-dom": "^18.2.8", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitest/coverage-c8": "^0.33.0", @@ -55,7 +54,10 @@ "eslint-plugin-jsdoc": "^46.7.0", "eslint-plugin-security": "^1.7.1", "express": "^4.18.2", + "jsdom": "^22.1.0", "prettier": "^3.0.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", "rollup": "^3.28.0", "ts-node": "^10.9.1", "typedoc": "^0.25.2", @@ -66,6 +68,16 @@ "nanoid": "^5.0.2" }, "peerDependencies": { - "ably": "^1.2.45" + "ably": "^1.2.45", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } } diff --git a/react/package.json b/react/package.json new file mode 100644 index 00000000..fb51e68a --- /dev/null +++ b/react/package.json @@ -0,0 +1,8 @@ +{ + "name": "@ably/spaces/react", + "type": "module", + "main": "../dist/cjs/react/index.js", + "module": "../dist/mjs/react/index.js", + "types": "../dist/mjs/react/index.d.ts", + "sideEffects": false +} diff --git a/src/react/contexts/SpaceContext.tsx b/src/react/contexts/SpaceContext.tsx new file mode 100644 index 00000000..1277b003 --- /dev/null +++ b/src/react/contexts/SpaceContext.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { useSpaces } from '../useSpaces.js'; + +import type { Space, SpaceOptions } from '../../'; + +export const SpaceContext = React.createContext(undefined); + +interface SpaceProviderProps { + name: string; + options?: Partial; + children?: React.ReactNode | React.ReactNode[] | null; +} +export const SpaceProvider: React.FC = ({ name, options, children }) => { + const [space, setSpace] = React.useState(undefined); + const spaces = useSpaces(); + const optionsRef = React.useRef(options); + + React.useEffect(() => { + optionsRef.current = options; + }, [options]); + + React.useEffect(() => { + let ignore: boolean = false; + + const init = async () => { + if (!spaces) { + throw new Error( + 'Could not find spaces client in context. ' + + 'Make sure your spaces hooks are called inside an ', + ); + } + + const spaceInstance = await spaces.get(name, optionsRef.current); + + if (spaceInstance && !space && !ignore) { + setSpace(spaceInstance); + } + }; + + init(); + + return () => { + ignore = true; + }; + }, [name, spaces]); + + return {children}; +}; diff --git a/src/react/contexts/SpacesContext.tsx b/src/react/contexts/SpacesContext.tsx new file mode 100644 index 00000000..3f685932 --- /dev/null +++ b/src/react/contexts/SpacesContext.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import type Spaces from '../../'; + +export const SpacesContext = React.createContext(undefined); + +interface SpacesProviderProps { + client: Spaces; + children?: React.ReactNode | React.ReactNode[] | null; +} +export const SpacesProvider: React.FC = ({ client: spaces, children }) => { + return {children}; +}; diff --git a/src/react/index.ts b/src/react/index.ts new file mode 100644 index 00000000..d7768f26 --- /dev/null +++ b/src/react/index.ts @@ -0,0 +1,9 @@ +export { SpacesProvider } from './contexts/SpacesContext.js'; +export { SpaceProvider } from './contexts/SpaceContext.js'; +export { useSpaces } from './useSpaces.js'; +export { useSpace } from './useSpace.js'; +export { useMembers } from './useMembers.js'; +export { useLocations } from './useLocations.js'; +export { useLocks } from './useLocks.js'; +export { useLock } from './useLock.js'; +export { useCursors } from './useCursors.js'; diff --git a/src/react/types.ts b/src/react/types.ts new file mode 100644 index 00000000..90a16e00 --- /dev/null +++ b/src/react/types.ts @@ -0,0 +1,12 @@ +import type { SpaceMember } from '..'; + +export interface UseSpaceOptions { + /** + * Skip parameter makes the hook skip execution - + * this is useful in order to conditionally register a subscription to + * an EventListener (needed because it's not possible to conditionally call a hook in react) + */ + skip?: boolean; +} + +export type UseSpaceCallback = (params: { members: SpaceMember[] }) => void; diff --git a/src/react/useChannelState.ts b/src/react/useChannelState.ts new file mode 100644 index 00000000..a3a39b12 --- /dev/null +++ b/src/react/useChannelState.ts @@ -0,0 +1,38 @@ +import { useState } from 'react'; +import { useEventListener } from './useEventListener.js'; + +import type { Types } from 'ably'; + +type ChannelStateListener = (stateChange: Types.ChannelStateChange) => void; + +const failedStateEvents: Types.ChannelState[] = ['suspended', 'failed', 'detached']; +const successStateEvents: Types.ChannelState[] = ['attached']; + +/** + * todo use `ably/react` hooks instead + */ +export const useChannelState = ( + emitter?: Types.EventEmitter, +) => { + const [channelError, setChannelError] = useState(null); + + useEventListener( + emitter, + (stateChange) => { + if (stateChange.reason) { + setChannelError(stateChange.reason); + } + }, + failedStateEvents, + ); + + useEventListener( + emitter, + () => { + setChannelError(null); + }, + successStateEvents, + ); + + return channelError; +}; diff --git a/src/react/useConnectionState.ts b/src/react/useConnectionState.ts new file mode 100644 index 00000000..ff8271f6 --- /dev/null +++ b/src/react/useConnectionState.ts @@ -0,0 +1,35 @@ +import { useState } from 'react'; +import { useEventListener } from './useEventListener.js'; + +import type { Types } from 'ably'; + +type ConnectionStateListener = (stateChange: Types.ConnectionStateChange) => void; + +const failedStateEvents: Types.ConnectionState[] = ['suspended', 'failed', 'disconnected']; +const successStateEvents: Types.ConnectionState[] = ['connected', 'closed']; + +export const useConnectionState = ( + emitter?: Types.EventEmitter, +) => { + const [connectionError, setConnectionError] = useState(null); + + useEventListener( + emitter, + (stateChange) => { + if (stateChange.reason) { + setConnectionError(stateChange.reason); + } + }, + failedStateEvents, + ); + + useEventListener( + emitter, + () => { + setConnectionError(null); + }, + successStateEvents, + ); + + return connectionError; +}; diff --git a/src/react/useCursors.test.tsx b/src/react/useCursors.test.tsx new file mode 100644 index 00000000..b7020e33 --- /dev/null +++ b/src/react/useCursors.test.tsx @@ -0,0 +1,125 @@ +/** + * @vitest-environment jsdom + */ + +import React from 'react'; +import { Realtime } from 'ably/promises'; +import { it, beforeEach, describe, expect, vi } from 'vitest'; +import { waitFor, renderHook } from '@testing-library/react'; +import { SpacesProvider } from './contexts/SpacesContext.js'; +import { SpaceProvider } from './contexts/SpaceContext.js'; +import Spaces from '../index.js'; +import Space from '../Space.js'; +import { createPresenceEvent } from '../utilities/test/fakes.js'; +import { useCursors } from './useCursors.js'; +import type { Types } from 'ably'; + +interface SpaceTestContext { + spaces: Spaces; + space: Space; + presenceMap: Map; +} + +vi.mock('ably/promises'); +vi.mock('nanoid'); + +describe('useCursors', () => { + beforeEach((context) => { + const client = new Realtime({ key: 'spaces-test' }); + context.spaces = new Spaces(client); + context.presenceMap = new Map(); + + const space = new Space('test', client); + const presence = space.channel.presence; + + context.space = space; + + vi.spyOn(context.spaces, 'get').mockImplementation(async () => space); + + vi.spyOn(presence, 'get').mockImplementation(async () => { + return Array.from(context.presenceMap.values()); + }); + }); + + it('invokes callback on cursor set', async ({ space, spaces, presenceMap }) => { + const callbackSpy = vi.fn(); + // @ts-ignore + const { result } = renderHook(() => useCursors(callbackSpy), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => { + expect(result.current.space).toBe(space); + }); + + await createPresenceEvent(space, presenceMap, 'enter'); + + const dispensing = space.cursors.cursorDispensing; + + const fakeMessage = { + connectionId: '1', + clientId: '1', + encoding: 'encoding', + extras: null, + id: '1', + name: 'fake', + timestamp: 1, + data: [{ cursor: { position: { x: 1, y: 1 } } }], + }; + + dispensing.processBatch(fakeMessage); + + await waitFor(() => { + expect(callbackSpy).toHaveBeenCalledWith({ + position: { x: 1, y: 1 }, + data: undefined, + clientId: '1', + connectionId: '1', + }); + }); + }); + + it('returns cursors', async ({ space, spaces, presenceMap }) => { + // @ts-ignore + const { result } = renderHook(() => useCursors({ returnCursors: true }), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => { + expect(result.current.space).toBe(space); + }); + + await createPresenceEvent(space, presenceMap, 'enter'); + await createPresenceEvent(space, presenceMap, 'enter', { clientId: '2', connectionId: '2' }); + const [member] = await space.members.getOthers(); + + const dispensing = space.cursors.cursorDispensing; + + const fakeMessage = { + connectionId: '2', + clientId: '2', + encoding: 'encoding', + extras: null, + id: '1', + name: 'fake', + timestamp: 1, + data: [{ cursor: { position: { x: 1, y: 1 } } }], + }; + + dispensing.processBatch(fakeMessage); + + await waitFor(() => { + expect(result.current.cursors).toEqual({ + '2': { member, cursorUpdate: { clientId: '2', connectionId: '2', position: { x: 1, y: 1 } } }, + }); + }); + }); +}); diff --git a/src/react/useCursors.ts b/src/react/useCursors.ts new file mode 100644 index 00000000..72fd9b28 --- /dev/null +++ b/src/react/useCursors.ts @@ -0,0 +1,102 @@ +import { useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { SpaceContext } from './contexts/SpaceContext.js'; +import { useMembers } from './useMembers.js'; +import { useChannelState } from './useChannelState.js'; +import { useConnectionState } from './useConnectionState.js'; +import { isFunction } from '../utilities/is.js'; + +import type { CursorUpdate, SpaceMember } from '../types.js'; +import type { ErrorInfo } from 'ably'; +import type Cursors from '../Cursors.js'; +import type { UseSpaceOptions } from './types.js'; +import type { Space } from '..'; + +interface UseCursorsOptions extends UseSpaceOptions { + /** + * Whether to return the cursors object described in UseCursorsResult, defaults to false + */ + returnCursors?: boolean; +} + +interface UseCursorsResult { + space?: Space; + connectionError: ErrorInfo | null; + channelError: ErrorInfo | null; + set?: Cursors['set']; + /** + * if UseCursorsOptions.returnCursors is truthy; a map from connectionId to associated space member and their cursor update + */ + cursors: Record; +} + +type UseCursorsCallback = (params: CursorUpdate) => void; + +/** + * Registers a subscription on the `Space.cursors` object + */ +function useCursors(options?: UseCursorsOptions): UseCursorsResult; +function useCursors(callback: UseCursorsCallback, options?: UseCursorsOptions): UseCursorsResult; +function useCursors( + callbackOrOptions?: UseCursorsCallback | UseCursorsOptions, + optionsOrNothing?: UseCursorsOptions, +): UseCursorsResult { + const space = useContext(SpaceContext); + const [cursors, setCursors] = useState>({}); + const { members } = useMembers(); + const channelError = useChannelState(space?.cursors.channel); + const connectionError = useConnectionState(); + + const connectionIdToMember: Record = useMemo(() => { + return members.reduce( + (acc, member) => { + acc[member.connectionId] = member; + return acc; + }, + {} as Record, + ); + }, [members]); + + const callback = isFunction(callbackOrOptions) ? callbackOrOptions : undefined; + const options = isFunction(callbackOrOptions) ? optionsOrNothing : callbackOrOptions; + + const callbackRef = useRef(callback); + const optionsRef = useRef(options); + + useEffect(() => { + callbackRef.current = callback; + optionsRef.current = options; + }, [callback, options]); + + useEffect(() => { + if (!space || !connectionIdToMember) return; + + const listener: UseCursorsCallback = (cursorUpdate) => { + if (!optionsRef.current?.skip) callbackRef.current?.(cursorUpdate); + + const { connectionId } = cursorUpdate; + + if (connectionId === space?.connectionId || !optionsRef.current?.returnCursors) return; + + setCursors((currentCursors) => ({ + ...currentCursors, + [connectionId]: { member: connectionIdToMember[connectionId], cursorUpdate }, + })); + }; + + space.cursors.subscribe('update', listener); + + return () => { + space.cursors.unsubscribe('update', listener); + }; + }, [space, connectionIdToMember]); + + return { + space, + connectionError, + channelError, + set: space?.cursors.set.bind(space?.cursors), + cursors, + }; +} + +export { useCursors }; diff --git a/src/react/useEventListener.ts b/src/react/useEventListener.ts new file mode 100644 index 00000000..4b69665b --- /dev/null +++ b/src/react/useEventListener.ts @@ -0,0 +1,43 @@ +import { useEffect, useRef } from 'react'; + +import type { Types } from 'ably'; + +type EventListener = (stateChange: T) => void; + +/** + * todo use `ably/react` hooks instead + */ +export const useEventListener = < + S extends Types.ConnectionState | Types.ChannelState, + C extends Types.ConnectionStateChange | Types.ChannelStateChange, +>( + emitter?: Types.EventEmitter, C, S>, + listener?: EventListener, + event?: S | S[], +) => { + const listenerRef = useRef(listener); + + useEffect(() => { + listenerRef.current = listener; + }, [listener]); + + useEffect(() => { + const callback: EventListener = (stateChange) => { + listenerRef.current?.(stateChange); + }; + + if (event) { + emitter?.on(event as S, callback); + } else { + emitter?.on(callback); + } + + return () => { + if (event) { + emitter?.off(event as S, callback); + } else { + emitter?.off(callback); + } + }; + }, [emitter, event]); +}; diff --git a/src/react/useLocations.test.tsx b/src/react/useLocations.test.tsx new file mode 100644 index 00000000..a569379c --- /dev/null +++ b/src/react/useLocations.test.tsx @@ -0,0 +1,81 @@ +/** + * @vitest-environment jsdom + */ + +import React from 'react'; +import { Realtime } from 'ably/promises'; +import { it, beforeEach, describe, expect, vi } from 'vitest'; +import { waitFor, renderHook } from '@testing-library/react'; +import { SpacesProvider } from './contexts/SpacesContext.js'; +import { SpaceProvider } from './contexts/SpaceContext.js'; +import type { Types } from 'ably'; +import Spaces from '../index.js'; +import { createLocationUpdate, createPresenceEvent } from '../utilities/test/fakes.js'; +import Space from '../Space.js'; +import { useLocations } from './useLocations.js'; + +interface SpaceTestContext { + spaces: Spaces; + space: Space; + presenceMap: Map; +} + +vi.mock('ably/promises'); +vi.mock('nanoid'); + +describe('useLocations', () => { + beforeEach((context) => { + const client = new Realtime({ key: 'spaces-test' }); + context.spaces = new Spaces(client); + context.presenceMap = new Map(); + + const space = new Space('test', client); + const presence = space.channel.presence; + + context.space = space; + + vi.spyOn(context.spaces, 'get').mockImplementation(async () => space); + + vi.spyOn(presence, 'get').mockImplementation(async () => { + return Array.from(context.presenceMap.values()); + }); + }); + + it('invokes callback with new location', async ({ space, spaces, presenceMap }) => { + const callbackSpy = vi.fn(); + // @ts-ignore + const { result } = renderHook(() => useLocations(callbackSpy), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => { + expect(result.current.space).toBe(space); + }); + + await createPresenceEvent(space, presenceMap, 'enter'); + const member = await space.members.getSelf(); + await createPresenceEvent(space, presenceMap, 'update', { + data: createLocationUpdate({ current: 'location1' }), + }); + + await waitFor(() => { + expect(callbackSpy).toHaveBeenCalledWith( + expect.objectContaining({ + member: { + ...member, + lastEvent: { + name: 'update', + timestamp: 1, + }, + location: 'location1', + }, + previousLocation: null, + }), + ); + }); + }); +}); diff --git a/src/react/useLocations.ts b/src/react/useLocations.ts new file mode 100644 index 00000000..bbce2c5c --- /dev/null +++ b/src/react/useLocations.ts @@ -0,0 +1,77 @@ +import { useContext, useEffect, useRef } from 'react'; +import { SpaceContext } from './contexts/SpaceContext.js'; +import { isArray, isFunction, isString } from '../utilities/is.js'; + +import type Locations from '../Locations.js'; +import type { SpaceMember } from '../types.js'; +import type { UseSpaceOptions } from './types.js'; +import type { Space } from '../'; + +interface UseLocationsResult { + space?: Space; + update?: Locations['set']; +} + +type UseLocationCallback = (locationUpdate: { member: SpaceMember }) => void; + +export type LocationsEvent = 'update'; + +function useLocations(callback?: UseLocationCallback, options?: UseSpaceOptions): UseLocationsResult; +function useLocations( + event: LocationsEvent | LocationsEvent[], + callback: UseLocationCallback, + options?: UseSpaceOptions, +): UseLocationsResult; + +/* + * Registers a subscription on the `Space.locations` object + */ +function useLocations( + eventOrCallback?: LocationsEvent | LocationsEvent[] | UseLocationCallback, + callbackOrOptions?: UseLocationCallback | UseSpaceOptions, + optionsOrNothing?: UseSpaceOptions, +): UseLocationsResult { + const space = useContext(SpaceContext); + const locations = space?.locations; + + const callback = + isString(eventOrCallback) || isArray(eventOrCallback) + ? (callbackOrOptions as UseLocationCallback) + : eventOrCallback; + + const options = isFunction(callbackOrOptions) ? optionsOrNothing : callbackOrOptions; + + const callbackRef = useRef(callback); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + if (callbackRef.current && locations && !options?.skip) { + const listener: UseLocationCallback = (params) => { + callbackRef.current?.(params); + }; + if (!isFunction(eventOrCallback) && eventOrCallback) { + locations.subscribe(eventOrCallback, listener); + } else { + locations.subscribe(listener); + } + + return () => { + if (!isFunction(eventOrCallback) && eventOrCallback) { + locations.unsubscribe(eventOrCallback, listener); + } else { + locations.unsubscribe(listener); + } + }; + } + }, [locations, options?.skip]); + + return { + space, + update: locations?.set.bind(locations), + }; +} + +export { useLocations }; diff --git a/src/react/useLock.ts b/src/react/useLock.ts new file mode 100644 index 00000000..0701f1d5 --- /dev/null +++ b/src/react/useLock.ts @@ -0,0 +1,55 @@ +import { useContext, useEffect, useState } from 'react'; +import { SpaceContext } from './contexts/SpaceContext.js'; + +import type { LockStatus, SpaceMember, Lock } from '../types.js'; + +interface UseLockResult { + status: LockStatus | null; + member: SpaceMember | null; +} + +/* + * Returns the status of a lock and, if it has been acquired, the member holding the lock + */ +export function useLock(lockId: string): UseLockResult { + const space = useContext(SpaceContext); + const [status, setStatus] = useState(null); + const [member, setMember] = useState(null); + + const initialized = status !== null; + + useEffect(() => { + if (!space) return; + + const handler = (lock: Lock) => { + if (lock.id !== lockId) return; + + if (lock.status === 'unlocked') { + setStatus(null); + setMember(null); + } else { + setStatus(lock.status); + setMember(lock.member); + } + }; + + space.locks.subscribe('update', handler); + + return () => { + space?.locks.unsubscribe('update', handler); + }; + }, [space, lockId]); + + useEffect(() => { + if (initialized || !space) return; + + const lock = space?.locks.get(lockId); + + if (lock) { + setMember(lock.member); + setStatus(lock.status); + } + }, [initialized, space]); + + return { status, member }; +} diff --git a/src/react/useLocks.test.tsx b/src/react/useLocks.test.tsx new file mode 100644 index 00000000..8ad080d7 --- /dev/null +++ b/src/react/useLocks.test.tsx @@ -0,0 +1,85 @@ +/** + * @vitest-environment jsdom + */ + +import React from 'react'; +import { Realtime } from 'ably/promises'; +import { it, beforeEach, describe, expect, vi } from 'vitest'; +import { waitFor, renderHook } from '@testing-library/react'; +import { SpacesProvider } from './contexts/SpacesContext.js'; +import { SpaceProvider } from './contexts/SpaceContext.js'; +import type { Types } from 'ably'; +import Spaces from '../index.js'; +import { createPresenceEvent } from '../utilities/test/fakes.js'; +import Space from '../Space.js'; +import { useLocks } from './useLocks.js'; + +interface SpaceTestContext { + spaces: Spaces; + space: Space; + presenceMap: Map; +} + +vi.mock('ably/promises'); +vi.mock('nanoid'); + +describe('useLocks', () => { + beforeEach((context) => { + const client = new Realtime({ key: 'spaces-test' }); + context.spaces = new Spaces(client); + context.presenceMap = new Map(); + + const space = new Space('test', client); + const presence = space.channel.presence; + + context.space = space; + + vi.spyOn(context.spaces, 'get').mockImplementation(async () => space); + + vi.spyOn(presence, 'get').mockImplementation(async () => { + return Array.from(context.presenceMap.values()); + }); + }); + + it('invokes callback on lock', async ({ space, spaces, presenceMap }) => { + const callbackSpy = vi.fn(); + // @ts-ignore + const { result } = renderHook(() => useLocks(callbackSpy), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => { + expect(result.current.space).toBe(space); + }); + + await createPresenceEvent(space, presenceMap, 'enter'); + const member = await space.members.getSelf(); + + const msg = Realtime.PresenceMessage.fromValues({ + action: 'update', + connectionId: member.connectionId, + extras: { + locks: [ + { + id: 'lockID', + status: 'pending', + timestamp: Date.now(), + }, + ], + }, + }); + await space.locks.processPresenceMessage(msg); + + expect(callbackSpy).toHaveBeenCalledWith( + expect.objectContaining({ + member: member, + id: 'lockID', + status: 'locked', + }), + ); + }); +}); diff --git a/src/react/useLocks.ts b/src/react/useLocks.ts new file mode 100644 index 00000000..88da80ad --- /dev/null +++ b/src/react/useLocks.ts @@ -0,0 +1,65 @@ +import { useEffect, useRef } from 'react'; +import { isArray, isFunction, isString } from '../utilities/is.js'; +import { useSpace, type UseSpaceResult } from './useSpace.js'; + +import type { UseSpaceOptions } from './types.js'; +import type { LocksEventMap } from '../Locks.js'; +import type { Lock } from '../types.js'; + +type LocksEvent = keyof LocksEventMap; + +type UseLocksCallback = (params: Lock) => void; + +/* + * Registers a subscription on the `Space.locks` object + */ +function useLocks(callback?: UseLocksCallback, options?: UseSpaceOptions): UseSpaceResult; +function useLocks( + event: LocksEvent | LocksEvent[], + callback: UseLocksCallback, + options?: UseSpaceOptions, +): UseSpaceResult; +function useLocks( + eventOrCallback?: LocksEvent | LocksEvent[] | UseLocksCallback, + callbackOrOptions?: UseLocksCallback | UseSpaceOptions, + optionsOrNothing?: UseSpaceOptions, +): UseSpaceResult { + const spaceContext = useSpace(); + const { space } = spaceContext; + + const callback = + isString(eventOrCallback) || isArray(eventOrCallback) ? (callbackOrOptions as UseLocksCallback) : eventOrCallback; + + const options = isFunction(callbackOrOptions) ? optionsOrNothing : callbackOrOptions; + + const callbackRef = useRef(callback); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + if (callbackRef.current && space?.locks && !options?.skip) { + const listener: UseLocksCallback = (params) => { + callbackRef.current?.(params); + }; + if (!isFunction(eventOrCallback) && eventOrCallback) { + space?.locks.subscribe(eventOrCallback, listener); + } else { + space?.locks.subscribe(listener); + } + + return () => { + if (!isFunction(eventOrCallback) && eventOrCallback) { + space?.locks.unsubscribe(eventOrCallback, listener); + } else { + space?.locks.unsubscribe(listener); + } + }; + } + }, [space?.locks, options?.skip]); + + return spaceContext; +} + +export { useLocks }; diff --git a/src/react/useMembers.test.tsx b/src/react/useMembers.test.tsx new file mode 100644 index 00000000..00653f4e --- /dev/null +++ b/src/react/useMembers.test.tsx @@ -0,0 +1,116 @@ +/** + * @vitest-environment jsdom + */ + +import React from 'react'; +import { Realtime } from 'ably/promises'; +import { it, beforeEach, describe, expect, vi } from 'vitest'; +import { waitFor, renderHook } from '@testing-library/react'; +import { SpacesProvider } from './contexts/SpacesContext.js'; +import { SpaceProvider } from './contexts/SpaceContext.js'; +import type { Types } from 'ably'; +import Spaces from '../index.js'; +import { useMembers } from './useMembers.js'; +import { createLocationUpdate, createPresenceEvent } from '../utilities/test/fakes.js'; +import Space from '../Space.js'; + +interface SpaceTestContext { + spaces: Spaces; + space: Space; + presenceMap: Map; +} + +vi.mock('ably/promises'); +vi.mock('nanoid'); + +describe('useMembers', () => { + beforeEach((context) => { + const client = new Realtime({ key: 'spaces-test' }); + context.spaces = new Spaces(client); + context.presenceMap = new Map(); + + const space = new Space('test', client); + const presence = space.channel.presence; + + context.space = space; + + vi.spyOn(context.spaces, 'get').mockImplementation(async () => space); + + vi.spyOn(presence, 'get').mockImplementation(async () => { + return Array.from(context.presenceMap.values()); + }); + }); + + it('invokes callback with enter and update on enter presence events', async ({ + space, + spaces, + presenceMap, + }) => { + const callbackSpy = vi.fn(); + // @ts-ignore + const { result } = renderHook(() => useMembers(['enter', 'leave'], callbackSpy), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => { + expect(result.current.space).toBe(space); + }); + + await createPresenceEvent(space, presenceMap, 'enter'); + const member = await space.members.getSelf(); + + await waitFor(() => { + expect(callbackSpy).toHaveBeenCalledTimes(1); + expect(result.current.members).toEqual([member]); + expect(result.current.others).toEqual([]); + expect(result.current.self).toEqual(member); + expect(result.current.self.location).toBe(null); + }); + + await createPresenceEvent(space, presenceMap, 'update', { + data: createLocationUpdate({ current: 'location1' }), + }); + + await waitFor(() => { + expect(result.current.self.location).toBe('location1'); + // callback hasn't been invoked + expect(callbackSpy).toHaveBeenCalledTimes(1); + }); + + await createPresenceEvent(space, presenceMap, 'leave'); + await waitFor(() => { + // callback has invoked + expect(callbackSpy).toHaveBeenCalledTimes(2); + }); + }); + + it('skips callback if skip option provided', async ({ space, spaces, presenceMap }) => { + const callbackSpy = vi.fn(); + // @ts-ignore + const { result } = renderHook(() => useMembers(callbackSpy, { skip: true }), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => { + expect(result.current.space).toBe(space); + }); + + await createPresenceEvent(space, presenceMap, 'enter'); + const member = await space.members.getSelf(); + + await waitFor(() => { + expect(result.current.members).toEqual([member]); + expect(result.current.others).toEqual([]); + expect(result.current.self).toEqual(member); + expect(callbackSpy).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/src/react/useMembers.ts b/src/react/useMembers.ts new file mode 100644 index 00000000..43da8b34 --- /dev/null +++ b/src/react/useMembers.ts @@ -0,0 +1,122 @@ +import { useEffect, useRef, useState } from 'react'; +import { useSpace } from './useSpace.js'; +import { isArray, isFunction, isString } from '../utilities/is.js'; + +import type { ErrorInfo } from 'ably'; +import type { Space, SpaceMember } from '..'; +import type { UseSpaceOptions } from './types.js'; +import type { MembersEventMap } from '../Members.js'; + +interface UseMembersResult { + space?: Space; + /** + * All members present in the space + */ + members: SpaceMember[]; + /** + * All members present in the space excluding the member associated with the spaces client + */ + others: SpaceMember[]; + /** + * The member associated with the spaces client + */ + self: SpaceMember | null; + channelError: ErrorInfo | null; + connectionError: ErrorInfo | null; +} + +type UseMembersCallback = (params: SpaceMember) => void; + +type MembersEvent = keyof MembersEventMap; + +function useMembers(callback?: UseMembersCallback, options?: UseSpaceOptions): UseMembersResult; +function useMembers( + event: MembersEvent | MembersEvent[], + callback: UseMembersCallback, + options?: UseSpaceOptions, +): UseMembersResult; + +function useMembers( + eventOrCallback?: MembersEvent | MembersEvent[] | UseMembersCallback, + callbackOrOptions?: UseMembersCallback | UseSpaceOptions, + optionsOrNothing?: UseSpaceOptions, +): UseMembersResult { + const { space, connectionError, channelError } = useSpace(); + const [members, setMembers] = useState([]); + const [others, setOthers] = useState([]); + const [self, setSelf] = useState(null); + + const callback = + isString(eventOrCallback) || isArray(eventOrCallback) ? (callbackOrOptions as UseMembersCallback) : eventOrCallback; + + const options = isFunction(callbackOrOptions) ? optionsOrNothing : callbackOrOptions; + + const callbackRef = useRef(callback); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + if (callbackRef.current && space?.members && !options?.skip) { + const listener: UseMembersCallback = (params) => { + callbackRef.current?.(params); + }; + if (!isFunction(eventOrCallback) && eventOrCallback) { + space?.members.subscribe(eventOrCallback, listener); + } else { + space?.members.subscribe(listener); + } + + return () => { + if (!isFunction(eventOrCallback) && eventOrCallback) { + space?.members.unsubscribe(eventOrCallback, listener); + } else { + space?.members.unsubscribe(listener); + } + }; + } + }, [space?.members, options?.skip]); + + useEffect(() => { + if (!space) return; + let ignore: boolean = false; + + const updateState = (updatedSelf: SpaceMember | null, updatedMembers: SpaceMember[]) => { + if (ignore) return; + setSelf(updatedSelf); + setMembers([...updatedMembers]); + setOthers(updatedMembers.filter((member) => member.connectionId !== updatedSelf?.connectionId)); + }; + + const handler = async ({ members: updatedMembers }: { members: SpaceMember[] }) => { + const updatedSelf = await space.members.getSelf(); + updateState(updatedSelf, updatedMembers); + }; + + const init = async () => { + const initSelf = await space.members.getSelf(); + const initMembers = await space.members.getAll(); + updateState(initSelf, initMembers); + space.subscribe('update', handler); + }; + + init(); + + return () => { + ignore = true; + space.unsubscribe('update', handler); + }; + }, [space]); + + return { + space, + members, + others, + self, + connectionError, + channelError, + }; +} + +export { useMembers }; diff --git a/src/react/useSpace.test.tsx b/src/react/useSpace.test.tsx new file mode 100644 index 00000000..af6baf93 --- /dev/null +++ b/src/react/useSpace.test.tsx @@ -0,0 +1,48 @@ +/** + * @vitest-environment jsdom + */ + +import React from 'react'; +import { Realtime } from 'ably/promises'; +import { it, beforeEach, describe, expect, vi } from 'vitest'; +import { waitFor, renderHook } from '@testing-library/react'; +import { SpacesProvider } from './contexts/SpacesContext.js'; +import { SpaceProvider } from './contexts/SpaceContext.js'; +import Spaces from '../index.js'; +import { useSpace } from './useSpace.js'; + +interface SpaceTestContext { + spaces: Spaces; +} + +vi.mock('ably/promises'); +vi.mock('nanoid'); + +describe('useSpace', () => { + beforeEach((context) => { + const client = new Realtime({ key: 'spaces-test' }); + context.spaces = new Spaces(client); + }); + + it('creates and retrieves space successfully', async ({ spaces }) => { + const spy = vi.spyOn(spaces, 'get'); + + // @ts-ignore + const { result } = renderHook(() => useSpace(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('spaces-test', undefined); + + const space = await spaces.get('spaces-test'); + + await waitFor(() => { + expect(result.current.space).toBe(space); + }); + }); +}); diff --git a/src/react/useSpace.ts b/src/react/useSpace.ts new file mode 100644 index 00000000..edc34135 --- /dev/null +++ b/src/react/useSpace.ts @@ -0,0 +1,48 @@ +import { useContext, useEffect, useRef } from 'react'; +import { SpaceContext } from './contexts/SpaceContext.js'; +import { useChannelState } from './useChannelState.js'; +import { useConnectionState } from './useConnectionState.js'; + +import type { ErrorInfo } from 'ably'; +import type { Space } from '..'; +import type { UseSpaceCallback, UseSpaceOptions } from './types.js'; + +export interface UseSpaceResult { + space?: Space; + enter?: Space['enter']; + leave?: Space['leave']; + updateProfileData?: Space['updateProfileData']; + connectionError: ErrorInfo | null; + channelError: ErrorInfo | null; +} + +export const useSpace = (callback?: UseSpaceCallback, options?: UseSpaceOptions): UseSpaceResult => { + const space = useContext(SpaceContext); + const callbackRef = useRef(callback); + + const channelError = useChannelState(space?.channel); + const connectionError = useConnectionState(); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + if (callbackRef.current && space && !options?.skip) { + const listener: UseSpaceCallback = (params) => { + callbackRef.current?.(params); + }; + space.subscribe('update', listener); + return () => space.unsubscribe('update', listener); + } + }, [space, options?.skip]); + + return { + space, + enter: space?.enter.bind(space), + leave: space?.leave.bind(space), + updateProfileData: space?.updateProfileData.bind(space), + connectionError, + channelError, + }; +}; diff --git a/src/react/useSpaces.ts b/src/react/useSpaces.ts new file mode 100644 index 00000000..8a65b915 --- /dev/null +++ b/src/react/useSpaces.ts @@ -0,0 +1,6 @@ +import { useContext } from 'react'; +import { SpacesContext } from './contexts/SpacesContext.js'; + +export const useSpaces = () => { + return useContext(SpacesContext); +}; diff --git a/tsconfig.base.json b/tsconfig.base.json index 13c51ac7..ed980f40 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -2,6 +2,7 @@ "include": ["./src/**/*.ts"], "exclude": ["./src/**/*.test.tsx", "./src/**/*.test.ts", "./src/fakes/**/*.ts"], "compilerOptions": { + "jsx": "react", "target": "es6", "rootDir": "./src", "sourceMap": true, @@ -13,6 +14,7 @@ "moduleResolution": "node", "skipLibCheck": true, "allowJs": true, + "allowSyntheticDefaultImports": true, "lib": ["DOM", "DOM.Iterable", "ESNext"], "types": [] } diff --git a/tsconfig.iife.json b/tsconfig.iife.json index 05994307..f18f24f8 100644 --- a/tsconfig.iife.json +++ b/tsconfig.iife.json @@ -13,5 +13,7 @@ "outDir": "dist/iife" }, "include": ["src/**/*"], - "exclude": ["dist", "node_modules", "src/utilities/test", "./src/**/*.test.ts", "./src/fakes/**/*.ts"] + "exclude": ["dist", "node_modules", "src/utilities/test", "./src/**/*.test.ts", "./src/fakes/**/*.ts", + "src/react" + ] }