From 22a54e0c51a36a793cc9f0d4abd9f92d7af33f84 Mon Sep 17 00:00:00 2001 From: Amin Mahboubi Date: Wed, 27 Jan 2021 13:38:07 +0100 Subject: [PATCH 1/4] feat: getInstance --- src/client.ts | 48 +++++++++++++++++++++++++++++++++++++++++++++ test/unit/client.js | 29 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/src/client.ts b/src/client.ts index dfe045f8f..324d7a1fa 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: StreamChat; + _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,50 @@ 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(key: string, options?: StreamChatOptions): StreamChat; + public static getInstance( + key: string, + secret?: string, + options?: StreamChatOptions, + ): StreamChat; + public static getInstance( + key: string, + secretOrOptions?: StreamChatOptions | string, + options?: StreamChatOptions, + ): StreamChat { + if (!StreamChat._instance) { + if (typeof secretOrOptions === 'string') { + StreamChat._instance = new StreamChat(key, secretOrOptions, options); + } else { + StreamChat._instance = new StreamChat(key, secretOrOptions); + } + } + + return StreamChat._instance; + } + devToken(userID: string) { return DevToken(userID); } diff --git a/test/unit/client.js b/test/unit/client.js index 2f693c25f..6eedb7df5 100644 --- a/test/unit/client.js +++ b/test/unit/client.js @@ -3,6 +3,35 @@ import { StreamChat } from '../../src/client'; const expect = chai.expect; +describe('StreamChat getInstance', () => { + beforeEach(() => { + delete StreamChat._instance; + }); + + it('instance is stored as static property', function () { + expect(StreamChat._instance).to.be.undefined; + + const client = StreamChat.getInstance('key'); + expect(client).to.equal(StreamChat._instance); + }); + + it('always return the same instance', function () { + 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', function () { + const client1 = StreamChat.getInstance('key2'); + const client2 = StreamChat.getInstance('key3'); + + expect(client1).to.equal(client2); + expect(client2.key).to.eql('key2'); + }); +}); + describe('Client userMuteStatus', function () { const client = new StreamChat('', ''); const user = { id: 'user' }; From 1ec659551291b0569047859b8d3b46fb1ee58f3e Mon Sep 17 00:00:00 2001 From: Amin Mahboubi Date: Wed, 27 Jan 2021 13:38:38 +0100 Subject: [PATCH 2/4] test: import types from src --- test/typescript/unit-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/typescript/unit-test.ts b/test/typescript/unit-test.ts index 283c37859..2f77acecd 100644 --- a/test/typescript/unit-test.ts +++ b/test/typescript/unit-test.ts @@ -23,7 +23,7 @@ import { PartialUserUpdate, PermissionObject, ConnectAPIResponse, -} from '../../dist/types'; +} from '../../'; const apiKey = 'apiKey'; type UserType = { From 2ce1cdd4cc579d5a536e999f9d483b64da5c5267 Mon Sep 17 00:00:00 2001 From: Amin Mahboubi Date: Wed, 27 Jan 2021 14:25:22 +0100 Subject: [PATCH 3/4] types: getInstace generics --- src/client.ts | 93 ++++++++++++++++++++++++++++++++---- test/typescript/unit-test.ts | 31 +++++++++++- 2 files changed, 114 insertions(+), 10 deletions(-) diff --git a/src/client.ts b/src/client.ts index 324d7a1fa..4d43d97c0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -104,7 +104,7 @@ export class StreamChat< ReactionType extends UnknownType = UnknownType, UserType extends UnknownType = UnknownType > { - private static _instance: StreamChat; + private static _instance?: unknown | StreamChat; // type is undefined|StreamChat, unknown is due to TS limitations with statics _user?: OwnUserResponse | UserResponse; activeChannels: { @@ -338,26 +338,101 @@ export class StreamChat< * @example secret is optional and only used in server side mode * StreamChat.getInstance('api_key', "secret", { httpsAgent: customAgent }) */ - public static getInstance(key: string, options?: StreamChatOptions): StreamChat; - public static getInstance( + 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; - public static getInstance( + ): 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 { + ): StreamChat< + AttachmentType, + ChannelType, + CommandType, + EventType, + MessageType, + ReactionType, + UserType + > { if (!StreamChat._instance) { if (typeof secretOrOptions === 'string') { - StreamChat._instance = new StreamChat(key, secretOrOptions, options); + StreamChat._instance = new StreamChat< + AttachmentType, + ChannelType, + CommandType, + EventType, + MessageType, + ReactionType, + UserType + >(key, secretOrOptions, options); } else { - StreamChat._instance = new StreamChat(key, secretOrOptions); + StreamChat._instance = new StreamChat< + AttachmentType, + ChannelType, + CommandType, + EventType, + MessageType, + ReactionType, + UserType + >(key, secretOrOptions); } } - return StreamChat._instance; + return StreamChat._instance as StreamChat< + AttachmentType, + ChannelType, + CommandType, + EventType, + MessageType, + ReactionType, + UserType + >; } devToken(userID: string) { diff --git a/test/typescript/unit-test.ts b/test/typescript/unit-test.ts index 2f77acecd..c155423a1 100644 --- a/test/typescript/unit-test.ts +++ b/test/typescript/unit-test.ts @@ -23,7 +23,7 @@ import { PartialUserUpdate, PermissionObject, ConnectAPIResponse, -} from '../../'; +} from '../../dist/types'; const apiKey = 'apiKey'; type UserType = { @@ -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); From d2fcfc3394cdc0501db4dc7e3aa5f7e4363324e9 Mon Sep 17 00:00:00 2001 From: Amin Mahboubi Date: Wed, 27 Jan 2021 15:13:08 +0100 Subject: [PATCH 4/4] test: client getInstance mutilple connect --- test/unit/client.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/test/unit/client.js b/test/unit/client.js index 6eedb7df5..fc94f555d 100644 --- a/test/unit/client.js +++ b/test/unit/client.js @@ -8,14 +8,14 @@ describe('StreamChat getInstance', () => { delete StreamChat._instance; }); - it('instance is stored as static property', function () { + 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', function () { + it('always return the same instance', () => { const client1 = StreamChat.getInstance('key1'); const client2 = StreamChat.getInstance('key1'); const client3 = StreamChat.getInstance('key1'); @@ -23,13 +23,25 @@ describe('StreamChat getInstance', () => { expect(client2).to.equal(client3); }); - it('changin params has no effect', function () { + 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 () {