diff --git a/src/common/lib/client/realtimechannel.ts b/src/common/lib/client/realtimechannel.ts index 929b1115e..f898d7c7b 100644 --- a/src/common/lib/client/realtimechannel.ts +++ b/src/common/lib/client/realtimechannel.ts @@ -392,18 +392,22 @@ class RealtimeChannel extends EventEmitter { async detach(): Promise { const connectionManager = this.connectionManager; - if (!connectionManager.activeState()) { - throw connectionManager.getError(); - } switch (this.state) { + // RTL5j case 'suspended': this.notifyState('detached'); return; case 'detached': return; + // RTL5b case 'failed': throw new ErrorInfo('Unable to detach; channel state = failed', 90001, 400); default: + // RTL5l: if connection is not connected, immediately transition to detached + if (connectionManager.state.state !== 'connected') { + this.notifyState('detached'); + return; + } this.requestState('detaching'); // eslint-disable-next-line no-fallthrough case 'detaching': diff --git a/test/realtime/channel.test.js b/test/realtime/channel.test.js index 2f3280e20..8f768a794 100644 --- a/test/realtime/channel.test.js +++ b/test/realtime/channel.test.js @@ -1652,6 +1652,31 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, Helper, async }); }); + /** @spec RTL5l */ + it('detaching when connection is not connected immediately transitions channel to detached', async function () { + const helper = this.test.helper; + const realtime = helper.AblyRealtime({ transports: [helper.bestTransport] }); + const channelName = 'detach_when_disconnected'; + const channel = realtime.channels.get(channelName); + + try { + await realtime.connection.once('connected'); + await channel.attach(); + expect(channel.state).to.equal('attached', 'channel should be attached'); + + // Simulate connection becoming disconnected + realtime.connection.connectionManager.requestState({ state: 'disconnected' }); + await realtime.connection.once('disconnected'); + expect(realtime.connection.state).to.equal('disconnected', 'connection should be disconnected'); + + // Detach should succeed immediately without waiting for reconnection + await channel.detach(); + expect(channel.state).to.equal('detached', 'channel should immediately transition to detached'); + } finally { + helper.closeAndFinish(helper.noop, realtime); + } + }); + /** @spec RTL5b */ it('detaching from failed channel results in error', function (done) { const helper = this.test.helper;