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 8e7f279
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 2 deletions.
90 changes: 90 additions & 0 deletions __mocks__/ably/promises/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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 createMockHistory() {

Check failure on line 24 in __mocks__/ably/promises/index.ts

View workflow job for this annotation

GitHub Actions / lint

'createMockHistory' is defined but never used

Check failure on line 24 in __mocks__/ably/promises/index.ts

View workflow job for this annotation

GitHub Actions / lint

'createMockHistory' is defined but never used
const mockHistory = {
items: [],
first: () => mockPromisify(mockHistory),
next: () => mockPromisify(mockHistory),
current: () => mockPromisify(mockHistory),
hasNext: () => false,
isLast: () => true,
};
return mockHistory;
}

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;
}
}
142 changes: 142 additions & 0 deletions src/Messages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { beforeEach, describe, vi, it, expect } from 'vitest';
import { Realtime, Types } from 'ably/promises';
import { ChatApi } from './ChatApi';

Check failure on line 3 in src/Messages.test.ts

View workflow job for this annotation

GitHub Actions / lint

Missing file extension for "./ChatApi"
import { Conversation } from './Conversation';

Check failure on line 4 in src/Messages.test.ts

View workflow job for this annotation

GitHub Actions / lint

Missing file extension for "./Conversation"

interface TestContext {
realtime: Realtime;
chatApi: ChatApi;
}

vi.mock('ably/promises');

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

describe('sending message', () => {
it<TestContext>('should return message if chat backend request come before realtime', async ({
realtime,
chatApi,
}) => {
let callback: Types.messageCallback<Types.Message>;
const channel = realtime.channels.get('conversationId');
vi.spyOn(channel, 'subscribe').mockImplementation(
// @ts-ignore
async (name: string, listener: Types.messageCallback<Types.Message>) => {
callback = listener;
},
);
vi.spyOn(chatApi, 'sendMessage').mockResolvedValue({ id: 'messageId' });
const conversation = new Conversation('conversationId', realtime, chatApi);
const messagePromise = conversation.messages.send('text');

callback({
clientId: 'clientId',
data: {
id: 'messageId',
content: 'text',
client_id: 'clientId',
},
} as Types.Message);

const message = await messagePromise;
expect(message.content).toBe('text');
expect(message.client_id).toBe('clientId');
});

it<TestContext>('should return message if chat backend request come before realtime', async ({
realtime,
chatApi,
}) => {
let callback: Types.messageCallback<Types.Message>;
const channel = realtime.channels.get('conversationId');
vi.spyOn(channel, 'subscribe').mockImplementation(
// @ts-ignore
async (name: string, listener: Types.messageCallback<Types.Message>) => {
callback = listener;
},
);
vi.spyOn(chatApi, 'sendMessage').mockImplementation(async (conversationId, text) => {
callback({
clientId: 'clientId',
data: {
id: 'messageId',
content: text,
client_id: 'clientId',
},
} as Types.Message);
return { id: 'messageId' };
});

const conversation = new Conversation('conversationId', realtime, chatApi);
const message = await conversation.messages.send('text');
expect(message.content).toBe('text');
expect(message.client_id).toBe('clientId');
});
});

describe('editing message', () => {
it<TestContext>('should return message if chat backend request come before realtime', async ({
realtime,
chatApi,
}) => {
let callback: Types.messageCallback<Types.Message>;
const channel = realtime.channels.get('conversationId');
vi.spyOn(channel, 'subscribe').mockImplementation(
// @ts-ignore
async (name: string, listener: Types.messageCallback<Types.Message>) => {
callback = listener;
},
);
vi.spyOn(chatApi, 'editMessage').mockResolvedValue({ id: 'messageId' });
const conversation = new Conversation('conversationId', realtime, chatApi);
const messagePromise = conversation.messages.edit('messageId', 'new_text');

callback({
clientId: 'clientId',
data: {
id: 'messageId',
content: 'new_text',
client_id: 'clientId',
},
} as Types.Message);

const message = await messagePromise;
expect(message.content).toBe('new_text');
expect(message.client_id).toBe('clientId');
});

it<TestContext>('should return message if chat backend request come before realtime', async ({
realtime,
chatApi,
}) => {
let callback: Types.messageCallback<Types.Message>;
const channel = realtime.channels.get('conversationId');
vi.spyOn(channel, 'subscribe').mockImplementation(
// @ts-ignore
async (name: string, listener: Types.messageCallback<Types.Message>) => {
callback = listener;
},
);
vi.spyOn(chatApi, 'editMessage').mockImplementation(async (conversationId, messageId, text) => {
callback({
clientId: 'clientId',
data: {
id: messageId,
content: text,
client_id: 'clientId',
},
} as Types.Message);
return { id: 'messageId' };
});

const conversation = new Conversation('conversationId', realtime, chatApi);
const message = await conversation.messages.edit('messageId', 'new_text');
expect(message.content).toBe('new_text');
expect(message.client_id).toBe('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 8e7f279

Please sign in to comment.