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

[CON-96] feat: add conversation caching and some tests #22

Merged
merged 11 commits into from
Jan 2, 2024
78 changes: 78 additions & 0 deletions __mocks__/ably/promises/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Types } from 'ably/promises';

const MOCK_CLIENT_ID = 'MOCK_CLIENT_ID';

const mockPromisify = <T>(expectedReturnValue): Promise<T> => new Promise((resolve) => resolve(expectedReturnValue));
const methodReturningVoidPromise = () => mockPromisify<void>((() => {})());

function createMockPresence() {
return {
get: () => mockPromisify<Types.PresenceMessage[]>([]),
update: () => mockPromisify<void>(undefined),
enter: methodReturningVoidPromise,
leave: methodReturningVoidPromise,
subscriptions: {
once: (_: unknown, fn: Function) => {
fn();
},
},
subscribe: () => {},
unsubscribe: () => {},
};
}

function createMockEmitter() {
return {
any: [],
events: {},
anyOnce: [],
eventsOnce: {},
};
}

function createMockChannel() {
return {
presence: createMockPresence(),
subscribe: () => {},
unsubscribe: () => {},
on: () => {},
off: () => {},
publish: () => {},
subscriptions: createMockEmitter(),
};
}

class MockRealtime {
public channels: {
get: () => ReturnType<typeof createMockChannel>;
};
public auth: {
clientId: string;
};
public connection: {
id?: string;
state: string;
};

public time() {}

constructor() {
this.channels = {
get: (() => {
const mockChannel = createMockChannel();
return () => mockChannel;
})(),
};
this.auth = {
clientId: MOCK_CLIENT_ID,
};
this.connection = {
id: '1',
state: 'connected',
};

this['options'] = {};
}
}

export { MockRealtime as Realtime };
11 changes: 9 additions & 2 deletions src/Conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@ export class Conversations {
private readonly realtime: Realtime;
private readonly chatApi: ChatApi;

private conversations: Record<string, Conversation> = {};

constructor(realtime: Realtime) {
this.realtime = realtime;
this.chatApi = new ChatApi((realtime as any).options.clientId);
}

get(conversationId: string) {
return new Conversation(conversationId, this.realtime, this.chatApi);
get(conversationId: string): Conversation {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also have a release method to remove the conversation

if (this.conversations[conversationId]) return this.conversations[conversationId];

const conversation = new Conversation(conversationId, this.realtime, this.chatApi);
this.conversations[conversationId] = conversation;

return conversation;
}
}
131 changes: 131 additions & 0 deletions src/Messages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { beforeEach, describe, vi, it, expect } from 'vitest';
import { Realtime, Types } from 'ably/promises';
import { ChatApi } from './ChatApi.js';
import { Conversation } from './Conversation.js';

interface TestContext {
realtime: Realtime;
chatApi: ChatApi;
emulateBackendPublish: Types.messageCallback<Partial<Types.Message>>;
}

vi.mock('ably/promises');

describe('Messages', () => {
beforeEach<TestContext>((context) => {
context.realtime = new Realtime({ clientId: 'clientId', key: 'key' });
context.chatApi = new ChatApi('clientId');

const channel = context.realtime.channels.get('conversationId');
vi.spyOn(channel, 'subscribe').mockImplementation(
// @ts-ignore
async (name: string, listener: Types.messageCallback<Types.Message>) => {
context.emulateBackendPublish = listener;
},
);
});

describe('sending message', () => {
it<TestContext>('should return message if chat backend request come before realtime', async (context) => {
const { chatApi, realtime } = context;
vi.spyOn(chatApi, 'sendMessage').mockResolvedValue({ id: 'messageId' });

const conversation = new Conversation('conversationId', realtime, chatApi);
const messagePromise = conversation.messages.send('text');

context.emulateBackendPublish({
clientId: 'clientId',
data: {
id: 'messageId',
content: 'text',
client_id: 'clientId',
},
});

const message = await messagePromise;

expect(message).toContain({
id: 'messageId',
content: 'text',
client_id: 'clientId',
});
});

it<TestContext>('should return message if chat backend request come after realtime', async (context) => {
const { chatApi, realtime } = context;

vi.spyOn(chatApi, 'sendMessage').mockImplementation(async (conversationId, text) => {
context.emulateBackendPublish({
clientId: 'clientId',
data: {
id: 'messageId',
content: text,
client_id: 'clientId',
},
});
return { id: 'messageId' };
});

const conversation = new Conversation('conversationId', realtime, chatApi);
const message = await conversation.messages.send('text');

expect(message).toContain({
id: 'messageId',
content: 'text',
client_id: 'clientId',
});
});
});

describe('editing message', () => {
it<TestContext>('should return message if chat backend request come before realtime', async (context) => {
const { chatApi, realtime } = context;
vi.spyOn(chatApi, 'editMessage').mockResolvedValue({ id: 'messageId' });

const conversation = new Conversation('conversationId', realtime, chatApi);
const messagePromise = conversation.messages.edit('messageId', 'new_text');

context.emulateBackendPublish({
clientId: 'clientId',
data: {
id: 'messageId',
content: 'new_text',
client_id: 'clientId',
},
});

const message = await messagePromise;

expect(message).toContain({
id: 'messageId',
content: 'new_text',
client_id: 'clientId',
});
});

it<TestContext>('should return message if chat backend request come after realtime', async (context) => {
const { chatApi, realtime } = context;

vi.spyOn(chatApi, 'editMessage').mockImplementation(async (conversationId, messageId, text) => {
context.emulateBackendPublish({
clientId: 'clientId',
data: {
id: messageId,
content: text,
client_id: 'clientId',
},
});
return { id: 'messageId' };
});

const conversation = new Conversation('conversationId', realtime, chatApi);
const message = await conversation.messages.edit('messageId', 'new_text');

expect(message).toContain({
id: 'messageId',
content: 'new_text',
client_id: 'clientId',
});
});
});
});
1 change: 1 addition & 0 deletions src/Messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export class Messages {
private readonly conversationId: string;
private readonly channel: RealtimeChannelPromise;
private readonly chatApi: ChatApi;

private messageToChannelListener = new WeakMap<MessageListener, ChannelListener>();

constructor(conversationId: string, channel: RealtimeChannelPromise, chatApi: ChatApi) {
Expand Down
Loading