diff --git a/ably.d.ts b/ably.d.ts index c326d3e13c..a6a2cf8828 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -1149,6 +1149,10 @@ declare namespace Types { * Indicates whether message continuity on this channel is preserved, see [Nonfatal channel errors](https://ably.com/docs/realtime/channels#nonfatal-errors) for more info. */ resumed: boolean; + /** + * Indicates whether the client can expect a backlog of messages from a rewind or resume. + */ + hasBacklog?: boolean; } /** diff --git a/src/common/lib/client/channelstatechange.ts b/src/common/lib/client/channelstatechange.ts index b778210032..09cf0a2c6f 100644 --- a/src/common/lib/client/channelstatechange.ts +++ b/src/common/lib/client/channelstatechange.ts @@ -5,11 +5,21 @@ class ChannelStateChange { current: string; resumed?: boolean; reason?: string | Error | ErrorInfo; + hasBacklog?: boolean; - constructor(previous: string, current: string, resumed?: boolean, reason?: string | Error | ErrorInfo | null) { + constructor( + previous: string, + current: string, + resumed?: boolean, + hasBacklog?: boolean, + reason?: string | Error | ErrorInfo | null + ) { this.previous = previous; this.current = current; - if (current === 'attached') this.resumed = resumed; + if (current === 'attached') { + this.resumed = resumed; + this.hasBacklog = hasBacklog; + } if (reason) this.reason = reason; } } diff --git a/src/common/lib/client/realtimechannel.ts b/src/common/lib/client/realtimechannel.ts index 0b8e6e8ab5..36ba290e78 100644 --- a/src/common/lib/client/realtimechannel.ts +++ b/src/common/lib/client/realtimechannel.ts @@ -622,12 +622,13 @@ class RealtimeChannel extends Channel { this.modes = (modesFromFlags && Utils.allToLowerCase(modesFromFlags)) || undefined; const resumed = message.hasFlag('RESUMED'); const hasPresence = message.hasFlag('HAS_PRESENCE'); + const hasBacklog = message.hasFlag('HAS_BACKLOG'); if (this.state === 'attached') { if (!resumed) { /* On a loss of continuity, the presence set needs to be re-synced */ this.presence.onAttached(hasPresence); } - const change = new ChannelStateChange(this.state, this.state, resumed, message.error); + const change = new ChannelStateChange(this.state, this.state, resumed, hasBacklog, message.error); this._allChannelChanges.emit('update', change); if (!resumed || this.channelOptions.updateOnAttached) { this.emit('update', change); @@ -636,7 +637,7 @@ class RealtimeChannel extends Channel { /* RTL5i: re-send DETACH and remain in the 'detaching' state */ this.checkPendingState(); } else { - this.notifyState('attached', message.error, resumed, hasPresence); + this.notifyState('attached', message.error, resumed, hasPresence, hasBacklog); } break; } @@ -797,7 +798,8 @@ class RealtimeChannel extends Channel { state: API.Types.ChannelState, reason?: ErrorInfo | null, resumed?: boolean, - hasPresence?: boolean + hasPresence?: boolean, + hasBacklog?: boolean ): void { Logger.logAction( Logger.LOG_MICRO, @@ -823,7 +825,7 @@ class RealtimeChannel extends Channel { if (reason) { this.errorReason = reason; } - const change = new ChannelStateChange(this.state, state, resumed, reason); + const change = new ChannelStateChange(this.state, state, resumed, hasBacklog, reason); const logLevel = state === 'failed' ? Logger.LOG_ERROR : Logger.LOG_MAJOR; Logger.logAction( logLevel, diff --git a/src/common/lib/client/realtimepresence.ts b/src/common/lib/client/realtimepresence.ts index 632bf49182..4cbc4148d5 100644 --- a/src/common/lib/client/realtimepresence.ts +++ b/src/common/lib/client/realtimepresence.ts @@ -477,7 +477,7 @@ class RealtimePresence extends Presence { const msg = 'Presence auto-re-enter failed: ' + err.toString(); const wrappedErr = new ErrorInfo(msg, 91004, 400); Logger.logAction(Logger.LOG_ERROR, 'RealtimePresence._ensureMyMembersPresent()', msg); - const change = new ChannelStateChange(this.channel.state, this.channel.state, true, wrappedErr); + const change = new ChannelStateChange(this.channel.state, this.channel.state, true, false, wrappedErr); this.channel.emit('update', change); } }; diff --git a/test/realtime/channel.test.js b/test/realtime/channel.test.js index 52d593b619..93ecad02db 100644 --- a/test/realtime/channel.test.js +++ b/test/realtime/channel.test.js @@ -1631,5 +1631,59 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async } ); }); + + it('rewind_has_backlog_0', function (done) { + var realtime = helper.AblyRealtime(); + var channelName = 'rewind_has_backlog_0'; + var channelOpts = { params: { rewind: '1' } }; + var channel = realtime.channels.get(channelName, channelOpts); + + // attach with rewind but no channel history - hasBacklog should be false + channel.attach(function (err, stateChange) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + + try { + expect(!stateChange.hasBacklog).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + }); + + it('rewind_has_backlog_1', function (done) { + var realtime = helper.AblyRealtime(); + var rest = helper.AblyRest(); + var channelName = 'rewind_has_backlog_1'; + var channelOpts = { params: { rewind: '1' } }; + var rtChannel = realtime.channels.get(channelName, channelOpts); + var restChannel = rest.channels.get(channelName); + + // attach with rewind after publishing - hasBacklog should be true + restChannel.publish('foo', 'bar', function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + rtChannel.attach(function (err, stateChange) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + + try { + expect(stateChange.hasBacklog).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + }); + }); }); });