diff --git a/src/client.ts b/src/client.ts index d567d20..bb3f5f5 100644 --- a/src/client.ts +++ b/src/client.ts @@ -12,6 +12,7 @@ import { IStompSocket, messageCallbackType, StompSocketState, + TickerStrategy, wsErrorCallbackType, } from './types.js'; import { Versions } from './versions.js'; @@ -100,6 +101,16 @@ export class Client { */ public heartbeatOutgoing: number = 10000; + /** + * Outgoing heartbeat strategy. + * Can be worker or interval strategy, but will always use interval if the client is used in a non-browser environment. + * + * Interval strategy can be helpful if you discover disconnects after moving the browser in the background while the client is connected. + * + * Defaults to interval strategy. + */ + public heartbeatStrategy: TickerStrategy = TickerStrategy.Interval; + /** * This switches on a non-standard behavior while sending WebSocket packets. * It splits larger (text) packets into chunks of [maxWebSocketChunkSize]{@link Client#maxWebSocketChunkSize}. @@ -453,6 +464,7 @@ export class Client { disconnectHeaders: this._disconnectHeaders, heartbeatIncoming: this.heartbeatIncoming, heartbeatOutgoing: this.heartbeatOutgoing, + heartbeatStrategy: this.heartbeatStrategy, splitLargeFrames: this.splitLargeFrames, maxWebSocketChunkSize: this.maxWebSocketChunkSize, forceBinaryWSFrames: this.forceBinaryWSFrames, diff --git a/src/stomp-config.ts b/src/stomp-config.ts index b536fae..463f0da 100644 --- a/src/stomp-config.ts +++ b/src/stomp-config.ts @@ -1,6 +1,7 @@ import { StompHeaders } from './stomp-headers.js'; import { ActivationState, + TickerStrategy, closeEventCallbackType, debugFnType, frameCallbackType, @@ -52,6 +53,11 @@ export class StompConfig { */ public heartbeatOutgoing?: number; + /** + * See [Client#heartbeatStrategy]{@link Client#heartbeatStrategy}. + */ + public heartbeatStrategy?: TickerStrategy; + /** * See [Client#splitLargeFrames]{@link Client#splitLargeFrames}. */ diff --git a/src/stomp-handler.ts b/src/stomp-handler.ts index 198a82f..aedae45 100644 --- a/src/stomp-handler.ts +++ b/src/stomp-handler.ts @@ -1,3 +1,4 @@ +import { augmentWebsocket } from './augment-websocket.js'; import { BYTE } from './byte.js'; import { Client } from './client.js'; import { FrameImpl } from './frame-impl.js'; @@ -6,6 +7,7 @@ import { ITransaction } from './i-transaction.js'; import { Parser } from './parser.js'; import { StompHeaders } from './stomp-headers.js'; import { StompSubscription } from './stomp-subscription.js'; +import { Ticker } from './ticker.js'; import { closeEventCallbackType, debugFnType, @@ -19,7 +21,6 @@ import { wsErrorCallbackType, } from './types.js'; import { Versions } from './versions.js'; -import { augmentWebsocket } from './augment-websocket.js'; /** * The STOMP protocol handler @@ -85,7 +86,7 @@ export class StompHandler { private _partialData: string; private _escapeHeaderValues: boolean; private _counter: number; - private _pinger: any; + private _pinger?: Ticker; private _ponger: any; private _lastServerActivityTS: number; @@ -289,12 +290,14 @@ export class StompHandler { if (this.heartbeatOutgoing !== 0 && serverIncoming !== 0) { const ttl: number = Math.max(this.heartbeatOutgoing, serverIncoming); this.debug(`send PING every ${ttl}ms`); - this._pinger = setInterval(() => { + + this._pinger = new Ticker(ttl, this._client.heartbeatStrategy); + this._pinger.start(() => { if (this._webSocket.readyState === StompSocketState.OPEN) { this._webSocket.send(BYTE.LF); this.debug('>>> PING'); } - }, ttl); + }); } if (this.heartbeatIncoming !== 0 && serverOutgoing !== 0) { @@ -426,7 +429,7 @@ export class StompHandler { this._connected = false; if (this._pinger) { - clearInterval(this._pinger); + this._pinger.stop(); this._pinger = undefined; } if (this._ponger) { diff --git a/src/ticker.ts b/src/ticker.ts new file mode 100644 index 0000000..f02a2ca --- /dev/null +++ b/src/ticker.ts @@ -0,0 +1,71 @@ +import { TickerStrategy } from './types.js'; + +export class Ticker { + private readonly _workerScript = ` + var startTime = Date.now(); + setInterval(function() { + self.postMessage(Date.now() - startTime); + }, ${this._interval}); + `; + + private _worker?: Worker; + private _timer?: NodeJS.Timer; + + constructor( + private readonly _interval: number, + private readonly _strategy = TickerStrategy.Interval) { + } + + public start(tick: (elapsedTime: number) => void): void { + this.stop(); + + if (this.shouldUseWorker()) { + this.runWorker(tick); + } else { + this.runInterval(tick); + } + } + + public stop(): void { + this.disposeWorker(); + this.disposeInterval(); + } + + private shouldUseWorker(): boolean { + return typeof(Worker) !== 'undefined' && this._strategy === TickerStrategy.Worker + } + + private runWorker(tick: (elapsedTime: number) => void): void { + if (!this._worker) { + this._worker = new Worker( + URL.createObjectURL( + new Blob([this._workerScript], { type: 'text/javascript' }) + ) + ); + this._worker.onmessage = (message) => tick(message.data); + } + } + + private runInterval(tick: (elapsedTime: number) => void): void { + if (!this._timer) { + const startTime = Date.now(); + this._timer = setInterval(() => { + tick(Date.now() - startTime); + }, this._interval); + } + } + + private disposeWorker(): void { + if (this._worker) { + this._worker.terminate(); + delete this._worker; + } + } + + private disposeInterval(): void { + if (this._timer) { + clearInterval(this._timer); + delete this._timer; + } + } +} diff --git a/src/types.ts b/src/types.ts index c8ba1f7..b9d7331 100644 --- a/src/types.ts +++ b/src/types.ts @@ -157,6 +157,14 @@ export enum ActivationState { INACTIVE, } +/** + * Possible ticker strategies + */ +export enum TickerStrategy { + Interval = 'interval', + Worker = 'worker' +} + /** * @internal */ @@ -167,6 +175,7 @@ export interface IStomptHandlerConfig { disconnectHeaders: StompHeaders; heartbeatIncoming: number; heartbeatOutgoing: number; + heartbeatStrategy: TickerStrategy; splitLargeFrames: boolean; maxWebSocketChunkSize: number; forceBinaryWSFrames: boolean;