github centrifugal/centrifuge-js 5.8.0

4 hours ago

What's Changed

This release makes connections and subscriptions reliable under network trouble, process suspension, re-entrant event handlers and server-side changes. There are no API removals. A few behaviours change, listed below; most apps need no code changes.

Highlights:

  • Back/forward cache: when a browser page enters the back/forward cache, the client now closes its connection right away and reconnects, with recovery, when the page is restored (#445). Before, the frozen page kept its WebSocket open: the server saw the user online until the ping timeout (3012 no pong), and kept the presence and subscriptions of a page nobody was looking at.
  • Reconnects: no more stuck connecting state, zero-delay reconnect loops or events in the wrong order after reconnects, disconnect() and connect() from event handlers.
  • Delivery: publications arrive in order, and nothing is delivered after unsubscribe() or disconnect(). Recovery positions are correct for stream, map and shared poll subscriptions.
  • Resume: a suspended laptop or backgrounded tab resumes without false timeouts, "no ping" reconnects or dropped handshakes.
  • Streaming transports: http_stream, SSE and WebTransport handle split protobuf replies, captive portals, failing emulation requests and large replies.
  • Map and shared poll subscriptions: many correctness fixes, including token expiry, delta and signature handling.
  • Memory: a disconnected client no longer stays reachable from window through its network listeners.

Notable changes

Read these four first: the client disconnects when a page enters the back/forward cache (#445); positions over the protobuf protocol (#436) are the only change that can require a code change; connect() now throws on a configuration that never worked (#374); and an exception in an event handler, or while handling a message, now stops the client visibly (#394, #444). Everything else in this section changes behaviour that was wrong before.

  • The client disconnects when the page enters the back/forward cache (#445). On pagehide with persisted set, a connected or connecting client closes its transport and moves to connecting with code 1 and reason page entered back/forward cache; subscriptions move to subscribing. On pageshow with persisted set, it reconnects at once and subscriptions recover as on any reconnect. Normal page loads and unloads (persisted false) are unchanged.
    • What the server sees: the disconnect and the presence leave happen when the user navigates away, instead of after the ping timeout.
    • Your own pagehide handler: a publish() or other call made from it no longer reaches the server when the page enters the cache, because the connection is already closed.
    • Opting out: the new option pageEventTarget (like networkEventTarget) defaults to globalThis where it supports addEventListener – window in browsers, nothing in Node or React Native. Pass a target which never fires these events, such as new EventTarget(), to turn the feature off. A custom target must dispatch the events with persisted: true.
  • Positions and shared poll versions are numbers over the protobuf protocol (#436). The offset of a streamPosition, of a publication context and of the position the library keeps is a number, as the public types declare, and so is the version of a shared poll update. Before, it was a Long object where long.js is available (it comes with protobufjs), and an app that stored such a position got {low, high, unsigned} back, which compares and adds as NaN. An app that worked around this, e.g. with offset.toNumber(), uses the number now. A position stored by an earlier version and passed back through since is converted. Offsets above 2^53 lose exactness, as the type implies.
  • Exception in an event handler. An exception thrown by an app event handler of the client or of a subscription now stops the client visibly, whatever caused the event: a received message, a call such as connect() or subscribe(), a timer or a teardown (#394, #444).
    • What happens: the client disconnects without reconnecting, with code 2 (badProtocol) and reason exception in event handler: <message>. The exception is reported as an unhandled rejection, and connect() works again afterwards. The library's own work completes as if the handler had returned, so the client is left consistent.
    • Publications: a publication of a stream or server-side subscription whose handler threw is not counted as delivered, so a recovery after connect() delivers it again.
    • Other exceptions while handling a message stop the client the same way, with reason exception during message handling: <message> (#394).
    • Before: an exception while handling a received message silently stopped processing while state stayed connected; on a timeout, a teardown or an error event it was only reported and the client kept reconnecting and retrying (#420, #426, #431); from connect() it was thrown to the caller. Throwing from event handlers is still unsupported: catch errors in handlers.
  • Undecodable data closes the transport, and the client reconnects with the usual backoff and transport fallback (#396, #421). Examples are a captive portal page, or a proxy answering an http_stream request. Before, every line threw from the transport callback, which ends a Node process without an uncaughtException handler.
  • Emulation requests (http_stream, SSE):
    • Transport close: closing the transport aborts pending emulation requests (#397). A network failure or a 404/5xx response now closes the transport, so the client reconnects at once instead of waiting for the command timeout (#393, #397).
    • Other statuses: statuses such as 413 still reject only that command.
  • Configuration errors are thrown from connect() (#374). Examples are no supported transport, no WebSocket implementation, or an http(s) endpoint given as a string. Before, the client stayed connecting, or retried forever reporting token or data errors.
  • subscribe() on a removed subscription throws (#424), as newSubscription() does for a channel that already has one. Before, it created a server subscription the client didn't track, so its publications were dropped.
  • removeSubscription() removes the subscription before its unsubscribed event (#427). A handler of that event can create a new subscription to the same channel. Calling removeSubscription() again with an old object no longer removes a newer subscription of that channel (#424).
  • State invalidation without getToken keeps the token. This applies to disconnect code 3014 and unsubscribe code 2502 (#401, #403). A client or subscription with a static token (or none) reconnects or resubscribes with it, instead of stopping on a configuration error or resubscribing without a token.
  • Subscription token expired without getToken (#443). A subscription with a static token that gets 109 now ends unsubscribed with code 1 (unauthorized) and a configuration error, like the connection does. Before, it cleared the token and subscribed again without one.
  • disconnect() fails calls made while disconnected (#443), as it already did for calls made while connecting. Before, they waited for a connection until their timeout.
  • subscribe() on a subscription already subscribing keeps its backoff (#443). Only a subscribe from unsubscribed resets it.
  • setToken() after a token expired error. Without getToken, setToken() now clears the pending token refresh, so the next connect() uses the new token (#422, #426). With getToken, the next attempt still calls it.
  • Overdue timers wait for data already received. This covers call timeouts, the no-ping timer and the connect timeout (#398, #417, #423, #426). A timer that runs late, e.g. after a laptop sleep, waits one more event loop turn, or 1 s when it ran at least 1 s late, so a reply, ping or open event already received is processed first.
  • Server-side subscription pushes go to the server-side subscription while a client-side subscription object of the same channel isn't subscribed (#425). Before, they were dropped.
  • Fewer duplicate error events. A transport failing to initialize, or a refused SSE/http_stream handshake, is reported as one transport error, without an extra connect error (#426, #432).
  • Subscription state events stay in order. subscribing and unsubscribed aren't emitted when a state handler has already changed the state again (#431, #433).
  • Shared poll:
    • Tracking during the subscribe: track() calls made while a subscribe completes, e.g. from a subscribed handler, are sent with the replay that follows (#430).
    • Stored signatures: a new signature replaces stored ones whose keys it fully covers (#433).
    • Duplicate versions: an update with a version not newer than the one held for the key is dropped. Before, a replay and its retry could deliver the same version twice, and an older version could replace a newer one (#443).
    • Permanently failed track: keys of a track failing with a permanent error (e.g. limit exceeded) are untracked, with a removal update each. Before, only an error was emitted, and the keys stayed tracked by the client and were replayed on every reconnect (#443).
  • Reconnect backoff. The zero-delay first round over a transport list no longer counts towards the backoff (#432).
  • A channel the server doesn't recover no longer reports a failed recovery (#436). With positioning enabled and recovery disabled, a subscription given a position by since or getState kept sending recover: true on every resubscribe, so the app was told wasRecovering: true, recovered: false each time although nothing was lost. recovered stays false, so apps that reload their state on it are unaffected.
  • Stream transports. A close of http_stream, SSE or WebTransport is handled after data already received is dispatched, so a disconnect push the server writes just before closing is applied (#429). WebSocket and SockJS are unchanged.

Pull requests

  • Close the connection when the page enters the back/forward cache by @FZambia in #445
  • Catch publish errors from nested debounce timer by @FZambia in #388
  • Remove network event listeners when client becomes disconnected by @FZambia in #390
  • Fix reconnect races, Safari online workaround and message order after reconnect by @FZambia in #391
  • Drop outdated pushes, keep dispatching after errors, fix map delta removals by @FZambia in #392
  • Stop recovered publications after teardown, close transport on failed emulation request by @FZambia in #393
  • Stop the client on an exception during message handling by @FZambia in #394
  • Wait for more data when a protobuf stream read ends inside a reply by @FZambia in #395
  • Close the transport on data that cannot be decoded by @FZambia in #396
  • Abort pending emulation requests on close, close the transport on error responses by @FZambia in #397
  • Process received data before failing an overdue call timeout by @FZambia in #398
  • Get a subscription token before recovering a map subscription by @FZambia in #399
  • Reconnect with the current token after state invalidation without getToken by @FZambia in #401
  • Do not close a new connection attempt on an outdated unsubscribe rejection by @FZambia in #402
  • Keep a subscription token after state invalidation without getToken by @FZambia in #403
  • Stop a map subscribe flow after unsubscribe from an event handler by @FZambia in #404
  • Handle a transport failing to initialize as a closed transport by @FZambia in #405
  • Validate transports in connect(), fail a hanging attempt without its transport by @FZambia in #374
  • Keep a map subscription's position before what the app didn't receive by @FZambia in #406
  • Back off after a connect error returned over an emulation transport by @FZambia in #407
  • Back off after a disconnect pushed during an emulation connect by @FZambia in #408
  • Keep the reconnect backoff growing while an emulation endpoint fails by @FZambia in #409
  • Complete a subscribe consistently when a handler unsubscribes or resets it by @FZambia in #410
  • Ignore replies of an earlier map subscribe flow, page without moving the position by @FZambia in #411
  • Let a ping waiting in the socket keep the connection on resume by @FZambia in #423
  • Wait for the unsubscribe reply in every resubscribe, keep removal to its own subscription by @FZambia in #424
  • Recover after a publication a handler resubscribed from, and other subscription state fixes by @FZambia in #425
  • Keep a handshake that completed while suspended, fall back from a silent socket, and other lifecycle fixes by @FZambia in #426
  • Remove a subscription before its unsubscribed event, keep earlier pending unsubscribes by @FZambia in #427
  • Keep shared poll versions of undelivered updates, drop outdated subscription token refreshes by @FZambia in #428
  • Send the connect command despite a throwing callback, apply a disconnect push read with a stream end, and other lifecycle fixes by @FZambia in #429
  • Ignore shared poll signatures obtained for an earlier subscription, track once from subscribe handlers by @FZambia in #430
  • Keep subscription events in order, stop refresh retries a handler ended, survive callbacks throwing by @FZambia in #431
  • Keep 5.7.4 fallback semantics for an opened socket, and other lifecycle fixes by @FZambia in #432
  • Refresh an expired shared poll signature, replay without covered signatures, and other subscription fixes by @FZambia in #433
  • Recover server-side subscriptions with the epoch of the first publication by @FZambia in #434
  • Keep the client id of a connection across its token refreshes by @FZambia in #435
  • Take the position of a reply as a number, stop asking to recover a channel the server does not recover by @FZambia in #436
  • Fix subscription state races, retry backoff and emulation ordering by @FZambia in #443
  • Stop the client when an event handler throws by @FZambia in #444
  • Bump handlebars from 4.7.9 to 4.7.10 by @dependabot[bot] in #446
  • Bump undici from 6.28.0 to 6.28.1 by @dependabot[bot] in #447
  • Update dev dependencies by @FZambia in #448

Full Changelog: 5.7.4...5.8.0

Don't miss a new centrifuge-js release

NewReleases is sending notifications on new releases.