Skip to content

Commit

Permalink
react tests
Browse files Browse the repository at this point in the history
  • Loading branch information
AndyTWF committed Oct 31, 2024
1 parent 65946e1 commit 11dfb0e
Show file tree
Hide file tree
Showing 28 changed files with 481 additions and 275 deletions.
31 changes: 0 additions & 31 deletions src/react/helper/eventual-room.ts

This file was deleted.

89 changes: 89 additions & 0 deletions src/react/helper/use-eventual-room.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Room } from '@ably/chat';
import { useEffect, useState } from 'react';

import { useLogger } from '../hooks/use-logger.js';
import { useRoomContext } from './use-room-context.js';
import { useStableReference } from './use-stable-reference.js';

/**
* Given a room promise, this hook will return the room object once it has been resolved. This is useful
* in hooks like useRoom to provide a direct reference to the room object, as Promises aren't usually the best
* thing to be passing around React components.
*
* @param roomId The roomId of the room
* @param room The room promise that we're waiting to resolve
* @returns The room object if it has resolved, otherwise undefined
*/
export const useEventualRoom = (): Room | undefined => {
const [roomState, setRoomState] = useState<Room | undefined>();
const context = useRoomContext('useEventualRoom');
const logger = useLogger();
logger.trace('useEventualRoom();', { roomId: context.roomId });

useEffect(() => {
logger.debug('useEventualRoom(); running useEffect', { roomId: context.roomId });
let unmounted = false;
void context.room
.then((room: Room) => {
if (unmounted) {
logger.debug('useEventualRoom(); already unmounted', { roomId: context.roomId });
return;
}

logger.debug('useEventualRoom(); resolved', { roomId: context.roomId });
setRoomState(room);
})
.catch((error: unknown) => {
logger.error('Failed to get room', { roomId: context.roomId, error });
});

return () => {
logger.debug('useEventualRoom(); cleanup', { roomId: context.roomId });
unmounted = true;
};
}, [context, logger]);

return roomState;
};

/**
* Similar to useEventualRoom, but instead of providing the room itself, it provides a property of the room - e.g.
* Messages. We use this to eventually provide access to underlying room interfaces as non-promise values
* in hooks like useMessages.
* @param roomId The roomId of the room
* @param room The room promise that we're waiting to resolve
* @param onResolve A function that will be called when the room promise resolves, and will return the property of the room
* @returns
*/
export const useEventualRoomProperty = <T>(onResolve: (room: Room) => T) => {
const [roomState, setRoomState] = useState<T | undefined>();
const context = useRoomContext('useEventualRoomProperty');
const logger = useLogger();
logger.trace('useEventualRoomProperty();', { roomId: context.roomId });
const onResolveRef = useStableReference(onResolve);

useEffect(() => {
let unmounted = false;
logger.debug('useEventualRoomProperty(); running useEffect', { roomId: context.roomId });
void context.room
.then((room: Room) => {
if (unmounted) {
logger.debug('useEventualRoomProperty(); already unmounted', { roomId: context.roomId });
return;
}

logger.debug('useEventualRoomProperty(); resolved', { roomId: context.roomId });
setRoomState(onResolveRef(room));
})
.catch((error: unknown) => {
logger.error('Failed to get room', { roomId: context.roomId, error });
});

return () => {
logger.debug('useEventualRoomProperty(); cleanup', { roomId: context.roomId });
unmounted = true;
};
}, [context, logger, onResolveRef]);

return roomState;
};
3 changes: 2 additions & 1 deletion src/react/helper/use-room-context.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as Ably from 'ably';
import { useContext } from 'react';

import { ChatRoomContext, ChatRoomContextType } from '../contexts/chat-room-context.js';

export const useRoomContext = (callingHook: string): ChatRoomContextType => {
const context = useContext(ChatRoomContext);
if (!context) {
throw new Error(`\`${callingHook}\`(); must be used within a <ChatRoomProvider>`);
throw new Ably.ErrorInfo(`${callingHook} hook must be used within a <ChatRoomProvider>`, 40000, 400);
}

return context;
Expand Down
7 changes: 5 additions & 2 deletions src/react/helper/use-room-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export const useRoomStatus = (params?: {
context.room,
(room: Room) => {
logger.debug('useRoomStatus(); subscribing internal listener');
// Set instantaneous values
setStatus(room.status.current);
setError(room.status.error);

// Add the subscription
const { off } = room.status.onChange((change) => {
logger.debug('useRoomStatus(); status change', change);
setStatus(change.current);
Expand Down Expand Up @@ -59,8 +64,6 @@ export const useRoomStatus = (params?: {
}

logger.debug('useRoomStatus(); setting initial status', { status: room.status.current });
setStatus(room.status.current);
setError(room.status.error);
if (onRoomStatusChangeRef) {
logger.debug('useRoomStatus(); sending initial status event');
onRoomStatusChangeRef({
Expand Down
25 changes: 25 additions & 0 deletions src/react/helper/use-stable-reference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useCallback, useEffect, useRef } from 'react';

/**
* The type of a callback function that can be stored in the reference.
*/
type Callback<CallbackArguments extends unknown[], ReturnType> = (...args: CallbackArguments) => ReturnType;

/**
* In some cases, we want to use a callback that is always the same, and always persists across renders.
* This function creates a stable reference to a callback, so that it can be used in a `useEffect` or `useCallback`
* without causing unnecessary re-renders.
*
* @param callback The callback to turn into a stable reference
* @returns A stable reference to the callback
*/
export const useStableReference = <Arguments extends unknown[], ReturnType>(
callback: Callback<Arguments, ReturnType>,
): Callback<Arguments, ReturnType> => {
const ref = useRef<Callback<Arguments, ReturnType>>(callback);
useEffect(() => {
ref.current = callback;
});

return useCallback((...args: Arguments) => ref.current(...args), []);
};
7 changes: 7 additions & 0 deletions src/react/hooks/use-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useCallback, useEffect, useState } from 'react';

import { wrapRoomPromise } from '../helper/room-promise.js';
import { useEventListenerRef } from '../helper/use-event-listener-ref.js';
import { useEventualRoomProperty } from '../helper/use-eventual-room.js';
import { useRoomContext } from '../helper/use-room-context.js';
import { useRoomStatus } from '../helper/use-room-status.js';
import { ChatStatusResponse } from '../types/chat-status-response.js';
Expand All @@ -33,6 +34,11 @@ export interface UseMessagesResponse extends ChatStatusResponse {
*/
readonly get: Messages['get'];

/**
* Provides access to the underlying {@link Messages} instance of the room.
*/
readonly messages?: Messages;

/**
* Retrieves the previous messages in the room.
*
Expand Down Expand Up @@ -167,6 +173,7 @@ export const useMessages = (params?: UseMessagesParams): UseMessagesResponse =>
}, [context, logger, onDiscontinuityRef]);

return {
messages: useEventualRoomProperty((room) => room.messages),
send,
get,
getPreviousMessages,
Expand Down
9 changes: 8 additions & 1 deletion src/react/hooks/use-occupancy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { OccupancyListener } from '@ably/chat';
import { Occupancy, OccupancyListener } from '@ably/chat';
import { useEffect, useState } from 'react';

import { wrapRoomPromise } from '../helper/room-promise.js';
import { useEventListenerRef } from '../helper/use-event-listener-ref.js';
import { useEventualRoomProperty } from '../helper/use-eventual-room.js';
import { useRoomContext } from '../helper/use-room-context.js';
import { useRoomStatus } from '../helper/use-room-status.js';
import { ChatStatusResponse } from '../types/chat-status-response.js';
Expand Down Expand Up @@ -35,6 +36,11 @@ export interface UseOccupancyResponse extends ChatStatusResponse {
* The current number of users present in the room, kept up to date by the hook.
*/
readonly presenceMembers: number;

/**
* Provides access to the underlying {@link Occupancy} instance of the room.
*/
readonly occupancy?: Occupancy;
}

/**
Expand Down Expand Up @@ -122,6 +128,7 @@ export const useOccupancy = (params?: UseOccupancyParams): UseOccupancyResponse
}, [listenerRef, context, logger]);

return {
occupancy: useEventualRoomProperty((room) => room.occupancy),
connectionStatus,
connectionError,
roomStatus,
Expand Down
9 changes: 8 additions & 1 deletion src/react/hooks/use-presence-listener.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ErrorCodes, errorInfoIs, PresenceListener, PresenceMember, Room, RoomLifecycle } from '@ably/chat';
import { ErrorCodes, errorInfoIs, Presence, PresenceListener, PresenceMember, Room, RoomLifecycle } from '@ably/chat';
import * as Ably from 'ably';
import { useCallback, useEffect, useRef, useState } from 'react';

import { wrapRoomPromise } from '../helper/room-promise.js';
import { useEventListenerRef } from '../helper/use-event-listener-ref.js';
import { useEventualRoomProperty } from '../helper/use-eventual-room.js';
import { useRoomContext } from '../helper/use-room-context.js';
import { useRoomStatus } from '../helper/use-room-status.js';
import { ChatStatusResponse } from '../types/chat-status-response.js';
Expand Down Expand Up @@ -44,6 +45,11 @@ export interface UsePresenceListenerResponse extends ChatStatusResponse {
*/
readonly presenceData: PresenceMember[];

/**
* Provides access to the underlying {@link Presence} instance of the room.
*/
readonly presence?: Presence;

/**
* The error state of the presence listener.
* The hook keeps {@link presenceData} up to date asynchronously, so this error state is provided to allow
Expand Down Expand Up @@ -300,6 +306,7 @@ export const usePresenceListener = (params?: UsePresenceListenerParams): UsePres
}, [context, onDiscontinuityRef, logger]);

return {
presence: useEventualRoomProperty((room) => room.presence),
connectionStatus,
connectionError,
roomStatus,
Expand Down
26 changes: 18 additions & 8 deletions src/react/hooks/use-presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';

import { wrapRoomPromise } from '../helper/room-promise.js';
import { useEventListenerRef } from '../helper/use-event-listener-ref.js';
import { useEventualRoomProperty } from '../helper/use-eventual-room.js';
import { useRoomContext } from '../helper/use-room-context.js';
import { useRoomStatus } from '../helper/use-room-status.js';
import { ChatStatusResponse } from '../types/chat-status-response.js';
Expand Down Expand Up @@ -32,6 +33,11 @@ export interface UsePresenceResponse extends ChatStatusResponse {
*/
readonly update: Presence['update'];

/**
* Provides access to the underlying {@link Presence} instance of the room.
*/
readonly presence?: Presence;

/**
* Indicates whether the current user is present in the room.
*/
Expand Down Expand Up @@ -92,18 +98,21 @@ export const usePresence = (params?: UsePresenceParams): UsePresenceResponse =>

// enter the room when the hook is mounted
useEffect(() => {
const canJoinPresence = roomStatus === RoomLifecycle.Attached && !INACTIVE_CONNECTION_STATES.has(connectionStatus);

// wait until the room is attached before attempting to enter, and ensure the connection is active
if (!canJoinPresence) {
logger.debug('usePresence(); skipping enter room', { roomStatus, connectionStatus, roomId: context.roomId });
return;
}

logger.debug('usePresence(); entering room', { roomId: context.roomId });
return wrapRoomPromise(
context.room,
(room: Room) => {
const canJoinPresence =
roomStatus === RoomLifecycle.Attached && !INACTIVE_CONNECTION_STATES.has(connectionStatus);

// wait until the room is attached before attempting to enter, and ensure the connection is active
if (!canJoinPresence) {
logger.debug('usePresence(); skipping enter room', { roomStatus, connectionStatus, roomId: context.roomId });
return () => {
// no-op
};
}

room.presence
.enter(dataRef.current?.enterWithData)
.then(() => {
Expand Down Expand Up @@ -178,6 +187,7 @@ export const usePresence = (params?: UsePresenceParams): UsePresenceResponse =>
);

return {
presence: useEventualRoomProperty((room) => room.presence),
connectionStatus,
connectionError,
roomStatus,
Expand Down
7 changes: 7 additions & 0 deletions src/react/hooks/use-room-reactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useCallback, useEffect } from 'react';

import { wrapRoomPromise } from '../helper/room-promise.js';
import { useEventListenerRef } from '../helper/use-event-listener-ref.js';
import { useEventualRoomProperty } from '../helper/use-eventual-room.js';
import { useRoomContext } from '../helper/use-room-context.js';
import { useRoomStatus } from '../helper/use-room-status.js';
import { ChatStatusResponse } from '../types/chat-status-response.js';
Expand All @@ -29,6 +30,11 @@ export interface UseRoomReactionsResponse extends ChatStatusResponse {
* A shortcut to the {@link RoomReactions.send} method.
*/
readonly send: RoomReactions['send'];

/**
* Provides access to the underlying {@link RoomReactions} instance of the room.
*/
readonly reactions?: RoomReactions;
}

/**
Expand Down Expand Up @@ -94,6 +100,7 @@ export const useRoomReactions = (params?: UseRoomReactionsParams): UseRoomReacti
);

return {
reactions: useEventualRoomProperty((room) => room.reactions),
connectionStatus,
connectionError,
roomStatus,
Expand Down
Loading

0 comments on commit 11dfb0e

Please sign in to comment.