Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: async room get #387

Merged
merged 15 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,19 +143,25 @@ chat.connection.offAllStatusChange();
You can create or retrieve a chat room with name `"basketball-stream"` this way:

```ts
const room = chat.rooms.get('basketball-stream', { reactions: RoomOptionsDefaults.reactions });
const room = await chat.rooms.get('basketball-stream', { reactions: RoomOptionsDefaults.reactions });
```

The second argument to `rooms.get` is a `RoomOptions` argument, which tells the Chat SDK what features you would like your room to use and how they should be configured. For example, you can set the timeout between keystrokes for typing events as part of the room options. Sensible defaults for each
of the features are provided for your convenience:
The second argument to `rooms.get` is a `RoomOptions` argument, which tells the Chat SDK what features you would like your room to use and how they should be configured.

- A typing timeout (time of inactivity before typing stops) of 10 seconds.
For example, you can set the timeout between keystrokes for typing events as part of the room options. Sensible defaults for each of the features are provided for your convenience:

- A typing timeout (time of inactivity before typing stops) of 5 seconds.
- Entry into, and subscription to, presence.

The defaults options for each feature may be viewed [here](https://github.com/ably/ably-chat-js/blob/main/src/RoomOptions.ts).

In order to use the same room but with different options, you must first `release` the room before requesting an instance with the changed options (see below for more information on releasing rooms).

Note that:

- If a `release` call is currently in progress for the room (see below), then a call to `get` will wait for that to resolve before resolving itself.
- If a `get` call is currently in progress for the room and `release` is called, the `get` call will reject.

### Attaching to a room

To start receiving events on a room, it must first be attached. This can be done using the `attach` method.
Expand Down
61 changes: 45 additions & 16 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC } from 'react';
import { FC, useEffect, useState } from 'react';
import { Chat } from './containers/Chat';
import { OccupancyComponent } from './components/OccupancyComponent';
import { UserPresenceComponent } from './components/UserPresenceComponent';
Expand All @@ -23,20 +23,49 @@ let roomId: string;

interface AppProps {}

const App: FC<AppProps> = () => (
<ChatRoomProvider
id={roomId}
release={true}
attach={true}
options={RoomOptionsDefaults}
>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '800px', margin: 'auto' }}>
<Chat />
<div style={{ display: 'flex', flexDirection: 'column' }}>
<UserPresenceComponent />
<OccupancyComponent />
const App: FC<AppProps> = () => {
const [roomIdState, setRoomId] = useState(roomId);
const updateRoomId = (newRoomId: string) => {
const params = new URLSearchParams(window.location.search);
params.set('room', newRoomId);
history.pushState(null, '', '?' + params.toString());
setRoomId(newRoomId);
};

// Add a useEffect that handles the popstate event to update the roomId when
// the user navigates back and forth in the browser history.
useEffect(() => {
const handlePopState = () => {
const params = new URLSearchParams(window.location.search);
const newRoomId = params.get('room') || 'abcd';
setRoomId(newRoomId);
};

window.addEventListener('popstate', handlePopState);

return () => {
window.removeEventListener('popstate', handlePopState);
};
}, []);

return (
<ChatRoomProvider
id={roomIdState}
release={true}
attach={true}
options={RoomOptionsDefaults}
>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '800px', margin: 'auto' }}>
<Chat
setRoomId={updateRoomId}
roomId={roomIdState}
/>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<UserPresenceComponent />
<OccupancyComponent />
</div>
</div>
</div>
</ChatRoomProvider>
);
</ChatRoomProvider>
);
};
export default App;
67 changes: 50 additions & 17 deletions demo/src/containers/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ReactionInput } from '../../components/ReactionInput';
import { ConnectionStatusComponent } from '../../components/ConnectionStatusComponent/ConnectionStatusComponent.tsx';
import { ConnectionStatus, Message, MessageEventPayload, MessageEvents, PaginatedResult, Reaction } from '@ably/chat';

export const Chat = () => {
export const Chat = (props: { roomId: string; setRoomId: (roomId: string) => void }) => {
const chatClient = useChatClient();
const clientId = chatClient.clientId;
const [messages, setMessages] = useState<Message[]>([]);
Expand All @@ -15,6 +15,21 @@ export const Chat = () => {

const isConnected: boolean = currentStatus === ConnectionStatus.Connected;

const backfillPreviousMessages = (getPreviousMessages: ReturnType<typeof useMessages>['getPreviousMessages']) => {
chatClient.logger.debug('backfilling previous messages');
if (getPreviousMessages) {
getPreviousMessages({ limit: 50 })
.then((result: PaginatedResult<Message>) => {
chatClient.logger.debug('backfilled messages', result);
setMessages(result.items.filter((m) => !m.isDeleted).reverse());
setLoading(false);
})
.catch((error: unknown) => {
chatClient.logger.error('Error fetching initial messages', error);
});
}
};

const {
send: sendMessage,
getPreviousMessages,
Expand Down Expand Up @@ -42,8 +57,11 @@ export const Chat = () => {
// this will trigger a re-fetch of the messages
setMessages([]);

// triggers the useEffect to fetch the initial messages again.
// set our state to loading, because we'll need to fetch previous messages again
setLoading(true);

// Do a message backfill
backfillPreviousMessages(getPreviousMessages);
},
});

Expand All @@ -60,21 +78,9 @@ export const Chat = () => {
const messagesEndRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
// try and fetch the messages up to attachment of the messages listener
if (getPreviousMessages && loading) {
getPreviousMessages({ limit: 50 })
.then((result: PaginatedResult<Message>) => {
// reverse the messages so they are displayed in the correct order
// and don't include deleted messages
setMessages(result.items.filter((m) => !m.isDeleted).reverse());
setLoading(false);
})
.catch((error: unknown) => {
console.error('Error fetching initial messages', error);
setLoading(false);
});
}
}, [getPreviousMessages, loading]);
chatClient.logger.debug('updating getPreviousMessages useEffect', { getPreviousMessages });
backfillPreviousMessages(getPreviousMessages);
}, [getPreviousMessages]);

const handleStartTyping = () => {
start().catch((error: unknown) => {
Expand Down Expand Up @@ -124,6 +130,19 @@ export const Chat = () => {
window.location.reload();
}

function changeRoomId() {
const newRoomId = prompt('Enter your new roomId');
if (!newRoomId) {
return;
}

// Clear the room messages
setMessages([]);
setLoading(true);
setRoomReactions([]);
props.setRoomId(newRoomId);
}

const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
Expand Down Expand Up @@ -151,6 +170,20 @@ export const Chat = () => {
</a>
.
</div>
<div
className="text-xs p-3"
style={{ backgroundColor: '#333' }}
>
You are in room <strong>{props.roomId}</strong>.{' '}
<a
href="#"
className="text-blue-600 dark:text-blue-500 hover:underline"
onClick={changeRoomId}
>
Change roomId
</a>
.
</div>
{loading && <div className="text-center m-auto">loading...</div>}
{!loading && (
<div
Expand Down
2 changes: 1 addition & 1 deletion demo/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const realtimeClient = new Ably.Realtime({
clientId,
});

const chatClient = new ChatClient(realtimeClient, { logLevel: LogLevel.Debug });
const chatClient = new ChatClient(realtimeClient, { logLevel: LogLevel.Info });

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"build": "npm run build:chat && npm run build:react",
"build:chat": "vite build --config ./src/core/vite.config.ts --emptyOutDir",
"build:react": "vite build --config ./src/react/vite.config.ts --emptyOutDir",
"build:start-demo": "npm run build && (cd demo && npm start)",
"prepare": "npm run build",
"test:typescript": "tsc",
"demo:reload": "npm run build && cd demo && npm i file:../",
Expand Down
5 changes: 2 additions & 3 deletions src/core/discontinuity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import EventEmitter from './utils/event-emitter.js';
*/
export interface HandlesDiscontinuity {
/**
* A promise of the channel that this object is associated with. The promise
* is resolved when the feature has finished initializing.
* A channel that this object is associated with.
*/
get channel(): Promise<Ably.RealtimeChannel>;
get channel(): Ably.RealtimeChannel;

/**
* Called when a discontinuity is detected on the channel.
Expand Down
5 changes: 5 additions & 0 deletions src/core/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ export enum ErrorCodes {
* An unknown error has happened in the room lifecycle.
*/
RoomLifecycleError = 102105,

/**
* Room was released before the operation could complete.
*/
RoomReleasedBeforeOperationCompleted = 102106,
}

/**
Expand Down
7 changes: 7 additions & 0 deletions src/core/id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Generates a random string that can be used as an identifier, for instance in identifying specific room
* objects.
*
* @returns A random string that can be used as an identifier.
*/
export const randomId = (): string => Math.random().toString(36).slice(2);
44 changes: 14 additions & 30 deletions src/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,9 @@ export interface Messages extends EmitsDiscontinuities {
/**
* Get the underlying Ably realtime channel used for the messages in this chat room.
*
* @returns A promise of the realtime channel.
* @returns The realtime channel.
*/
get channel(): Promise<Ably.RealtimeChannel>;
get channel(): Ably.RealtimeChannel;
}

/**
Expand All @@ -253,7 +253,7 @@ export class DefaultMessages
implements Messages, HandlesDiscontinuity, ContributesToRoomLifecycle
{
private readonly _roomId: string;
private readonly _channel: Promise<Ably.RealtimeChannel>;
private readonly _channel: Ably.RealtimeChannel;
private readonly _chatApi: ChatApi;
private readonly _clientId: string;
private readonly _listenerSubscriptionPoints: Map<
Expand All @@ -272,25 +272,12 @@ export class DefaultMessages
* @param chatApi An instance of the ChatApi.
* @param clientId The client ID of the user.
* @param logger An instance of the Logger.
* @param initAfter A promise that is awaited before creating any channels.
*/
constructor(
roomId: string,
realtime: Ably.Realtime,
chatApi: ChatApi,
clientId: string,
logger: Logger,
initAfter: Promise<void>,
) {
*/
constructor(roomId: string, realtime: Ably.Realtime, chatApi: ChatApi, clientId: string, logger: Logger) {
super();
this._roomId = roomId;

this._channel = initAfter.then(() => this._makeChannel(roomId, realtime));

// Catch this so it won't send unhandledrejection global event
this._channel.catch((error: unknown) => {
logger.debug('Messages: channel initialization canceled', { roomId, error });
});
this._channel = this._makeChannel(roomId, realtime);
AndyTWF marked this conversation as resolved.
Show resolved Hide resolved

this._chatApi = chatApi;
this._clientId = clientId;
Expand All @@ -299,7 +286,7 @@ export class DefaultMessages
}

/**
* Creates the realtime channel for messages. Called after initAfter is resolved.
* Creates the realtime channel for messages.
*/
private _makeChannel(roomId: string, realtime: Ably.Realtime): Ably.RealtimeChannel {
const channel = getChannel(messagesChannelName(roomId), realtime);
Expand Down Expand Up @@ -398,7 +385,7 @@ export class DefaultMessages
private async _resolveSubscriptionStart(): Promise<{
fromSerial: string;
}> {
const channelWithProperties = await this._getChannelProperties();
const channelWithProperties = this._getChannelProperties();

// If we are attached, we can resolve with the channelSerial
if (channelWithProperties.state === 'attached') {
Expand All @@ -412,14 +399,11 @@ export class DefaultMessages
return this._subscribeAtChannelAttach();
}

private async _getChannelProperties(): Promise<
Ably.RealtimeChannel & {
properties: { attachSerial: string | undefined; channelSerial: string | undefined };
}
> {
private _getChannelProperties(): Ably.RealtimeChannel & {
properties: { attachSerial: string | undefined; channelSerial: string | undefined };
} {
// Get the attachSerial from the channel properties
const channel = await this._channel;
return channel as Ably.RealtimeChannel & {
return this._channel as Ably.RealtimeChannel & {
properties: {
attachSerial: string | undefined;
channelSerial: string | undefined;
Expand All @@ -428,7 +412,7 @@ export class DefaultMessages
}

private async _subscribeAtChannelAttach(): Promise<{ fromSerial: string }> {
const channelWithProperties = await this._getChannelProperties();
const channelWithProperties = this._getChannelProperties();
return new Promise((resolve, reject) => {
// Check if the state is now attached
if (channelWithProperties.state === 'attached') {
Expand Down Expand Up @@ -468,7 +452,7 @@ export class DefaultMessages
/**
* @inheritdoc Messages
*/
get channel(): Promise<Ably.RealtimeChannel> {
get channel(): Ably.RealtimeChannel {
return this._channel;
}

Expand Down
Loading
Loading