diff --git a/ably.d.ts b/ably.d.ts index cfeb56c63d..ab17fa9737 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -3208,6 +3208,10 @@ declare namespace Types { * This will typically be empty as all presence messages received from Ably are automatically decoded client-side using this value. However, if the message encoding cannot be processed, this attribute will contain the remaining transformations not applied to the data payload. */ encoding: string; + /** + * A JSON object of arbitrary key-value pairs that may contain metadata, and/or ancillary payloads. Valid payloads include `headers`. + */ + extras: any; /** * A unique ID assigned to each `PresenceMessage` by Ably. */ diff --git a/src/common/lib/client/realtimepresence.ts b/src/common/lib/client/realtimepresence.ts index 4cbc4148d5..ff2f454626 100644 --- a/src/common/lib/client/realtimepresence.ts +++ b/src/common/lib/client/realtimepresence.ts @@ -104,41 +104,56 @@ class RealtimePresence extends Presence { if (isAnonymousOrWildcard(this)) { throw new ErrorInfo('clientId must be specified to enter a presence channel', 40012, 400); } - return this._enterOrUpdateClient(undefined, undefined, data, 'enter', callback); + if (typeof data === 'function') { + callback = data as ErrCallback; + data = null; + } + return this.enterMessage(PresenceMessage.fromValues({ data }), callback); } update(data: unknown, callback: ErrCallback): void | Promise { if (isAnonymousOrWildcard(this)) { throw new ErrorInfo('clientId must be specified to update presence data', 40012, 400); } - return this._enterOrUpdateClient(undefined, undefined, data, 'update', callback); + if (typeof data === 'function') { + callback = data as ErrCallback; + data = null; + } + return this.updateMessage(PresenceMessage.fromValues({ data }), callback); } enterClient(clientId: string, data: unknown, callback: ErrCallback): void | Promise { - return this._enterOrUpdateClient(undefined, clientId, data, 'enter', callback); + if (typeof data === 'function') { + callback = data as ErrCallback; + data = null; + } + return this.enterMessage(PresenceMessage.fromValues({ clientId, data }), callback); } updateClient(clientId: string, data: unknown, callback: ErrCallback): void | Promise { - return this._enterOrUpdateClient(undefined, clientId, data, 'update', callback); + if (typeof data === 'function') { + callback = data as ErrCallback; + data = null; + } + return this.updateMessage(PresenceMessage.fromValues({ clientId, data }), callback); } - _enterOrUpdateClient( - id: string | undefined, - clientId: string | undefined, - data: unknown, - action: string, - callback: ErrCallback - ): void | Promise { + enterMessage(msg: PresenceMessage, callback: ErrCallback): void | Promise { + msg.action = 'enter'; + return this._enterOrUpdateClient(msg, callback); + } + + updateMessage(msg: PresenceMessage, callback: ErrCallback): void | Promise { + msg.action = 'update'; + return this._enterOrUpdateClient(msg, callback); + } + + _enterOrUpdateClient(presence: PresenceMessage, callback: ErrCallback): void | Promise { if (!callback) { - if (typeof data === 'function') { - callback = data as ErrCallback; - data = null; - } else { - if (this.channel.realtime.options.promises) { - return Utils.promisify(this, '_enterOrUpdateClient', [id, clientId, data, action]); - } - callback = noop; + if (this.channel.realtime.options.promises) { + return Utils.promisify(this, '_enterOrUpdateClient', [presence]); } + callback = noop; } const channel = this.channel; @@ -149,21 +164,15 @@ class RealtimePresence extends Presence { Logger.logAction( Logger.LOG_MICRO, - 'RealtimePresence.' + action + 'Client()', - 'channel = ' + channel.name + ', id = ' + id + ', client = ' + (clientId || '(implicit) ' + getClientId(this)) + 'RealtimePresence.' + presence.action + 'Client()', + 'channel = ' + + channel.name + + ', id = ' + + presence.id + + ', client = ' + + (presence.clientId || '(implicit) ' + getClientId(this)) ); - const presence = PresenceMessage.fromValues({ - action: action, - data: data, - }); - if (id) { - presence.id = id; - } - if (clientId) { - presence.clientId = clientId; - } - PresenceMessage.encode(presence, channel.channelOptions as CipherOptions, (err: IPartialErrorInfo) => { if (err) { callback(err); @@ -185,7 +194,7 @@ class RealtimePresence extends Presence { break; default: err = new PartialErrorInfo( - 'Unable to ' + action + ' presence channel while in ' + channel.state + ' state', + 'Unable to ' + presence.action + ' presence channel while in ' + channel.state + ' state', 90001 ); err.code = 90001; @@ -491,7 +500,13 @@ class RealtimePresence extends Presence { ); // RTP17g: Send ENTER containing the member id, clientId and data // attributes. - this._enterOrUpdateClient(entry.id, entry.clientId, entry.data, 'enter', reenterCb); + const msg = PresenceMessage.fromValues({ + id: entry.id, + action: 'enter', + clientId: entry.clientId, + data: entry.data, + }); + this._enterOrUpdateClient(msg, reenterCb); } } diff --git a/src/common/lib/types/presencemessage.ts b/src/common/lib/types/presencemessage.ts index b258d7e3a9..b0799bab01 100644 --- a/src/common/lib/types/presencemessage.ts +++ b/src/common/lib/types/presencemessage.ts @@ -16,6 +16,7 @@ class PresenceMessage { connectionId?: string; data?: string | Buffer | Uint8Array; encoding?: string; + extras?: any; size?: number; static Actions = ['absent', 'present', 'enter', 'leave', 'update']; @@ -53,6 +54,7 @@ class PresenceMessage { action: number; data: string | Buffer | Uint8Array; encoding?: string; + extras?: any; } { /* encode data to base64 if present and we're returning real JSON; * although msgpack calls toJSON(), we know it is a stringify() @@ -78,6 +80,7 @@ class PresenceMessage { action: toActionValue(this.action as string), data: data, encoding: encoding, + extras: this.extras, }; } @@ -95,6 +98,9 @@ class PresenceMessage { result += '; data (buffer)=' + Platform.BufferUtils.base64Encode(this.data); else result += '; data (json)=' + JSON.stringify(this.data); } + if (this.extras) { + result += '; extras=' + JSON.stringify(this.extras); + } result += ']'; return result; } diff --git a/test/realtime/presence.test.js b/test/realtime/presence.test.js index 359ca588a4..4aa4050b9d 100644 --- a/test/realtime/presence.test.js +++ b/test/realtime/presence.test.js @@ -6,6 +6,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async var createPM = Ably.Realtime.ProtocolMessage.fromDeserialized; var closeAndFinish = helper.closeAndFinish; var monitorConnection = helper.monitorConnection; + var PresenceMessage = Ably.Realtime.PresenceMessage; function extractClientIds(presenceSet) { return utils @@ -370,6 +371,43 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async monitorConnection(done, clientRealtime); }); + /* + * Attach to channel, enter presence channel with extras and check received + * PresenceMessage has extras. + */ + it('presenceMessageExtras', function (done) { + var clientRealtime = helper.AblyRealtime({ clientId: testClientId, tokenDetails: authToken }); + var channelName = 'presenceMessageExtras'; + var clientChannel = clientRealtime.channels.get(channelName); + var presence = clientChannel.presence; + presence.subscribe( + function (presenceMessage) { + try { + expect(presenceMessage.extras).to.deep.equal( + { headers: { key: 'value' } }, + 'extras should have headers "key=value"' + ); + } catch (err) { + closeAndFinish(done, clientRealtime, err); + return; + } + closeAndFinish(done, clientRealtime); + }, + function onPresenceSubscribe(err) { + if (err) { + closeAndFinish(done, clientRealtime, err); + return; + } + clientChannel.presence.enterMessage( + PresenceMessage.fromValues({ + extras: { headers: { key: 'value' } }, + }) + ); + } + ); + monitorConnection(done, clientRealtime); + }); + /* * Enter presence channel (without attaching), detach, then enter again to reattach */