From 2b00cab3306c06bd6d13b5ce93afe9c8a615574f Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Thu, 16 Jan 2025 14:10:05 +0200 Subject: [PATCH 1/5] up CI to use Centrifugo v6 (#301) --- docker-compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3f78bf2c..1d1e5a40 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,15 +2,15 @@ version: '3.8' services: centrifugo: - image: centrifugo/centrifugo:v5.4.0 + image: centrifugo/centrifugo:v6.0.0 command: - centrifugo ports: - "8000:8000" environment: - CENTRIFUGO_CLIENT_INSECURE=true - - CENTRIFUGO_HTTP_STREAM=true - - CENTRIFUGO_SSE=true - - CENTRIFUGO_PRESENCE=true + - CENTRIFUGO_HTTP_STREAM_ENABLED=true + - CENTRIFUGO_SSE_ENABLED=true + - CENTRIFUGO_CHANNEL_WITHOUT_NAMESPACE_PRESENCE=true - CENTRIFUGO_CLIENT_CONCURRENCY=8 - CENTRIFUGO_LOG_LEVEL=debug From 032ef7210146968e0fb51e9bb45db77586128115 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Tue, 21 Jan 2025 15:20:58 +0200 Subject: [PATCH 2/5] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d653759..c8614f6f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This SDK provides a client to connect to [Centrifugo](https://github.com/centrif The features implemented by this SDK can be found in [SDK feature matrix](https://centrifugal.dev/docs/transports/client_sdk#sdk-feature-matrix). -> `centrifuge-js` v5.x is compatible with [Centrifugo](https://github.com/centrifugal/centrifugo) server v4 and v5 and [Centrifuge](https://github.com/centrifugal/centrifuge) >= 0.25.0. For Centrifugo v2, Centrifugo v3 and Centrifuge < 0.25.0 you should use `centrifuge-js` v2.x. +> `centrifuge-js` v5.x is compatible with [Centrifugo](https://github.com/centrifugal/centrifugo) server v6, v5 and v4, and [Centrifuge](https://github.com/centrifugal/centrifuge) >= 0.25.0. For Centrifugo v2, Centrifugo v3 and Centrifuge < 0.25.0 you should use `centrifuge-js` v2.x. * [Install](#install) * [Quick start](#quick-start) From 08c3551047e9fcbc5de715ad9ad81de4b8f1e3d6 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Thu, 23 Jan 2025 21:22:02 +0200 Subject: [PATCH 3/5] Fix race causing duplicate subscribe requests (#303) --- docker-compose.yml | 5 +- src/centrifuge.test.ts | 127 +++++++++++++++++++++++++++++++++++++++-- src/subscription.ts | 7 ++- 3 files changed, 129 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1d1e5a40..76cdcc5e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: centrifugo: - image: centrifugo/centrifugo:v6.0.0 + image: centrifugo/centrifugo:v6 command: - centrifugo ports: @@ -13,4 +13,5 @@ services: - CENTRIFUGO_SSE_ENABLED=true - CENTRIFUGO_CHANNEL_WITHOUT_NAMESPACE_PRESENCE=true - CENTRIFUGO_CLIENT_CONCURRENCY=8 - - CENTRIFUGO_LOG_LEVEL=debug + - CENTRIFUGO_LOG_LEVEL=trace + - CENTRIFUGO_CLIENT_TOKEN_HMAC_SECRET_KEY=secret diff --git a/src/centrifuge.test.ts b/src/centrifuge.test.ts index 8b81a9c2..fcc5fac9 100644 --- a/src/centrifuge.test.ts +++ b/src/centrifuge.test.ts @@ -368,16 +368,40 @@ test.each(transportCases)("%s: subscribe and unsubscribe loop", async (transport sub.subscribe() const presenceStats = await sub.presenceStats(); - expect(presenceStats.numClients).toBe(1) + expect(presenceStats.numClients).toBe(1); expect(presenceStats.numUsers).toBe(1); const presence = await sub.presence(); expect(Object.keys(presence.clients).length).toBe(1) await sub.unsubscribe() - const presenceStats2 = await c.presenceStats('test'); - expect(presenceStats2.numClients).toBe(0) + + const retryWithDelay = async (fn, validate, maxRetries, delay) => { + for (let i = 0; i < maxRetries; i++) { + const result = await fn(); + if (validate(result)) { + return result; + } + await new Promise(resolve => setTimeout(resolve, delay)); + } + throw new Error("Validation failed after retries"); + }; + + const presenceStats2 = await retryWithDelay( + () => c.presenceStats('test'), + (stats: any) => stats.numClients === 0 && stats.numUsers === 0, + 3, + 2000 + ); + + const presence2 = await retryWithDelay( + () => c.presence('test'), + (presence: any) => Object.keys(presence.clients).length === 0, + 3, + 2000 + ); + + expect(presenceStats2.numClients).toBe(0); expect(presenceStats2.numUsers).toBe(0); - const presence2 = await c.presence('test'); - expect(Object.keys(presence2.clients).length).toBe(0) + expect(Object.keys(presence2.clients).length).toBe(0); let disconnectCalled: any; const disconnectedPromise = new Promise((resolve, _) => { @@ -552,3 +576,96 @@ test.each(websocketOnly)("%s: reconnect after close before transport open", asyn await disconnectedPromise; expect(c.state).toBe(State.Disconnected); }); + +test.each(transportCases)("%s: subscribes and unsubscribes from many subs", async (transport, endpoint) => { + const c = new Centrifuge([{ + transport: transport as TransportName, + endpoint: endpoint, + }], { + websocket: WebSocket, + fetch: fetch, + eventsource: EventSource, + readableStream: ReadableStream, + emulationEndpoint: 'http://localhost:8000/emulation', + // debug: true + }); + // Keep an array of promises so that we can wait for each subscription's 'unsubscribed' event. + const unsubscribedPromises: Promise[] = []; + + const channels = [ + 'test1', + 'test2', + 'test3', + 'test4', + 'test5', + ]; + + // Subscription tokens for anonymous users without ttl. Using an HMAC secret key used in tests ("secret"). + const testTokens = { + 'test1': "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3Mzc1MzIzNDgsImNoYW5uZWwiOiJ0ZXN0MSJ9.eqPQxbBtyYxL8Hvbkm-P6aH7chUsSG_EMWe-rTwF_HI", + 'test2': "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3Mzc1MzIzODcsImNoYW5uZWwiOiJ0ZXN0MiJ9.tTJB3uSa8XpEmCvfkmrSKclijofnJ5RkQk6L2SaGtUE", + 'test3': "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3Mzc1MzIzOTgsImNoYW5uZWwiOiJ0ZXN0MyJ9.nyLcMrIot441CszOKska7kQIjo2sEm8pSxV1XWfNCsI", + 'test4': "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3Mzc1MzI0MDksImNoYW5uZWwiOiJ0ZXN0NCJ9.wWAX2AhJX6Ep4HVexQWSVF3-cWytVhzY9Pm7QsMdCsI", + 'test5': "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3Mzc1MzI0MTgsImNoYW5uZWwiOiJ0ZXN0NSJ9.hCSfpHYws5TXLKkN0bW0DU6C-wgEUNuhGaIy8W1sT9o" + } + + c.connect(); + + const subscriptions: any[] = []; + + for (const channel of channels) { + const sub = c.newSubscription(channel, { + getToken: async function () { + // Sleep for a random time between 0 and 100 milliseconds to emulate network. + const sleep = (ms: any) => new Promise(resolve => setTimeout(resolve, ms)); + await sleep(Math.random() * 100); + return testTokens[channel]; + } + }); + + // Create a promise for the 'unsubscribed' event of this subscription. + const unsubPromise = new Promise((resolve) => { + sub.on("unsubscribed", (ctx) => { + resolve(ctx); + }); + }); + unsubscribedPromises.push(unsubPromise); + + // Actually subscribe. + sub.subscribe(); + subscriptions.push(sub); + } + + // Wait until all subscriptions are in the Subscribed state. + await Promise.all( + subscriptions.map(async (sub) => { + await sub.ready(5000); + expect(sub.state).toBe(SubscriptionState.Subscribed); + }) + ); + + // The client itself should be connected now. + expect(c.state).toBe(State.Connected); + + // Unsubscribe from all and then disconnect. + subscriptions.forEach((sub) => { + sub.unsubscribe(); + }); + c.disconnect(); + + // Wait until all 'unsubscribed' events are received. + const unsubscribedContexts = await Promise.all(unsubscribedPromises); + + // Confirm each subscription is now Unsubscribed. + subscriptions.forEach((sub) => { + expect(sub.state).toBe(SubscriptionState.Unsubscribed); + }); + + // The client should be disconnected. + expect(c.state).toBe(State.Disconnected); + + // Assert the correct unsubscribe code for each subscription. + unsubscribedContexts.forEach((ctx) => { + expect(ctx.code).toBe(unsubscribedCodes.unsubscribeCalled); + }); +}); diff --git a/src/subscription.ts b/src/subscription.ts index c829d45c..c59ba11a 100644 --- a/src/subscription.ts +++ b/src/subscription.ts @@ -355,9 +355,11 @@ export class Subscription extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter { this._inflight = false; @@ -464,6 +464,7 @@ export class Subscription extends (EventEmitter as new () => TypedEventEmitter Date: Fri, 24 Jan 2025 13:11:44 +0200 Subject: [PATCH 4/5] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c8614f6f..693afd3b 100644 --- a/README.md +++ b/README.md @@ -131,14 +131,14 @@ Supported transports are: * `websocket` * `http_stream` * `sse` -* `sockjs` - SockJS can also be used as a fallback, SockJS is currently in DEPRECATED status in Centrifugal ecosystem. Also, sticky sessions must be used on the backend in distributed case with it. See more details below +* `sockjs` - SockJS can also be used as a fallback in Centrifugo < v6, in Centrifugo v6 SockJS was removed and will be removed in `centrifuge-js` v6 too. Also, sticky sessions must be used on the backend in distributed case with it. See more details below * `webtransport` - this SDK also supports WebTransport in experimental form. See details below If you want to use sticky sessions on a load balancer level as an optimimization for Centrifugal bidirectional emulation layer keep in mind that we currently use `same-origin` credentials policy for emulation requests in `http_stream` and `sse` transport cases. So cookies will only be passed in same-origin case. Please open an issue in case you need to configure more relaxed credentials. Though in most cases stickyness based on client's IP may be sufficient enough. ### Using SockJS -**SockJS usage is DEPRECATED in the Centrifugal ecosystem** +**SockJS usage is DEPRECATED**. Its support was removed in Centrifugo v6, and it will also be removed from this SDK in v6 release. If you want to use SockJS you must also import SockJS client before centrifuge.js From e9fb984e0f8531c093ae9f01897ee13d6f05f936 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sat, 25 Jan 2025 11:57:13 +0200 Subject: [PATCH 5/5] prepare 5.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bfa4c4b3..9a127074 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "centrifuge", - "version": "5.3.0", + "version": "5.3.1", "description": "JavaScript client SDK for bidirectional communication with Centrifugo and Centrifuge-based server from browser, NodeJS and React Native", "main": "build/index.js", "types": "build/index.d.ts",