Skip to content

Commit

Permalink
feat: add conversation caching and some tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ttypic committed Dec 15, 2023
1 parent cd0ea46 commit 356b8f7
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 2 deletions.
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 {
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

0 comments on commit 356b8f7

Please sign in to comment.