From c9a16689bffac70745fbf5897f9742768870c0c5 Mon Sep 17 00:00:00 2001 From: Amin Mahboubi Date: Thu, 28 Jan 2021 09:14:33 +0100 Subject: [PATCH] [CJS3] client single instance (#599) * feat: getInstance * test: import types from src * types: getInstace generics * test: client getInstance mutilple connect --- src/client.ts | 123 +++++++++++++++++++++++++++++++++++ test/typescript/unit-test.ts | 29 +++++++++ test/unit/client.js | 41 ++++++++++++ 3 files changed, 193 insertions(+) diff --git a/src/client.ts b/src/client.ts index dfe045f8f..4d43d97c0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -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 | UserResponse; activeChannels: { [key: string]: Channel< @@ -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 @@ -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 initialize the client in user mode + * StreamChat.getInstance('api_key') + * @example initialize the client in user mode with options + * StreamChat.getInstance('api_key', { timeout:5000 }) + * @example secret is optional and only used in server side mode + * 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); } diff --git a/test/typescript/unit-test.ts b/test/typescript/unit-test.ts index 283c37859..c155423a1 100644 --- a/test/typescript/unit-test.ts +++ b/test/typescript/unit-test.ts @@ -80,6 +80,31 @@ const clientWithoutSecret: StreamChat< logger: (logLevel: string, msg: string, extraData?: Record) => {}, }); +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(); @@ -104,6 +129,10 @@ const updateUsers: Promise<{ users: { [key: string]: UserResponse }; }> = client.partialUpdateUsers([updateRequest]); +const updateUsersWithSingletonClient: Promise<{ + users: { [key: string]: UserResponse }; +}> = singletonClient.partialUpdateUsers([updateRequest]); + const eventHandler = (event: Event) => {}; voidReturn = client.on(eventHandler); voidReturn = client.off(eventHandler); diff --git a/test/unit/client.js b/test/unit/client.js index 2f693c25f..fc94f555d 100644 --- a/test/unit/client.js +++ b/test/unit/client.js @@ -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' };