From 501904a912258a40c0e4b3f10ec69e21f6ea6cdd Mon Sep 17 00:00:00 2001 From: Kamal Bennani Date: Mon, 20 Nov 2023 10:54:50 +0100 Subject: [PATCH 1/2] feat: expose a way to switch from one cluster/app to the other --- package-lock.json | 4 ++-- src/core/connection/connection_manager.ts | 9 +++++++++ src/core/options.ts | 5 +++++ src/core/pusher.ts | 20 ++++++++++++++----- .../core/connection/connection_manager.d.ts | 1 + types/src/core/options.d.ts | 4 ++++ types/src/core/pusher.d.ts | 4 +++- 7 files changed, 39 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0e9f6a439..b7ea0c54c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pusher-js", - "version": "8.2.0", + "version": "8.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pusher-js", - "version": "8.2.0", + "version": "8.3.0", "license": "MIT", "dependencies": { "tweetnacl": "^1.0.3" diff --git a/src/core/connection/connection_manager.ts b/src/core/connection/connection_manager.ts index f913da446..333abcde5 100644 --- a/src/core/connection/connection_manager.ts +++ b/src/core/connection/connection_manager.ts @@ -96,6 +96,15 @@ export default class ConnectionManager extends EventsDispatcher { this.updateStrategy(); } + switchCluster(key: string) { + this.key = key; + // This ensures that the new config coming from + // pusher instance are taken into account + // such as appKey and cluster + this.updateStrategy(); + this.retryIn(0); + } + /** Establishes a connection to Pusher. * * Does nothing when connection is already established. See top-level doc diff --git a/src/core/options.ts b/src/core/options.ts index baf974eee..b748a02a3 100644 --- a/src/core/options.ts +++ b/src/core/options.ts @@ -44,6 +44,11 @@ export interface Options { wssPort?: number; } +export interface ClusterOptions { + appKey: string; + cluster: string; +} + export function validateOptions(options) { if (options == null) { throw 'You must pass an options object'; diff --git a/src/core/pusher.ts b/src/core/pusher.ts index 0d9616094..b1ffc8ed1 100644 --- a/src/core/pusher.ts +++ b/src/core/pusher.ts @@ -1,6 +1,5 @@ import AbstractRuntime from '../runtimes/interface'; import Runtime from 'runtime'; -import Util from './util'; import * as Collections from './utils/collections'; import Channels from './channels/channels'; import Channel from './channels/channel'; @@ -10,14 +9,11 @@ import TimelineSender from './timeline/timeline_sender'; import TimelineLevel from './timeline/level'; import { defineTransport } from './strategies/strategy_builder'; import ConnectionManager from './connection/connection_manager'; -import ConnectionManagerOptions from './connection/connection_manager_options'; import { PeriodicTimer } from './utils/timers'; import Defaults from './defaults'; -import * as DefaultConfig from './config'; import Logger from './logger'; import Factory from './utils/factory'; -import UrlStore from 'core/utils/url_store'; -import { Options, validateOptions } from './options'; +import { Options, ClusterOptions, validateOptions } from './options'; import { Config, getConfig } from './config'; import StrategyOptions from './strategies/strategy_options'; import UserFacade from './user'; @@ -53,6 +49,7 @@ export default class Pusher { /* INSTANCE PROPERTIES */ key: string; + options: Options; config: Config; channels: Channels; global_emitter: EventsDispatcher; @@ -141,6 +138,19 @@ export default class Pusher { } } + /** + * Allows you to switch Pusher cluster without + * losing all the channels/subscription binding + * as this is internally managed by the SDK. + */ + switchCluster(options: ClusterOptions) { + const { appKey, cluster } = options; + this.key = appKey; + this.options = { ...this.options, cluster }; + this.config = getConfig(this.options, this); + this.connection.switchCluster(this.key); + } + channel(name: string): Channel { return this.channels.find(name); } diff --git a/types/src/core/connection/connection_manager.d.ts b/types/src/core/connection/connection_manager.d.ts index e63fea71f..53dc9110f 100644 --- a/types/src/core/connection/connection_manager.d.ts +++ b/types/src/core/connection/connection_manager.d.ts @@ -24,6 +24,7 @@ export default class ConnectionManager extends EventsDispatcher { handshakeCallbacks: HandshakeCallbacks; connectionCallbacks: ConnectionCallbacks; constructor(key: string, options: ConnectionManagerOptions); + switchCluster(key: string): void; connect(): void; send(data: any): boolean; send_event(name: string, data: any, channel?: string): boolean; diff --git a/types/src/core/options.d.ts b/types/src/core/options.d.ts index 64c47f625..ee8ce2b3e 100644 --- a/types/src/core/options.d.ts +++ b/types/src/core/options.d.ts @@ -31,4 +31,8 @@ export interface Options { wsPort?: number; wssPort?: number; } +export interface ClusterOptions { + appKey: string; + cluster: string; +} export declare function validateOptions(options: any): void; diff --git a/types/src/core/pusher.d.ts b/types/src/core/pusher.d.ts index 9d0f652d7..a3708fc0e 100644 --- a/types/src/core/pusher.d.ts +++ b/types/src/core/pusher.d.ts @@ -6,7 +6,7 @@ import Timeline from './timeline/timeline'; import TimelineSender from './timeline/timeline_sender'; import ConnectionManager from './connection/connection_manager'; import { PeriodicTimer } from './utils/timers'; -import { Options } from './options'; +import { Options, ClusterOptions } from './options'; import { Config } from './config'; import UserFacade from './user'; export default class Pusher { @@ -21,6 +21,7 @@ export default class Pusher { static log: (message: any) => void; private static getClientFeatures; key: string; + options: Options; config: Config; channels: Channels; global_emitter: EventsDispatcher; @@ -31,6 +32,7 @@ export default class Pusher { timelineSenderTimer: PeriodicTimer; user: UserFacade; constructor(app_key: string, options: Options); + switchCluster(options: ClusterOptions): void; channel(name: string): Channel; allChannels(): Channel[]; connect(): void; From 83f4db8ac42c37518739249a70252f3ca3759e74 Mon Sep 17 00:00:00 2001 From: Kamal Bennani Date: Mon, 20 Nov 2023 12:06:57 +0100 Subject: [PATCH 2/2] test: cover the new logic with unit tests --- spec/javascripts/helpers/mocks.js | 1 + .../connection/connection_manager_spec.js | 43 +++++++++++++++++++ spec/javascripts/unit/core/pusher_spec.js | 41 ++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/spec/javascripts/helpers/mocks.js b/spec/javascripts/helpers/mocks.js index 0e11d7c13..400affbf9 100644 --- a/spec/javascripts/helpers/mocks.js +++ b/spec/javascripts/helpers/mocks.js @@ -245,6 +245,7 @@ var Mocks = { manager.disconnect = jasmine.createSpy("disconnect"); manager.send_event = jasmine.createSpy("send_event"); manager.isUsingTLS = jasmine.createSpy("isUsingTLS").and.returnValue(false); + manager.switchCluster = jasmine.createSpy("switchCluster"); return manager; }, diff --git a/spec/javascripts/unit/core/connection/connection_manager_spec.js b/spec/javascripts/unit/core/connection/connection_manager_spec.js index 412577cc1..db0a7c83d 100644 --- a/spec/javascripts/unit/core/connection/connection_manager_spec.js +++ b/spec/javascripts/unit/core/connection/connection_manager_spec.js @@ -116,6 +116,49 @@ describe("ConnectionManager", function() { }); }); + describe("#switchCluster", function() { + it("should update cluster key", function() { + expect(manager.key).toEqual("foo"); + manager.switchCluster("bar"); + expect(manager.key).toEqual("bar"); + }); + + it("should re-build the strategy", function() { + expect(managerOptions.getStrategy.calls.count()).toEqual(1); + manager.switchCluster("bar"); + expect(managerOptions.getStrategy.calls.count()).toEqual(2); + expect(managerOptions.getStrategy).toHaveBeenCalledWith({ + key: "bar", + useTLS: false, + timeline: timeline + }); + }); + + it("should try to connect using the strategy", function() { + manager.switchCluster("bar"); + // connection is retried with a zero delay + jasmine.clock().tick(0); + expect(strategy.connect).toHaveBeenCalled(); + }); + + it("should transition to connecting", function() { + var onConnecting = jasmine.createSpy("onConnecting"); + var onStateChange = jasmine.createSpy("onStateChange"); + manager.bind("connecting", onConnecting); + manager.bind("state_change", onStateChange); + + manager.switchCluster("bar");// connection is retried with a zero delay + jasmine.clock().tick(0); + + expect(manager.state).toEqual("connecting"); + expect(onConnecting).toHaveBeenCalled(); + expect(onStateChange).toHaveBeenCalledWith({ + previous: "initialized", + current: "connecting" + }); + }); + }); + describe("before establishing a connection", function() { beforeEach(function() { manager.connect(); diff --git a/spec/javascripts/unit/core/pusher_spec.js b/spec/javascripts/unit/core/pusher_spec.js index 250b420ea..28cfd73f9 100644 --- a/spec/javascripts/unit/core/pusher_spec.js +++ b/spec/javascripts/unit/core/pusher_spec.js @@ -307,6 +307,47 @@ describe("Pusher", function() { }); }); + describe("switch cluster", function() { + var pusher; + var subscribedChannels + + beforeEach(function() { + pusher = new Pusher("foo", {cluster: "mt1"}); + + subscribedChannels = { + channel1: pusher.subscribe("channel1"), + channel2: pusher.subscribe("channel2") + }; + + pusher.connect(); + pusher.connection.state = "connected"; + pusher.connection.emit("connected"); + }); + + it("should resubscribe to all channels", function() { + expect(subscribedChannels.channel1.subscribe).toHaveBeenCalledTimes(1); + expect(subscribedChannels.channel2.subscribe).toHaveBeenCalledTimes(1); + + pusher.switchCluster({ appKey: 'bar', cluster: 'us3' }); + pusher.connect(); + pusher.connection.state = 'connected'; + pusher.connection.emit('connected'); + + expect(subscribedChannels.channel1.subscribe).toHaveBeenCalledTimes(2); + expect(subscribedChannels.channel2.subscribe).toHaveBeenCalledTimes(2); + }); + + it("should send events via the connection manager", function() { + pusher.switchCluster({ appKey: 'bar', cluster: 'us3' }); + pusher.send_event("event", { key: "value" }, "channel"); + expect(pusher.connection.send_event).toHaveBeenCalledWith( + "event", + { key: "value" }, + "channel" + ); + }); + }) + describe("#unsubscribe", function() { it("should unsubscribe the channel if subscription is not pending", function() { var channel = pusher.subscribe("yyy");