diff --git a/ably.d.ts b/ably.d.ts index cfeb56c63d..5433d5b63f 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -2454,7 +2454,7 @@ declare namespace Types { /** * Enters the presence set for the channel, passing a `data` payload. A `clientId` is required to be present on a channel. * - * @param data - The payload associated with the presence member. + * @param data - The data payload or {@link PresenceMessage} associated with the presence member. * @param callback - A function which will be called upon completion of the operation. If the operation succeeded, then the function will be called with `null`. If it failed, the function will be called with information about the error. */ enter(data?: any, callback?: errorCallback): void; @@ -2467,14 +2467,14 @@ declare namespace Types { /** * Updates the `data` payload for a presence member. If called before entering the presence set, this is treated as an {@link PresenceAction.ENTER} event. * - * @param data - The payload to update for the presence member. + * @param data - The data payload or {@link PresenceMessage} object to update for the presence member. * @param callback - A function which will be called upon completion of the operation. If the operation succeeded, then the function will be called with `null`. If it failed, the function will be called with information about the error. */ update(data?: any, callback?: errorCallback): void; /** * Leaves the presence set for the channel. A client must have previously entered the presence set before they can leave it. * - * @param data - The payload associated with the presence member. + * @param data - The data payload or {@link PresenceMessage} associated with the presence member. * @param callback - A function which will be called upon completion of the operation. If the operation succeeded, then the function will be called with `null`. If it failed, the function will be called with information about the error. */ leave(data?: any, callback?: errorCallback): void; @@ -2488,7 +2488,7 @@ declare namespace Types { * Enters the presence set of the channel for a given `clientId`. Enables a single client to update presence on behalf of any number of clients using a single connection. The library must have been instantiated with an API key or a token bound to a wildcard `clientId`. * * @param clientId - The ID of the client to enter into the presence set. - * @param data - The payload associated with the presence member. + * @param data - The data payload or {@link PresenceMessage} associated with the presence member. * @param callback - A function which will be called upon completion of the operation. If the operation succeeded, then the function will be called with `null`. If it failed, the function will be called with information about the error. */ enterClient(clientId: string, data?: any, callback?: errorCallback): void; @@ -2503,7 +2503,7 @@ declare namespace Types { * Updates the `data` payload for a presence member using a given `clientId`. Enables a single client to update presence on behalf of any number of clients using a single connection. The library must have been instantiated with an API key or a token bound to a wildcard `clientId`. * * @param clientId - The ID of the client to update in the presence set. - * @param data - The payload to update for the presence member. + * @param data - The data payload or {@link PresenceMessage} object to update for the presence member. * @param callback - A function which will be called upon completion of the operation. If the operation succeeded, then the function will be called with `null`. If it failed, the function will be called with information about the error. */ updateClient(clientId: string, data?: any, callback?: errorCallback): void; @@ -2511,7 +2511,7 @@ declare namespace Types { * Leaves the presence set of the channel for a given `clientId`. Enables a single client to update presence on behalf of any number of clients using a single connection. The library must have been instantiated with an API key or a token bound to a wildcard `clientId`. * * @param clientId - The ID of the client to leave the presence set for. - * @param data - The payload associated with the presence member. + * @param data - The data payload or {@link PresenceMessage} associated with the presence member. * @param callback - A function which will be called upon completion of the operation. If the operation succeeded, then the function will be called with `null`. If it failed, the function will be called with information about the error. */ leaveClient(clientId: string, data?: any, callback?: errorCallback): void; @@ -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. */ @@ -3236,6 +3240,14 @@ declare namespace Types { * @param channelOptions - A {@link ChannelOptions} object containing the cipher. */ fromEncodedArray: (JsonArray: any[], channelOptions?: ChannelOptions) => PresenceMessage[]; + + /** + * Initialises a `PresenceMessage` from a `PresenceMessage`-like object. + * + * @param values - The values to intialise the `PresenceMessage` from. + * @param stringifyAction - Whether to convert the `action` field from a number to a string. + */ + fromValues(values: PresenceMessage | Record, stringifyAction?: boolean): PresenceMessage; } /** diff --git a/src/common/lib/client/realtimepresence.ts b/src/common/lib/client/realtimepresence.ts index 4cbc4148d5..8837e07f6f 100644 --- a/src/common/lib/client/realtimepresence.ts +++ b/src/common/lib/client/realtimepresence.ts @@ -153,10 +153,8 @@ class RealtimePresence extends Presence { 'channel = ' + channel.name + ', id = ' + id + ', client = ' + (clientId || '(implicit) ' + getClientId(this)) ); - const presence = PresenceMessage.fromValues({ - action: action, - data: data, - }); + const presence = PresenceMessage.fromData(data); + presence.action = action; if (id) { presence.id = id; } diff --git a/src/common/lib/types/presencemessage.ts b/src/common/lib/types/presencemessage.ts index b258d7e3a9..593172acb2 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; } @@ -155,6 +161,15 @@ class PresenceMessage { }); } + static fromData(data: unknown): PresenceMessage { + if (data instanceof PresenceMessage) { + return data; + } + return PresenceMessage.fromValues({ + data, + }); + } + static getMessagesSize = Message.getMessagesSize; } diff --git a/test/realtime/presence.test.js b/test/realtime/presence.test.js index 359ca588a4..6a7e911902 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.enter( + PresenceMessage.fromValues({ + extras: { headers: { key: 'value' } }, + }) + ); + } + ); + monitorConnection(done, clientRealtime); + }); + /* * Enter presence channel (without attaching), detach, then enter again to reattach */