Skip to content

Commit

Permalink
[CJS3] client single instance (#599)
Browse files Browse the repository at this point in the history
* feat: getInstance

* test: import types from src

* types: getInstace generics

* test: client getInstance mutilple connect
  • Loading branch information
Amin Mahboubi authored Jan 28, 2021
1 parent 00b7596 commit c9a1668
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 0 deletions.
123 changes: 123 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export class StreamChat<
ReactionType extends UnknownType = UnknownType,
UserType extends UnknownType = UnknownType
> {
private static _instance?: unknown | StreamChat; // type is undefined|StreamChat, unknown is due to TS limitations with statics

_user?: OwnUserResponse<ChannelType, CommandType, UserType> | UserResponse<UserType>;
activeChannels: {
[key: string]: Channel<
Expand Down Expand Up @@ -171,6 +173,8 @@ export class StreamChat<

/**
* Initialize a client
*
* **Only use constructor for advanced usages. It is strongly advised to use `StreamChat.getInstance()` instead of `new StreamChat()` to reduce integration issues due to multiple WebSocket connections**
* @param {string} key - the api key
* @param {string} [secret] - the api secret
* @param {StreamChatOptions} [options] - additional options, here you can pass custom options to axios instance
Expand Down Expand Up @@ -312,6 +316,125 @@ export class StreamChat<
this.recoverStateOnReconnect = this.options.recoverStateOnReconnect;
}

/**
* Get a client instance
*
* This function always returns the same Client instance to avoid issues raised by multiple Client and WS connections
*
* **After the first call, the client configration will not change if the key or options parameters change**
*
* @param {string} key - the api key
* @param {string} [secret] - the api secret
* @param {StreamChatOptions} [options] - additional options, here you can pass custom options to axios instance
* @param {boolean} [options.browser] - enforce the client to be in browser mode
* @param {boolean} [options.warmUp] - default to false, if true, client will open a connection as soon as possible to speed up following requests
* @param {Logger} [options.Logger] - custom logger
* @param {number} [options.timeout] - default to 3000
* @param {httpsAgent} [options.httpsAgent] - custom httpsAgent, in node it's default to https.agent()
* @example <caption>initialize the client in user mode</caption>
* StreamChat.getInstance('api_key')
* @example <caption>initialize the client in user mode with options</caption>
* StreamChat.getInstance('api_key', { timeout:5000 })
* @example <caption>secret is optional and only used in server side mode</caption>
* StreamChat.getInstance('api_key', "secret", { httpsAgent: customAgent })
*/
public static getInstance<
AttachmentType extends UnknownType = UnknownType,
ChannelType extends UnknownType = UnknownType,
CommandType extends string = LiteralStringForUnion,
EventType extends UnknownType = UnknownType,
MessageType extends UnknownType = UnknownType,
ReactionType extends UnknownType = UnknownType,
UserType extends UnknownType = UnknownType
>(
key: string,
options?: StreamChatOptions,
): StreamChat<
AttachmentType,
ChannelType,
CommandType,
EventType,
MessageType,
ReactionType,
UserType
>;
public static getInstance<
AttachmentType extends UnknownType = UnknownType,
ChannelType extends UnknownType = UnknownType,
CommandType extends string = LiteralStringForUnion,
EventType extends UnknownType = UnknownType,
MessageType extends UnknownType = UnknownType,
ReactionType extends UnknownType = UnknownType,
UserType extends UnknownType = UnknownType
>(
key: string,
secret?: string,
options?: StreamChatOptions,
): StreamChat<
AttachmentType,
ChannelType,
CommandType,
EventType,
MessageType,
ReactionType,
UserType
>;
public static getInstance<
AttachmentType extends UnknownType = UnknownType,
ChannelType extends UnknownType = UnknownType,
CommandType extends string = LiteralStringForUnion,
EventType extends UnknownType = UnknownType,
MessageType extends UnknownType = UnknownType,
ReactionType extends UnknownType = UnknownType,
UserType extends UnknownType = UnknownType
>(
key: string,
secretOrOptions?: StreamChatOptions | string,
options?: StreamChatOptions,
): StreamChat<
AttachmentType,
ChannelType,
CommandType,
EventType,
MessageType,
ReactionType,
UserType
> {
if (!StreamChat._instance) {
if (typeof secretOrOptions === 'string') {
StreamChat._instance = new StreamChat<
AttachmentType,
ChannelType,
CommandType,
EventType,
MessageType,
ReactionType,
UserType
>(key, secretOrOptions, options);
} else {
StreamChat._instance = new StreamChat<
AttachmentType,
ChannelType,
CommandType,
EventType,
MessageType,
ReactionType,
UserType
>(key, secretOrOptions);
}
}

return StreamChat._instance as StreamChat<
AttachmentType,
ChannelType,
CommandType,
EventType,
MessageType,
ReactionType,
UserType
>;
}

devToken(userID: string) {
return DevToken(userID);
}
Expand Down
29 changes: 29 additions & 0 deletions test/typescript/unit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,31 @@ const clientWithoutSecret: StreamChat<
logger: (logLevel: string, msg: string, extraData?: Record<string, unknown>) => {},
});

const singletonClient = StreamChat.getInstance<
AttachmentType,
ChannelType,
CommandType,
EventType,
MessageType,
ReactionType,
UserType
>(apiKey);

const singletonClient1: StreamChat<
{},
ChannelType,
string & {},
{},
{},
{},
UserType
> = StreamChat.getInstance<{}, ChannelType, string & {}, {}, {}, {}, UserType>(apiKey);

const singletonClient2: StreamChat<{}, ChannelType> = StreamChat.getInstance<
{},
ChannelType
>(apiKey, '', {});

const devToken: string = client.devToken('joshua');
const token: string = client.createToken('james', 3600);
const authType: string = client.getAuthType();
Expand All @@ -104,6 +129,10 @@ const updateUsers: Promise<{
users: { [key: string]: UserResponse<UserType> };
}> = client.partialUpdateUsers([updateRequest]);

const updateUsersWithSingletonClient: Promise<{
users: { [key: string]: UserResponse<UserType> };
}> = singletonClient.partialUpdateUsers([updateRequest]);

const eventHandler = (event: Event) => {};
voidReturn = client.on(eventHandler);
voidReturn = client.off(eventHandler);
Expand Down
41 changes: 41 additions & 0 deletions test/unit/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,47 @@ import { StreamChat } from '../../src/client';

const expect = chai.expect;

describe('StreamChat getInstance', () => {
beforeEach(() => {
delete StreamChat._instance;
});

it('instance is stored as static property', () => {
expect(StreamChat._instance).to.be.undefined;

const client = StreamChat.getInstance('key');
expect(client).to.equal(StreamChat._instance);
});

it('always return the same instance', () => {
const client1 = StreamChat.getInstance('key1');
const client2 = StreamChat.getInstance('key1');
const client3 = StreamChat.getInstance('key1');
expect(client1).to.equal(client2);
expect(client2).to.equal(client3);
});

it('changin params has no effect', () => {
const client1 = StreamChat.getInstance('key2');
const client2 = StreamChat.getInstance('key3');

expect(client1).to.equal(client2);
expect(client2.key).to.eql('key2');
});

it('should throw error if connectUser called twice on an instance', async () => {
const client1 = StreamChat.getInstance('key2', { allowServerSideConnect: true });
client1._setupConnection = () => Promise.resolve();
client1._setToken = () => Promise.resolve();

await client1.connectUser({ id: 'vishal' }, 'token');
const client2 = StreamChat.getInstance('key2');
expect(() => client2.connectUser({ id: 'Amin' }, 'token')).to.throw(
/connectUser was called twice/,
);
});
});

describe('Client userMuteStatus', function () {
const client = new StreamChat('', '');
const user = { id: 'user' };
Expand Down

0 comments on commit c9a1668

Please sign in to comment.