centrifuge 5.4.0 → 5.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,14 +16,15 @@ The features implemented by this SDK can be found in [SDK feature matrix](https:
16
16
  * [WebTransport (experimental)](#webtransport-experimental)
17
17
  * [Client API](#client-api)
18
18
  * [Client methods and events](#client-methods-and-events)
19
+ * [Client options](#client-options)
19
20
  * [Connection token](#connection-token)
20
21
  * [Subscription API](#subscription-api)
21
22
  * [Subscription methods and events](#subscription-methods-and-events)
22
23
  * [Subscription token](#subscription-token)
24
+ * [Subscription options](#subscription-options)
23
25
  * [Subscription management API](#subscription-management-api)
24
26
  * [Message batching](#message-batching)
25
27
  * [Server-side subscriptions](#server-side-subscriptions)
26
- * [Configuration options](#configuration-options)
27
28
  * [Protobuf support](#protobuf-support)
28
29
  * [Using with NodeJS](#using-with-nodejs)
29
30
  * [Custom WebSocket constructor](#custom-websocket-constructor)
@@ -223,6 +224,16 @@ centrifuge.on('disconnected', function(ctx) {
223
224
  });
224
225
  ```
225
226
 
227
+ #### state event
228
+
229
+ `state` event is fired when client state changes. It provides both old and new state.
230
+
231
+ ```javascript
232
+ centrifuge.on('state', function(ctx) {
233
+ console.log('state changed from', ctx.oldState, 'to', ctx.newState);
234
+ });
235
+ ```
236
+
226
237
  #### disconnect method
227
238
 
228
239
  In some cases you may need to disconnect your client from server, use `.disconnect()` method to do this:
@@ -313,6 +324,22 @@ Returns a Promise which will be resolved upon connection establishement (i.e. wh
313
324
 
314
325
  `setToken` may be useful to dynamically change the connection token. For example when you need to implement login/logout workflow. See an example in [blog post](https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#token-behaviour-adjustments-in-sdks).
315
326
 
327
+ #### setData method
328
+
329
+ `setData` (since v5.5.0) allows setting connection data (some extra payload to deliver to the backend with connection request). This only affects the next connection attempt, not the current one. Note that if `getData` callback is configured, it will override this value during reconnects.
330
+
331
+ ```javascript
332
+ centrifuge.setData({ 'name': 'Maria' });
333
+ ```
334
+
335
+ #### setHeaders method
336
+
337
+ `setHeaders` allows setting connection [emulated headers](https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#headers-emulation). These headers will be sent with the next connection attempt. **Requires Centrifugo v6**.
338
+
339
+ ```javascript
340
+ centrifuge.setHeaders({ 'Authorization': 'XXX' });
341
+ ```
342
+
316
343
  #### error event
317
344
 
318
345
  To listen asynchronous error happening internally while Centrifuge client works you can set an `error` handler:
@@ -327,6 +354,121 @@ centrifuge.on('error', function(ctx) {
327
354
 
328
355
  This can help you to log failed connection attempts, or token refresh errors, etc.
329
356
 
357
+ ### Client options
358
+
359
+ Let's look at available configuration parameters when initializing `Centrifuge` object instance.
360
+
361
+ #### token
362
+
363
+ Set initial connection token (JWT). See [Connection Token](#connection-token) section for more details.
364
+
365
+ #### getToken
366
+
367
+ Set function for getting connection token. This may be used for initial token loading and token refresh mechanism (when initial token is going to expire). See [Connection Token](#connection-token) section for more details.
368
+
369
+ #### data
370
+
371
+ Set custom data to send to a server within every connect command.
372
+
373
+ #### getData
374
+
375
+ Set function for getting/renewing connection data. This callback is called upon reconnects to get fresh connection data. In many cases you may prefer using `setData` method of Centrifuge Client instead.
376
+
377
+ ```javascript
378
+ const centrifuge = new Centrifuge('ws://localhost:8000/connection/websocket', {
379
+ getData: async () => {
380
+ // Return fresh data on each reconnect
381
+ return { 'timestamp': Date.now() };
382
+ }
383
+ });
384
+ ```
385
+
386
+ #### name
387
+
388
+ Set custom client name. By default, it's set to `js`. This is useful for analytics and semantically must identify an environment from which client establishes a connection.
389
+
390
+ #### version
391
+
392
+ Version of your application - useful for analytics.
393
+
394
+ #### headers
395
+
396
+ Provide header emulation - these headers are sent with first protocol message. The backend can process those in a customized manner. In case of Centrifugo these headers are then used like real HTTP headers sent from the client. **Requires Centrifugo v6**.
397
+
398
+ ```javascript
399
+ const centrifuge = new Centrifuge('ws://localhost:8000/connection/websocket', {
400
+ headers: {
401
+ 'X-Custom-Header': 'value',
402
+ 'Authorization': 'Bearer token'
403
+ }
404
+ });
405
+ ```
406
+
407
+ #### debug
408
+
409
+ `debug` is a boolean option which is `false` by default. When enabled lots of various debug
410
+ messages will be logged into javascript console. Mostly useful for development or
411
+ troubleshooting.
412
+
413
+ #### minReconnectDelay
414
+
415
+ When client disconnected from a server it will automatically try to reconnect using a backoff algorithm with jitter. `minReconnectDelay` option sets minimal interval value in milliseconds before first reconnect attempt. Default is `500` milliseconds.
416
+
417
+ #### maxReconnectDelay
418
+
419
+ `maxReconnectDelay` sets an upper reconnect delay value. Default is `20000` milliseconds - i.e. clients won't have delays between reconnect attempts which are larger than 20 seconds.
420
+
421
+ #### maxServerPingDelay
422
+
423
+ `maxServerPingDelay` sets the maximum delay of server pings after which connection is considered broken and client reconnects. In milliseconds. Default is `10000`.
424
+
425
+ #### timeout
426
+
427
+ Timeout for operations in milliseconds. Default is `5000`.
428
+
429
+ #### websocket
430
+
431
+ `websocket` option allows to explicitly provide custom WebSocket client to use. By default centrifuge-js will try to use global WebSocket object, so if you are in web browser – it will just use native WebSocket implementation. See notes about using `centrifuge-js` with NodeJS below.
432
+
433
+ #### fetch
434
+
435
+ Provide shim for fetch implementation. Useful when working in environments where fetch is not available globally.
436
+
437
+ #### readableStream
438
+
439
+ Provide shim for ReadableStream. Useful when working in environments where ReadableStream is not available globally.
440
+
441
+ #### eventsource
442
+
443
+ Provide shim for EventSource object. Useful when working in environments where EventSource is not available globally.
444
+
445
+ #### emulationEndpoint
446
+
447
+ Which emulation endpoint to use for bidirectional emulation transports. Default is `/emulation`.
448
+
449
+ #### sockjs
450
+
451
+ `sockjs` option allows to explicitly provide SockJS client object to Centrifuge client.
452
+
453
+ #### sockjsOptions
454
+
455
+ `sockjsOptions` allows modifying options passed to SockJS constructor. For example:
456
+
457
+ ```javascript
458
+ const centrifuge = new Centrifuge(transports, {
459
+ sockjs: SockJS,
460
+ sockjsOptions: {
461
+ transports: ['websocket', 'xhr-streaming'],
462
+ timeout: 10000
463
+ }
464
+ });
465
+ ```
466
+
467
+ #### networkEventTarget
468
+
469
+ EventTarget for network online/offline events. In browser environment Centrifuge uses global window online/offline events automatically by default. This option allows providing a custom EventTarget for handling network state changes in other environments.
470
+
471
+
330
472
  ### Connection Token
331
473
 
332
474
  Depending on authentication scheme used by a server you may also want to provide connection token:
@@ -535,6 +677,25 @@ sub.removeAllListeners();
535
677
 
536
678
  Returns a Promise which will be resolved upon subscription success (i.e. when Subscription goes to `subscribed` state).
537
679
 
680
+ #### setData method of subscription
681
+
682
+ `setData` (since v5.5.0) allows setting subscription data (some extra payload to deliver to the backend with subscription request). This only applies on the next subscription attempt. Note that if `getData` callback is configured, it will override this value during resubscriptions.
683
+
684
+ #### setTagsFilter method of subscription
685
+
686
+ `setTagsFilter` (since v5.5.0) allows setting tags filter for the subscription. Cannot be used together with `delta` option.
687
+
688
+ See Centrifugo [Channel publication filtering](https://centrifugal.dev/docs/server/publication_filtering) docs.
689
+
690
+ ```javascript
691
+ const tagsFilter = {
692
+ key: "ticker",
693
+ cmp: "eq",
694
+ val: ticker
695
+ };
696
+ sub.setTagsFilter(tagsFilter);
697
+ ```
698
+
538
699
  ### Subscription token
539
700
 
540
701
  You may want to provide subscription token:
@@ -586,6 +747,81 @@ sub.subscribe();
586
747
 
587
748
  > If initial token is not provided, but `getToken` is specified – then SDK assumes that developer wants to use token authorization for a channel subscription. In this case SDK attempts to get a subscription token before initial subscribe.
588
749
 
750
+ ### Subscription Options
751
+
752
+ When creating a new subscription using `centrifuge.newSubscription(channel, options)`, you can provide various options to customize subscription behavior:
753
+
754
+ #### token
755
+
756
+ Allows setting initial subscription token (JWT). See [Subscription token](#subscription-token) section for more details.
757
+
758
+ #### getToken
759
+
760
+ Allows setting function to get/refresh subscription token. This will only be called when new token needed, not on every resubscribe. See [Subscription token](#subscription-token) section for more details.
761
+
762
+ #### data
763
+
764
+ Data to send to a server with subscribe command.
765
+
766
+ #### getData
767
+
768
+ Allows setting function to get/renew subscription data during resubscriptions. In many cases you may prefer using `setData` method of Subscription instead.
769
+
770
+ ```javascript
771
+ const sub = centrifuge.newSubscription("news", {
772
+ getData: async (ctx) => {
773
+ // ctx.channel contains channel name
774
+ return { 'timestamp': Date.now() };
775
+ }
776
+ });
777
+ ```
778
+
779
+ #### since
780
+
781
+ Force recovery on first subscribe from a provided `StreamPosition`. This is useful when you want to recover messages from a specific point during initial Subscription initialization.
782
+
783
+ ```javascript
784
+ const sub = centrifuge.newSubscription("news", {
785
+ since: { offset: 100, epoch: 'xyz' }
786
+ });
787
+ ```
788
+
789
+ #### minResubscribeDelay
790
+
791
+ Min delay between resubscribe attempts in milliseconds. Default is `500`.
792
+
793
+ #### maxResubscribeDelay
794
+
795
+ Max delay between resubscribe attempts in milliseconds. Default is `20000`.
796
+
797
+ #### delta
798
+
799
+ Delta format to be used for differential updates. Currently only `'fossil'` is supported. Cannot be used together with `tagsFilter`.
800
+
801
+ ```javascript
802
+ const sub = centrifuge.newSubscription("news", {
803
+ delta: 'fossil'
804
+ });
805
+ ```
806
+
807
+ #### tagsFilter
808
+
809
+ Server-side tags filter to apply for publications in channel. Cannot be used together with `delta`.
810
+
811
+ See Centrifugo [Channel publication filtering](https://centrifugal.dev/docs/server/publication_filtering) docs.
812
+
813
+ ```javascript
814
+ const tagsFilter = {
815
+ key: "ticker",
816
+ cmp: "eq",
817
+ val: ticker
818
+ };
819
+
820
+ const sub = centrifuge.newSubscription("tickers", {
821
+ tagsFilter: tagsFilter
822
+ });
823
+ ```
824
+
589
825
  ## Subscription management API
590
826
 
591
827
  According to [client SDK spec](https://centrifugal.dev/docs/transports/client_api#subscription-management) centrifuge-js supports several methods to manage client-side subscriptions in internal registry. The following methods are available on top level of the Centrifuge SDK client instance.
@@ -680,62 +916,6 @@ Additionally, Client has several top-level methods to call with server-side subs
680
916
  * `presence(channel)`
681
917
  * `presenceStats(channel)`
682
918
 
683
- ## Configuration options
684
-
685
- You can check out all available options with description [in source code](https://github.com/centrifugal/centrifuge-js/blob/master/src/types.ts#L82).
686
-
687
- Let's look at available configuration parameters when initializing `Centrifuge` object instance.
688
-
689
- ### debug
690
-
691
- `debug` is a boolean option which is `false` by default. When enabled lots of various debug
692
- messages will be logged into javascript console. Mostly useful for development or
693
- troubleshooting.
694
-
695
- ### minReconnectDelay
696
-
697
- When client disconnected from a server it will automatically try to reconnect using a backoff algorithm with jitter. `minReconnectDelay` option sets minimal interval value in milliseconds before first reconnect attempt. Default is `500` milliseconds.
698
-
699
- ### maxReconnectDelay
700
-
701
- `maxReconnectDelay` sets an upper reconnect delay value. Default is `20000` milliseconds - i.e. clients won't have delays between reconnect attempts which are larger than 20 seconds.
702
-
703
- ### maxServerPingDelay
704
-
705
- `maxServerPingDelay` sets the maximum delay of server pings after which connection is considered broken and client reconnects. In milliseconds. Default is `10000`.
706
-
707
- ### token
708
-
709
- Set initial connection token.
710
-
711
- ### getToken
712
-
713
- Set function for getting connection token. This may be used for initial token loading and token refresh mechanism (when initial token is going to expire).
714
-
715
- ### data
716
-
717
- Set custom data to send to a server withing every connect command.
718
-
719
- ### name
720
-
721
- Set custom client name. By default, it's set to `js`. This is useful for analitycs and semantically must identify an environment from which client establishes a connection.
722
-
723
- ### version
724
-
725
- Version of your application - useful for analitycs.
726
-
727
- ### timeout
728
-
729
- Timeout for operations in milliseconds.
730
-
731
- ### websocket
732
-
733
- `websocket` option allows to explicitly provide custom WebSocket client to use. By default centrifuge-js will try to use global WebSocket object, so if you are in web browser – it will just use native WebSocket implementation. See notes about using `centrifuge-js` with NodeJS below.
734
-
735
- ### sockjs
736
-
737
- `sockjs` option allows to explicitly provide SockJS client object to Centrifuge client.
738
-
739
919
  ## Protobuf support
740
920
 
741
921
  To import client which uses Protobuf protocol under the hood:
@@ -71,6 +71,10 @@ export declare class Centrifuge extends Centrifuge_base {
71
71
  disconnect(): void;
72
72
  /** setToken allows setting connection token. Or resetting used token to be empty. */
73
73
  setToken(token: string): void;
74
+ /** setData allows setting connection data. This only affects the next connection attempt,
75
+ * not the current one. Note that if getData callback is configured, it will override
76
+ * this value during reconnects. */
77
+ setData(data: any): void;
74
78
  /** setHeaders allows setting connection emulated headers. */
75
79
  setHeaders(headers: {
76
80
  [key: string]: string;
package/build/codes.d.ts CHANGED
@@ -34,3 +34,6 @@ export declare enum unsubscribedCodes {
34
34
  unauthorized = 1,
35
35
  clientClosed = 2
36
36
  }
37
+ export declare enum subscriptionFlags {
38
+ channelCompaction = 1
39
+ }
package/build/index.js CHANGED
@@ -556,6 +556,10 @@ exports.unsubscribedCodes = void 0;
556
556
  unsubscribedCodes[unsubscribedCodes["unauthorized"] = 1] = "unauthorized";
557
557
  unsubscribedCodes[unsubscribedCodes["clientClosed"] = 2] = "clientClosed";
558
558
  })(exports.unsubscribedCodes || (exports.unsubscribedCodes = {}));
559
+ exports.subscriptionFlags = void 0;
560
+ (function (subscriptionFlags) {
561
+ subscriptionFlags[subscriptionFlags["channelCompaction"] = 1] = "channelCompaction";
562
+ })(exports.subscriptionFlags || (exports.subscriptionFlags = {}));
559
563
 
560
564
  /** State of client. */
561
565
  exports.State = void 0;
@@ -632,6 +636,7 @@ class Subscription extends EventEmitter$1 {
632
636
  this._recover = false;
633
637
  this._offset = null;
634
638
  this._epoch = null;
639
+ this._id = 0;
635
640
  this._recoverable = false;
636
641
  this._positioned = false;
637
642
  this._joinLeave = false;
@@ -645,6 +650,7 @@ class Subscription extends EventEmitter$1 {
645
650
  this._refreshTimeout = null;
646
651
  this._delta = '';
647
652
  this._delta_negotiated = false;
653
+ this._tagsFilter = null;
648
654
  this._prevValue = null;
649
655
  this._unsubPromise = Promise.resolve();
650
656
  this._setOptions(options);
@@ -726,6 +732,57 @@ class Subscription extends EventEmitter$1 {
726
732
  return this._centrifuge.history(this.channel, opts);
727
733
  });
728
734
  }
735
+ /**
736
+ * Sets server-side tags filter for the subscription.
737
+ * This only applies on the next subscription attempt, not the current one.
738
+ * Cannot be used together with delta option.
739
+ *
740
+ * @param tagsFilter - Filter configuration object or null to remove filter
741
+ * @throws {Error} If both delta and tagsFilter are configured
742
+ *
743
+ * @example
744
+ * ```typescript
745
+ * // Simple equality filter
746
+ * sub.setTagsFilter({
747
+ * key: 'ticker',
748
+ * cmp: 'eq',
749
+ * val: 'BTC'
750
+ * });
751
+ * ```
752
+ *
753
+ * @example
754
+ * ```typescript
755
+ * // Complex filter with logical operators
756
+ * sub.setTagsFilter({
757
+ * op: 'and',
758
+ * nodes: [
759
+ * { key: 'ticker', cmp: 'eq', val: 'BTC' },
760
+ * { key: 'price', cmp: 'gt', val: '50000' }
761
+ * ]
762
+ * });
763
+ * ```
764
+ *
765
+ * @example
766
+ * ```typescript
767
+ * // Filter with IN operator
768
+ * sub.setTagsFilter({
769
+ * key: 'ticker',
770
+ * cmp: 'in',
771
+ * vals: ['BTC', 'ETH', 'SOL']
772
+ * });
773
+ * ```
774
+ */
775
+ setTagsFilter(tagsFilter) {
776
+ if (tagsFilter && this._delta) {
777
+ throw new Error('cannot use delta and tagsFilter together');
778
+ }
779
+ this._tagsFilter = tagsFilter;
780
+ }
781
+ /** setData allows setting subscription data. This only applied on the next subscription attempt,
782
+ * Note that if getData callback is configured, it will override this value during resubscriptions. */
783
+ setData(data) {
784
+ this._data = data;
785
+ }
729
786
  _methodCall() {
730
787
  if (this._isSubscribed()) {
731
788
  return Promise.resolve();
@@ -788,6 +845,9 @@ class Subscription extends EventEmitter$1 {
788
845
  return;
789
846
  }
790
847
  this._clearSubscribingState();
848
+ if (result.id) {
849
+ this._id = result.id;
850
+ }
791
851
  if (result.recoverable) {
792
852
  this._recover = true;
793
853
  this._offset = result.offset || 0;
@@ -983,6 +1043,7 @@ class Subscription extends EventEmitter$1 {
983
1043
  req.recoverable = true;
984
1044
  if (this._joinLeave)
985
1045
  req.join_leave = true;
1046
+ req.flag = exports.subscriptionFlags.channelCompaction;
986
1047
  if (this._needRecover()) {
987
1048
  req.recover = true;
988
1049
  const offset = this._getOffset();
@@ -994,6 +1055,8 @@ class Subscription extends EventEmitter$1 {
994
1055
  }
995
1056
  if (this._delta)
996
1057
  req.delta = this._delta;
1058
+ if (this._tagsFilter)
1059
+ req.tf = this._tagsFilter;
997
1060
  return { subscribe: req };
998
1061
  }
999
1062
  _debug(...args) {
@@ -1174,6 +1237,12 @@ class Subscription extends EventEmitter$1 {
1174
1237
  }
1175
1238
  this._delta = options.delta;
1176
1239
  }
1240
+ if (options.tagsFilter) {
1241
+ this._tagsFilter = options.tagsFilter;
1242
+ }
1243
+ if (this._tagsFilter && this._delta) {
1244
+ throw new Error('cannot use delta and tagsFilter together');
1245
+ }
1177
1246
  }
1178
1247
  _getOffset() {
1179
1248
  const offset = this._offset;
@@ -2155,22 +2224,24 @@ class Centrifuge extends EventEmitter$1 {
2155
2224
  * state and rejects in case of client goes to Disconnected or Failed state.
2156
2225
  * Users can provide optional timeout in milliseconds. */
2157
2226
  ready(timeout) {
2158
- switch (this.state) {
2159
- case exports.State.Disconnected:
2160
- return Promise.reject({ code: exports.errorCodes.clientDisconnected, message: 'client disconnected' });
2161
- case exports.State.Connected:
2162
- return Promise.resolve();
2163
- default:
2164
- return new Promise((resolve, reject) => {
2165
- const ctx = { resolve, reject };
2166
- if (timeout) {
2167
- ctx.timeout = setTimeout(() => {
2168
- reject({ code: exports.errorCodes.timeout, message: 'timeout' });
2169
- }, timeout);
2170
- }
2171
- this._promises[this._nextPromiseId()] = ctx;
2172
- });
2173
- }
2227
+ return __awaiter(this, void 0, void 0, function* () {
2228
+ switch (this.state) {
2229
+ case exports.State.Disconnected:
2230
+ throw { code: exports.errorCodes.clientDisconnected, message: 'client disconnected' };
2231
+ case exports.State.Connected:
2232
+ return;
2233
+ default:
2234
+ return new Promise((resolve, reject) => {
2235
+ const ctx = { resolve, reject };
2236
+ if (timeout) {
2237
+ ctx.timeout = setTimeout(() => {
2238
+ reject({ code: exports.errorCodes.timeout, message: 'timeout' });
2239
+ }, timeout);
2240
+ }
2241
+ this._promises[this._nextPromiseId()] = ctx;
2242
+ });
2243
+ }
2244
+ });
2174
2245
  }
2175
2246
  /** connect to a server. */
2176
2247
  connect() {
@@ -2194,6 +2265,12 @@ class Centrifuge extends EventEmitter$1 {
2194
2265
  setToken(token) {
2195
2266
  this._token = token;
2196
2267
  }
2268
+ /** setData allows setting connection data. This only affects the next connection attempt,
2269
+ * not the current one. Note that if getData callback is configured, it will override
2270
+ * this value during reconnects. */
2271
+ setData(data) {
2272
+ this._data = data;
2273
+ }
2197
2274
  /** setHeaders allows setting connection emulated headers. */
2198
2275
  setHeaders(headers) {
2199
2276
  this._config.headers = headers;
@@ -2360,7 +2437,7 @@ class Centrifuge extends EventEmitter$1 {
2360
2437
  this._codec = new JsonCodec();
2361
2438
  this._formatOverride();
2362
2439
  if (this._config.debug === true ||
2363
- (typeof localStorage !== 'undefined' && localStorage.getItem('centrifuge.debug'))) {
2440
+ (typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function' && localStorage.getItem('centrifuge.debug'))) {
2364
2441
  this._debugEnabled = true;
2365
2442
  }
2366
2443
  this._debug('config', this._config);
@@ -3278,7 +3355,19 @@ class Centrifuge extends EventEmitter$1 {
3278
3355
  });
3279
3356
  return unsubscribePromise;
3280
3357
  }
3281
- _getSub(channel) {
3358
+ _getSub(channel, id) {
3359
+ if (id && id > 0) {
3360
+ for (const ch in this._subs) {
3361
+ if (this._subs.hasOwnProperty(ch)) {
3362
+ const sub = this._subs[ch];
3363
+ // @ts-ignore – we are accessing private property for internal use
3364
+ if (sub._id === id) {
3365
+ return sub;
3366
+ }
3367
+ }
3368
+ }
3369
+ return null;
3370
+ }
3282
3371
  const sub = this._subs[channel];
3283
3372
  if (!sub) {
3284
3373
  return null;
@@ -3491,9 +3580,9 @@ class Centrifuge extends EventEmitter$1 {
3491
3580
  errback({ error, next });
3492
3581
  }
3493
3582
  }
3494
- _handleJoin(channel, join) {
3495
- const sub = this._getSub(channel);
3496
- if (!sub) {
3583
+ _handleJoin(channel, join, id) {
3584
+ const sub = this._getSub(channel, id);
3585
+ if (!sub && channel) {
3497
3586
  if (this._isServerSub(channel)) {
3498
3587
  const ctx = { channel: channel, info: this._getJoinLeaveContext(join.info) };
3499
3588
  this.emit('join', ctx);
@@ -3503,9 +3592,9 @@ class Centrifuge extends EventEmitter$1 {
3503
3592
  // @ts-ignore – we are hiding some symbols from public API autocompletion.
3504
3593
  sub._handleJoin(join);
3505
3594
  }
3506
- _handleLeave(channel, leave) {
3507
- const sub = this._getSub(channel);
3508
- if (!sub) {
3595
+ _handleLeave(channel, leave, id) {
3596
+ const sub = this._getSub(channel, id);
3597
+ if (!sub && channel) {
3509
3598
  if (this._isServerSub(channel)) {
3510
3599
  const ctx = { channel: channel, info: this._getJoinLeaveContext(leave.info) };
3511
3600
  this.emit('leave', ctx);
@@ -3516,8 +3605,8 @@ class Centrifuge extends EventEmitter$1 {
3516
3605
  sub._handleLeave(leave);
3517
3606
  }
3518
3607
  _handleUnsubscribe(channel, unsubscribe) {
3519
- const sub = this._getSub(channel);
3520
- if (!sub) {
3608
+ const sub = this._getSub(channel, 0);
3609
+ if (!sub && channel) {
3521
3610
  if (this._isServerSub(channel)) {
3522
3611
  delete this._serverSubs[channel];
3523
3612
  this.emit('unsubscribed', { channel: channel });
@@ -3580,9 +3669,9 @@ class Centrifuge extends EventEmitter$1 {
3580
3669
  }
3581
3670
  return info;
3582
3671
  }
3583
- _handlePublication(channel, pub) {
3584
- const sub = this._getSub(channel);
3585
- if (!sub) {
3672
+ _handlePublication(channel, pub, id) {
3673
+ const sub = this._getSub(channel, id);
3674
+ if (!sub && channel) {
3586
3675
  if (this._isServerSub(channel)) {
3587
3676
  const ctx = this._getPublicationContext(channel, pub);
3588
3677
  this.emit('publication', ctx);
@@ -3607,17 +3696,18 @@ class Centrifuge extends EventEmitter$1 {
3607
3696
  }
3608
3697
  _handlePush(data, next) {
3609
3698
  const channel = data.channel;
3699
+ const id = data.id;
3610
3700
  if (data.pub) {
3611
- this._handlePublication(channel, data.pub);
3701
+ this._handlePublication(channel, data.pub, id);
3612
3702
  }
3613
3703
  else if (data.message) {
3614
3704
  this._handleMessage(data.message);
3615
3705
  }
3616
3706
  else if (data.join) {
3617
- this._handleJoin(channel, data.join);
3707
+ this._handleJoin(channel, data.join, id);
3618
3708
  }
3619
3709
  else if (data.leave) {
3620
- this._handleLeave(channel, data.leave);
3710
+ this._handleLeave(channel, data.leave, id);
3621
3711
  }
3622
3712
  else if (data.unsubscribe) {
3623
3713
  this._handleUnsubscribe(channel, data.unsubscribe);