diff --git a/ably.d.ts b/ably.d.ts index cbe1df3dd..228f90b78 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -2332,6 +2332,7 @@ export interface Message { name?: string; /** * Timestamp of when the message was received by Ably, as milliseconds since the Unix epoch. + * (This is the timestamp of the current version of the message) */ timestamp?: number; /** @@ -2339,27 +2340,32 @@ export interface Message { */ action?: MessageAction; /** - * This message's unique serial. + * This message's unique serial (an identifier that will be the same in all future + * updates of this message). */ serial?: string; /** - * The serial of the message that this message is a reference to. + * If this message references another, the serial of that message. */ refSerial?: string; /** - * The type of reference this message is, in relation to the message it references. + * If this message references another, the type of reference that is. */ refType?: string; /** - * If an `update` operation was applied to this message, this will be the timestamp the update occurred. + * The timestamp of the very first version of a given message (will differ from + * createdAt only if the message has been updated or deleted). */ - updatedAt?: number; + createdAt?: number; /** - * The serial of the operation that updated this message. + * The version of the message, lexicographically-comparable with other versions (that + * share the same serial) Will differ from the serial only if the message has been + * updated or deleted. */ - updateSerial?: string; + version?: string; /** - * If this message resulted from an operation, this will contain the operation details. + * In the case of an updated or deleted message, this will contain metadata about the + * update or delete operation. */ operation?: Operation; } @@ -2516,9 +2522,8 @@ export interface PresenceMessageStatic { * 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; + fromValues(values: Partial>): PresenceMessage; } /** diff --git a/modular.d.ts b/modular.d.ts index 6dac14d39..94f0ba27f 100644 --- a/modular.d.ts +++ b/modular.d.ts @@ -16,6 +16,8 @@ * | `MessageStatic.fromEncodedArray()` | [`decodeMessages()`](../functions/modular.decodeMessages.html) | * | `MessageStatic.fromEncodedArray()` | [`decodeEncryptedMessages()`](../functions/modular.decodeEncryptedMessages.html) | * | `PresenceMessageStatic.fromEncoded()` | [`decodePresenceMessage()`](../functions/modular.decodePresenceMessage.html) | + * | `PresenceMessageStatic.fromEncoded()` | [`decodeEncryptedPresenceMessage()`](../functions/modular.decodeEncryptedPresenceMessage.html) | + * | `PresenceMessageStatic.fromEncodedArray()` | [`decodeEncryptedPresenceMessages()`](../functions/modular.decodeEncryptedPresenceMessages.html) | * | `PresenceMessageStatic.fromEncodedArray()` | [`decodePresenceMessages()`](../functions/modular.decodePresenceMessages.html) | * | `PresenceMessageStatic.fromValues()` | [`constructPresenceMessage()`](../functions/modular.constructPresenceMessage.html) | * @@ -60,6 +62,8 @@ export declare const decodeMessages: MessageStatic['fromEncodedArray']; export declare const decodeEncryptedMessages: MessageStatic['fromEncodedArray']; export declare const decodePresenceMessage: PresenceMessageStatic['fromEncoded']; export declare const decodePresenceMessages: PresenceMessageStatic['fromEncodedArray']; +export declare const decodeEncryptedPresenceMessage: PresenceMessageStatic['fromEncoded']; +export declare const decodeEncryptedPresenceMessages: PresenceMessageStatic['fromEncodedArray']; export declare const constructPresenceMessage: PresenceMessageStatic['fromValues']; /** diff --git a/scripts/moduleReport.ts b/scripts/moduleReport.ts index aacec5a00..8117950a4 100644 --- a/scripts/moduleReport.ts +++ b/scripts/moduleReport.ts @@ -34,6 +34,8 @@ const functions = [ { name: 'decodeEncryptedMessages', transitiveImports: ['Crypto'] }, { name: 'decodePresenceMessage', transitiveImports: [] }, { name: 'decodePresenceMessages', transitiveImports: [] }, + { name: 'decodeEncryptedPresenceMessage', transitiveImports: ['Crypto'] }, + { name: 'decodeEncryptedPresenceMessages', transitiveImports: ['Crypto'] }, { name: 'constructPresenceMessage', transitiveImports: [] }, ]; diff --git a/src/common/lib/client/defaultrealtime.ts b/src/common/lib/client/defaultrealtime.ts index 77854711a..da67d5d4a 100644 --- a/src/common/lib/client/defaultrealtime.ts +++ b/src/common/lib/client/defaultrealtime.ts @@ -15,6 +15,7 @@ import { PresenceMap } from './presencemap'; import { fromValues as presenceMessageFromValues, fromValuesArray as presenceMessagesFromValuesArray, + fromWireProtocol as presenceMessageFromWireProtocol, } from '../types/presencemessage'; import { Http } from 'common/types/http'; import Defaults from '../util/defaults'; @@ -40,6 +41,7 @@ export class DefaultRealtime extends BaseRealtime { RealtimePresence, presenceMessageFromValues, presenceMessagesFromValuesArray, + presenceMessageFromWireProtocol, }, WebSocketTransport, MessageInteractions: FilteredSubscriptions, diff --git a/src/common/lib/client/modularplugins.ts b/src/common/lib/client/modularplugins.ts index 7bb05e30b..c3bdf1ea0 100644 --- a/src/common/lib/client/modularplugins.ts +++ b/src/common/lib/client/modularplugins.ts @@ -8,6 +8,7 @@ import { FilteredSubscriptions } from './filteredsubscriptions'; import { fromValues as presenceMessageFromValues, fromValuesArray as presenceMessagesFromValuesArray, + fromWireProtocol as presenceMessageFromWireProtocol, } from '../types/presencemessage'; import { TransportCtor } from '../transport/transport'; import * as PushPlugin from 'plugins/push'; @@ -15,6 +16,7 @@ import * as PushPlugin from 'plugins/push'; export interface PresenceMessagePlugin { presenceMessageFromValues: typeof presenceMessageFromValues; presenceMessagesFromValuesArray: typeof presenceMessagesFromValuesArray; + presenceMessageFromWireProtocol: typeof presenceMessageFromWireProtocol; } export type RealtimePresencePlugin = PresenceMessagePlugin & { diff --git a/src/common/lib/client/realtimechannel.ts b/src/common/lib/client/realtimechannel.ts index 4929295ec..89e807033 100644 --- a/src/common/lib/client/realtimechannel.ts +++ b/src/common/lib/client/realtimechannel.ts @@ -603,7 +603,8 @@ class RealtimeChannel extends EventEmitter { const messages = message.messages as Array, firstMessage = messages[0], - lastMessage = messages[messages.length - 1]; + lastMessage = messages[messages.length - 1], + channelSerial = message.channelSerial; if ( firstMessage.extras && @@ -652,6 +653,16 @@ class RealtimeChannel extends EventEmitter { return; } + for (let i = 0; i < messages.length; i++) { + const msg = messages[i]; + if (channelSerial && !msg.version) { + msg.version = channelSerial + ':' + i.toString().padStart(3, '0'); + // already done in fromWireProtocol -- but for realtime messages the source + // fields might be copied from the protocolmessage, so need to do it again + msg.expandFields(); + } + } + this._lastPayload.messageId = lastMessage.id; this._lastPayload.protocolMessageChannelSerial = message.channelSerial; this.onEvent(messages); @@ -720,7 +731,7 @@ class RealtimeChannel extends EventEmitter { if (!msg.connectionId) msg.connectionId = connectionId; if (!msg.timestamp) msg.timestamp = timestamp; - if (!msg.id) msg.id = id + ':' + i; + if (id && !msg.id) msg.id = id + ':' + i; } return { unrecoverableError: false }; diff --git a/src/common/lib/client/restchannelmixin.ts b/src/common/lib/client/restchannelmixin.ts index c104d66be..67fd701f3 100644 --- a/src/common/lib/client/restchannelmixin.ts +++ b/src/common/lib/client/restchannelmixin.ts @@ -2,7 +2,7 @@ import * as API from '../../../../ably'; import RestChannel from './restchannel'; import RealtimeChannel from './realtimechannel'; import * as Utils from '../util/utils'; -import Message, { fromResponseBody as messageFromResponseBody } from '../types/message'; +import Message, { WireProtocolMessage, _fromEncodedArray } from '../types/message'; import Defaults from '../util/defaults'; import PaginatedResource, { PaginatedResult } from './paginatedresource'; import Resource from './resource'; @@ -30,19 +30,16 @@ export class RestChannelMixin { Utils.mixin(headers, client.options.headers); - const options = channel.channelOptions; return new PaginatedResource(client, this.basePath(channel) + '/messages', headers, envelope, async function ( body, headers, unpacked, ) { - return await messageFromResponseBody( - body as Message[], - options, - channel.logger, - client._MsgPack, - unpacked ? undefined : format, - ); + const decoded: WireProtocolMessage[] = unpacked + ? (body as WireProtocolMessage[]) + : Utils.decodeBody(body, client._MsgPack, format); + + return _fromEncodedArray(decoded, channel); }).get(params as Record); } diff --git a/src/common/lib/client/restpresence.ts b/src/common/lib/client/restpresence.ts index 3ce4e142a..6bf932746 100644 --- a/src/common/lib/client/restpresence.ts +++ b/src/common/lib/client/restpresence.ts @@ -1,8 +1,7 @@ import * as Utils from '../util/utils'; import Logger from '../util/logger'; import PaginatedResource, { PaginatedResult } from './paginatedresource'; -import PresenceMessage, { fromResponseBody as presenceMessageFromResponseBody } from '../types/presencemessage'; -import { CipherOptions } from '../types/message'; +import PresenceMessage, { WireProtocolPresenceMessage, _fromEncodedArray } from '../types/presencemessage'; import RestChannel from './restchannel'; import Defaults from '../util/defaults'; @@ -26,20 +25,17 @@ class RestPresence { Utils.mixin(headers, client.options.headers); - const options = this.channel.channelOptions; return new PaginatedResource( client, this.channel.client.rest.presenceMixin.basePath(this), headers, envelope, async (body, headers, unpacked) => { - return await presenceMessageFromResponseBody( - body as Record[], - options as CipherOptions, - this.logger, - client._MsgPack, - unpacked ? undefined : format, - ); + const decoded: WireProtocolPresenceMessage[] = unpacked + ? (body as WireProtocolPresenceMessage[]) + : Utils.decodeBody(body, client._MsgPack, format); + + return _fromEncodedArray(decoded, this.channel); }, ).get(params); } diff --git a/src/common/lib/client/restpresencemixin.ts b/src/common/lib/client/restpresencemixin.ts index f0b2cca37..e9d562120 100644 --- a/src/common/lib/client/restpresencemixin.ts +++ b/src/common/lib/client/restpresencemixin.ts @@ -3,8 +3,7 @@ import RealtimePresence from './realtimepresence'; import * as Utils from '../util/utils'; import Defaults from '../util/defaults'; import PaginatedResource, { PaginatedResult } from './paginatedresource'; -import PresenceMessage, { fromResponseBody as presenceMessageFromResponseBody } from '../types/presencemessage'; -import { CipherOptions } from '../types/message'; +import PresenceMessage, { WireProtocolPresenceMessage, _fromEncodedArray } from '../types/presencemessage'; import { RestChannelMixin } from './restchannelmixin'; export class RestPresenceMixin { @@ -23,19 +22,18 @@ export class RestPresenceMixin { Utils.mixin(headers, client.options.headers); - const options = presence.channel.channelOptions; - return new PaginatedResource(client, this.basePath(presence) + '/history', headers, envelope, async function ( - body, + return new PaginatedResource( + client, + this.basePath(presence) + '/history', headers, - unpacked, - ) { - return await presenceMessageFromResponseBody( - body as Record[], - options as CipherOptions, - presence.logger, - client._MsgPack, - unpacked ? undefined : format, - ); - }).get(params); + envelope, + async (body, headers, unpacked) => { + const decoded: WireProtocolPresenceMessage[] = unpacked + ? (body as WireProtocolPresenceMessage[]) + : Utils.decodeBody(body, client._MsgPack, format); + + return _fromEncodedArray(decoded, presence.channel); + }, + ).get(params); } } diff --git a/src/common/lib/types/defaultmessage.ts b/src/common/lib/types/defaultmessage.ts index 79fffccf5..1161e909c 100644 --- a/src/common/lib/types/defaultmessage.ts +++ b/src/common/lib/types/defaultmessage.ts @@ -1,4 +1,5 @@ import Message, { + WireProtocolMessage, CipherOptions, decode, encode, @@ -6,28 +7,35 @@ import Message, { fromEncoded, fromEncodedArray, fromValues, + fromWireProtocol, } from './message'; import * as API from '../../../../ably'; import Platform from 'common/platform'; import PresenceMessage from './presencemessage'; import { ChannelOptions } from 'common/types/channel'; import Logger from '../util/logger'; +import type { Properties } from '../util/utils'; /** `DefaultMessage` is the class returned by `DefaultRest` and `DefaultRealtime`’s `Message` static property. It introduces the static methods described in the `MessageStatic` interface of the public API of the non tree-shakable version of the library. */ export class DefaultMessage extends Message { static async fromEncoded(encoded: unknown, inputOptions?: API.ChannelOptions): Promise { - return fromEncoded(Logger.defaultLogger, Platform.Crypto, encoded, inputOptions); + return fromEncoded(Logger.defaultLogger, Platform.Crypto, encoded as WireProtocolMessage, inputOptions); } static async fromEncodedArray(encodedArray: Array, options?: API.ChannelOptions): Promise { - return fromEncodedArray(Logger.defaultLogger, Platform.Crypto, encodedArray, options); + return fromEncodedArray(Logger.defaultLogger, Platform.Crypto, encodedArray as WireProtocolMessage[], options); } // Used by tests - static fromValues(values: Message | Record, options?: { stringifyAction?: boolean }): Message { - return fromValues(values, options); + static fromValues(values: Properties): Message { + return fromValues(values); + } + + // Used by tests + static fromWireProtocol(values: WireProtocolMessage): Message { + return fromWireProtocol(values); } // Used by tests diff --git a/src/common/lib/types/defaultpresencemessage.ts b/src/common/lib/types/defaultpresencemessage.ts index 1b8592c3e..a5ef3877d 100644 --- a/src/common/lib/types/defaultpresencemessage.ts +++ b/src/common/lib/types/defaultpresencemessage.ts @@ -1,23 +1,35 @@ import * as API from '../../../../ably'; import Logger from '../util/logger'; -import PresenceMessage, { fromEncoded, fromEncodedArray, fromValues } from './presencemessage'; +import PresenceMessage, { + fromEncoded, + fromEncodedArray, + fromValues, + WireProtocolPresenceMessage, +} from './presencemessage'; +import Platform from 'common/platform'; +import type { Properties } from '../util/utils'; /** `DefaultPresenceMessage` is the class returned by `DefaultRest` and `DefaultRealtime`’s `PresenceMessage` static property. It introduces the static methods described in the `PresenceMessageStatic` interface of the public API of the non tree-shakable version of the library. */ export class DefaultPresenceMessage extends PresenceMessage { static async fromEncoded(encoded: unknown, inputOptions?: API.ChannelOptions): Promise { - return fromEncoded(Logger.defaultLogger, encoded, inputOptions); + return fromEncoded(Logger.defaultLogger, Platform.Crypto, encoded as WireProtocolPresenceMessage, inputOptions); } static async fromEncodedArray( encodedArray: Array, options?: API.ChannelOptions, ): Promise { - return fromEncodedArray(Logger.defaultLogger, encodedArray, options); + return fromEncodedArray( + Logger.defaultLogger, + Platform.Crypto, + encodedArray as WireProtocolPresenceMessage[], + options, + ); } - static fromValues(values: PresenceMessage | Record, stringifyAction?: boolean): PresenceMessage { - return fromValues(values, stringifyAction); + static fromValues(values: Properties): PresenceMessage { + return fromValues(values); } } diff --git a/src/common/lib/types/message.ts b/src/common/lib/types/message.ts index 0a9f0d419..d0ac41d62 100644 --- a/src/common/lib/types/message.ts +++ b/src/common/lib/types/message.ts @@ -1,13 +1,17 @@ import Platform from 'common/platform'; import Logger from '../util/logger'; import ErrorInfo from './errorinfo'; -import { ChannelOptions } from '../../types/channel'; import PresenceMessage from './presencemessage'; import * as Utils from '../util/utils'; import { Bufferlike as BrowserBufferlike } from '../../../platform/web/lib/util/bufferutils'; import * as API from '../../../../ably'; -import { IUntypedCryptoStatic } from 'common/types/ICryptoStatic'; -import { MsgPack } from 'common/types/msgpack'; + +import type { IUntypedCryptoStatic } from 'common/types/ICryptoStatic'; +import type { ChannelOptions } from '../../types/channel'; +import type { Properties } from '../util/utils'; +import type RestChannel from '../client/restchannel'; +import type RealtimeChannel from '../client/realtimechannel'; +type Channel = RestChannel | RealtimeChannel; const MessageActionArray: API.MessageAction[] = [ 'message.unset', @@ -56,6 +60,8 @@ export type EncodingDecodingContext = { baseEncodedPreviousPayload?: Buffer | BrowserBufferlike; }; +export type WireProtocolMessage = Omit & { action: number }; + function normaliseContext(context: CipherOptions | EncodingDecodingContext | ChannelOptions): EncodingDecodingContext { if (!context || !(context as EncodingDecodingContext).channelOptions) { return { @@ -67,7 +73,7 @@ function normaliseContext(context: CipherOptions | EncodingDecodingContext | Cha return context as EncodingDecodingContext; } -function normalizeCipherOptions( +export function normalizeCipherOptions( Crypto: IUntypedCryptoStatic | null, logger: Logger, options: API.ChannelOptions | null, @@ -103,10 +109,10 @@ function getMessageSize(msg: Message) { export async function fromEncoded( logger: Logger, Crypto: IUntypedCryptoStatic | null, - encoded: unknown, + encoded: WireProtocolMessage, inputOptions?: API.ChannelOptions, ): Promise { - const msg = fromValues(encoded as Message | Record, { stringifyAction: true }); + const msg = fromWireProtocol(encoded); const options = normalizeCipherOptions(Crypto, logger, inputOptions ?? null); /* if decoding fails at any point, catch and return the message decoded to * the fullest extent possible */ @@ -121,7 +127,7 @@ export async function fromEncoded( export async function fromEncodedArray( logger: Logger, Crypto: IUntypedCryptoStatic | null, - encodedArray: Array, + encodedArray: Array, options?: API.ChannelOptions, ): Promise { return Promise.all( @@ -131,6 +137,26 @@ export async function fromEncodedArray( ); } +// these forms of the functions are used internally when we have a channel instance +// already, so don't need to normalise channel options +export async function _fromEncoded(encoded: WireProtocolMessage, channel: Channel): Promise { + const msg = fromWireProtocol(encoded); + try { + await decode(msg, channel.channelOptions); + } catch (e) { + Logger.logAction(channel.logger, Logger.LOG_ERROR, 'Message._fromEncoded()', (e as Error).toString()); + } + return msg; +} + +export async function _fromEncodedArray(encodedArray: WireProtocolMessage[], channel: Channel): Promise { + return Promise.all( + encodedArray.map(function (encoded) { + return _fromEncoded(encoded, channel); + }), + ); +} + async function encrypt(msg: T, options: CipherOptions): Promise { let data = msg.data, encoding = msg.encoding, @@ -272,45 +298,19 @@ export async function decode( context.baseEncodedPreviousPayload = lastPayload; } -export async function fromResponseBody( - body: Array, - options: ChannelOptions | EncodingDecodingContext, - logger: Logger, - MsgPack: MsgPack | null, - format?: Utils.Format, -): Promise { - if (format) { - body = Utils.decodeBody(body, MsgPack, format); - } - - for (let i = 0; i < body.length; i++) { - const msg = (body[i] = fromValues(body[i], { stringifyAction: true })); - try { - await decode(msg, options); - } catch (e) { - Logger.logAction(logger, Logger.LOG_ERROR, 'Message.fromResponseBody()', (e as Error).toString()); - } - } - return body; +export function fromValues(values: Properties): Message { + return Object.assign(new Message(), values); } -export function fromValues( - values: Message | Record, - options?: { stringifyAction?: boolean }, -): Message { - const stringifyAction = options?.stringifyAction; - if (stringifyAction) { - const action = toMessageActionString(values.action as number) || values.action; - return Object.assign(new Message(), { ...values, action }); - } - return Object.assign(new Message(), values); +export function fromWireProtocol(values: WireProtocolMessage): Message { + const action = toMessageActionString(values.action as number) || values.action; + const res = Object.assign(new Message(), { ...values, action }); + res.expandFields(); + return res; } -export function fromValuesArray(values: unknown[]): Message[] { - const count = values.length, - result = new Array(count); - for (let i = 0; i < count; i++) result[i] = fromValues(values[i] as Record); - return result; +export function fromValuesArray(values: Properties[]): Message[] { + return values.map(fromValues); } /* This should be called on encode()d (and encrypt()d) Messages (as it @@ -336,12 +336,12 @@ class Message { encoding?: string | null; extras?: any; size?: number; - action?: API.MessageAction | number; + action?: API.MessageAction; serial?: string; refSerial?: string; refType?: string; - updatedAt?: number; - updateSerial?: string; + createdAt?: number; + version?: string; operation?: API.Operation; /** @@ -377,14 +377,27 @@ class Message { action: toMessageActionNumber(this.action as API.MessageAction) || this.action, refSerial: this.refSerial, refType: this.refType, - updatedAt: this.updatedAt, - updateSerial: this.updateSerial, + createdAt: this.createdAt, + version: this.version, operation: this.operation, encoding, data, }; } + expandFields() { + if (this.action === 'message.create') { + // TM2k + if (this.version && !this.serial) { + this.serial = this.version; + } + // TM2o + if (this.timestamp && !this.createdAt) { + this.createdAt = this.timestamp; + } + } + } + toString(): string { let result = '[Message'; if (this.name) result += '; name=' + this.name; @@ -404,10 +417,10 @@ class Message { if (this.action) result += '; action=' + this.action; if (this.serial) result += '; serial=' + this.serial; + if (this.version) result += '; version=' + this.version; if (this.refSerial) result += '; refSerial=' + this.refSerial; if (this.refType) result += '; refType=' + this.refType; - if (this.updatedAt) result += '; updatedAt=' + this.updatedAt; - if (this.updateSerial) result += '; updateSerial=' + this.updateSerial; + if (this.createdAt) result += '; createdAt=' + this.createdAt; if (this.operation) result += '; operation=' + JSON.stringify(this.operation); result += ']'; return result; diff --git a/src/common/lib/types/presencemessage.ts b/src/common/lib/types/presencemessage.ts index 34e0d2d06..62ac6c073 100644 --- a/src/common/lib/types/presencemessage.ts +++ b/src/common/lib/types/presencemessage.ts @@ -1,22 +1,30 @@ import Logger from '../util/logger'; import Platform from 'common/platform'; -import { encode as encodeMessage, decode as decodeMessage, getMessagesSize, CipherOptions } from './message'; -import * as Utils from '../util/utils'; +import { normalizeCipherOptions, encode as encodeMessage, decode as decodeMessage, getMessagesSize } from './message'; import * as API from '../../../../ably'; -import { MsgPack } from 'common/types/msgpack'; + +import type { IUntypedCryptoStatic } from 'common/types/ICryptoStatic'; +import type { Properties } from '../util/utils'; +import type RestChannel from '../client/restchannel'; +import type RealtimeChannel from '../client/realtimechannel'; +type Channel = RestChannel | RealtimeChannel; const actions = ['absent', 'present', 'enter', 'leave', 'update']; +export type WireProtocolPresenceMessage = Omit & { action: number }; + function toActionValue(actionString: string) { return actions.indexOf(actionString); } export async function fromEncoded( logger: Logger, - encoded: unknown, - options?: API.ChannelOptions, + Crypto: IUntypedCryptoStatic | null, + encoded: WireProtocolPresenceMessage, + inputOptions?: API.ChannelOptions, ): Promise { - const msg = fromValues(encoded as PresenceMessage | Record, true); + const msg = fromWireProtocol(encoded); + const options = normalizeCipherOptions(Crypto, logger, inputOptions ?? null); /* if decoding fails at any point, catch and return the message decoded to * the fullest extent possible */ try { @@ -29,60 +37,57 @@ export async function fromEncoded( export async function fromEncodedArray( logger: Logger, - encodedArray: unknown[], + Crypto: IUntypedCryptoStatic | null, + encodedArray: WireProtocolPresenceMessage[], options?: API.ChannelOptions, ): Promise { return Promise.all( encodedArray.map(function (encoded) { - return fromEncoded(logger, encoded, options); + return fromEncoded(logger, Crypto, encoded, options); }), ); } -export function fromValues( - values: PresenceMessage | Record, - stringifyAction?: boolean, -): PresenceMessage { - if (stringifyAction) { - values.action = actions[values.action as number]; +// these forms of the functions are used internally when we have a channel instance +// already, so don't need to normalise channel options +export async function _fromEncoded(encoded: WireProtocolPresenceMessage, channel: Channel): Promise { + const msg = fromWireProtocol(encoded); + try { + await decode(msg, channel.channelOptions); + } catch (e) { + Logger.logAction(channel.logger, Logger.LOG_ERROR, 'PresenceMessage._fromEncoded()', (e as Error).toString()); } - return Object.assign(new PresenceMessage(), values); + return msg; } -export { encodeMessage as encode }; -export const decode = decodeMessage; - -export async function fromResponseBody( - body: Record[], - options: CipherOptions, - logger: Logger, - MsgPack: MsgPack | null, - format?: Utils.Format, +export async function _fromEncodedArray( + encodedArray: WireProtocolPresenceMessage[], + channel: Channel, ): Promise { - const messages: PresenceMessage[] = []; - if (format) { - body = Utils.decodeBody(body, MsgPack, format); - } + return Promise.all( + encodedArray.map(function (encoded) { + return _fromEncoded(encoded, channel); + }), + ); +} - for (let i = 0; i < body.length; i++) { - const msg = (messages[i] = fromValues(body[i], true)); - try { - await decode(msg, options); - } catch (e) { - Logger.logAction(logger, Logger.LOG_ERROR, 'PresenceMessage.fromResponseBody()', (e as Error).toString()); - } - } - return messages; +export function fromValues(values: Properties): PresenceMessage { + return Object.assign(new PresenceMessage(), values); +} + +export function fromWireProtocol(values: WireProtocolPresenceMessage): PresenceMessage { + const action = actions[values.action]; + return Object.assign(new PresenceMessage(), { ...values, action }); } -export function fromValuesArray(values: unknown[]): PresenceMessage[] { - const count = values.length, - result = new Array(count); - for (let i = 0; i < count; i++) result[i] = fromValues(values[i] as Record); - return result; +export { encodeMessage as encode }; +export const decode = decodeMessage; + +export function fromValuesArray(values: Properties[]): PresenceMessage[] { + return values.map(fromValues); } -export function fromData(data: unknown): PresenceMessage { +export function fromData(data: any): PresenceMessage { if (data instanceof PresenceMessage) { return data; } @@ -94,7 +99,7 @@ export function fromData(data: unknown): PresenceMessage { export { getMessagesSize }; class PresenceMessage { - action?: string | number; + action?: string; id?: string; timestamp?: number; clientId?: string; diff --git a/src/common/lib/types/protocolmessage.ts b/src/common/lib/types/protocolmessage.ts index ccb15841f..449cd5b9c 100644 --- a/src/common/lib/types/protocolmessage.ts +++ b/src/common/lib/types/protocolmessage.ts @@ -3,10 +3,16 @@ import * as API from '../../../../ably'; import { PresenceMessagePlugin } from '../client/modularplugins'; import * as Utils from '../util/utils'; import ErrorInfo from './errorinfo'; -import Message, { fromValues as messageFromValues, fromValuesArray as messagesFromValuesArray } from './message'; +import Message, { + fromWireProtocol as messageFromWireProtocol, + fromValuesArray as messagesFromValuesArray, + WireProtocolMessage, +} from './message'; import PresenceMessage, { + fromWireProtocol as presenceMessageFromWireProtocol, fromValues as presenceMessageFromValues, fromValuesArray as presenceMessagesFromValuesArray, + WireProtocolPresenceMessage, } from './presencemessage'; export const actions = { @@ -85,30 +91,30 @@ export function fromDeserialized( deserialized.error = ErrorInfo.fromValues(error as ErrorInfo); } - const messages = deserialized.messages as Message[]; - if (messages) { - for (let i = 0; i < messages.length; i++) { - messages[i] = messageFromValues(messages[i], { stringifyAction: true }); - } + let messages: Message[] | undefined; + if (deserialized.messages) { + const dm = deserialized.messages as WireProtocolMessage[]; + messages = dm.map((m) => messageFromWireProtocol(m)); } - const presence = presenceMessagePlugin ? (deserialized.presence as PresenceMessage[]) : undefined; - if (presenceMessagePlugin) { - if (presence && presenceMessagePlugin) { - for (let i = 0; i < presence.length; i++) { - presence[i] = presenceMessagePlugin.presenceMessageFromValues(presence[i], true); - } - } + let presence: PresenceMessage[] | undefined; + if (presenceMessagePlugin && deserialized.presence) { + const dp = deserialized.presence as WireProtocolPresenceMessage[]; + presence = dp.map((pm) => presenceMessagePlugin.presenceMessageFromWireProtocol(pm)); } - return Object.assign(new ProtocolMessage(), { ...deserialized, presence }); + return Object.assign(new ProtocolMessage(), { ...deserialized, presence, messages }); } /** * Used by the tests. */ export function fromDeserializedIncludingDependencies(deserialized: Record): ProtocolMessage { - return fromDeserialized(deserialized, { presenceMessageFromValues, presenceMessagesFromValuesArray }); + return fromDeserialized(deserialized, { + presenceMessageFromValues, + presenceMessagesFromValuesArray, + presenceMessageFromWireProtocol, + }); } export function fromValues(values: unknown): ProtocolMessage { diff --git a/src/common/lib/util/utils.ts b/src/common/lib/util/utils.ts index f1af5ff36..ef2a6ccf1 100644 --- a/src/common/lib/util/utils.ts +++ b/src/common/lib/util/utils.ts @@ -467,3 +467,6 @@ export async function withTimeoutAsync(promise: Promise, timeout = 5000, e const e = new ErrorInfo(err, 50000, 500); return Promise.race([promise, new Promise((_resolve, reject) => setTimeout(() => reject(e), timeout))]); } + +type NonFunctionKeyNames = { [P in keyof A]: A[P] extends Function ? never : P }[keyof A]; +export type Properties = Pick>; diff --git a/src/platform/web/modular/presencemessage.ts b/src/platform/web/modular/presencemessage.ts index 6c9b891a0..40e90830e 100644 --- a/src/platform/web/modular/presencemessage.ts +++ b/src/platform/web/modular/presencemessage.ts @@ -1,15 +1,24 @@ import * as API from '../../../../ably'; import { fromEncoded, fromEncodedArray, fromValues } from '../../../common/lib/types/presencemessage'; +import { Crypto } from './crypto'; import Logger from '../../../common/lib/util/logger'; // The type assertions for the functions below are due to https://github.com/ably/ably-js/issues/1421 export const decodePresenceMessage = ((obj, options) => { - return fromEncoded(Logger.defaultLogger, obj, options); + return fromEncoded(Logger.defaultLogger, null, obj, options); +}) as API.PresenceMessageStatic['fromEncoded']; + +export const decodeEncryptedPresenceMessage = ((obj, options) => { + return fromEncoded(Logger.defaultLogger, Crypto, obj, options); }) as API.PresenceMessageStatic['fromEncoded']; export const decodePresenceMessages = ((obj, options) => { - return fromEncodedArray(Logger.defaultLogger, obj, options); + return fromEncodedArray(Logger.defaultLogger, null, obj, options); +}) as API.PresenceMessageStatic['fromEncodedArray']; + +export const decodeEncryptedPresenceMessages = ((obj, options) => { + return fromEncodedArray(Logger.defaultLogger, Crypto, obj, options); }) as API.PresenceMessageStatic['fromEncodedArray']; export const constructPresenceMessage = fromValues as API.PresenceMessageStatic['fromValues']; diff --git a/src/platform/web/modular/realtimepresence.ts b/src/platform/web/modular/realtimepresence.ts index f4f138d72..4d42fd932 100644 --- a/src/platform/web/modular/realtimepresence.ts +++ b/src/platform/web/modular/realtimepresence.ts @@ -3,12 +3,14 @@ import { default as realtimePresenceClass } from '../../../common/lib/client/rea import { fromValues as presenceMessageFromValues, fromValuesArray as presenceMessagesFromValuesArray, + fromWireProtocol as presenceMessageFromWireProtocol, } from '../../../common/lib/types/presencemessage'; const RealtimePresence: RealtimePresencePlugin = { RealtimePresence: realtimePresenceClass, presenceMessageFromValues, presenceMessagesFromValuesArray, + presenceMessageFromWireProtocol, }; export { RealtimePresence }; diff --git a/test/browser/modular.test.js b/test/browser/modular.test.js index 739b1becb..6834223cb 100644 --- a/test/browser/modular.test.js +++ b/test/browser/modular.test.js @@ -8,6 +8,8 @@ import { decodeEncryptedMessage, decodeMessages, decodeEncryptedMessages, + decodeEncryptedPresenceMessage, + decodeEncryptedPresenceMessages, Crypto, MsgPack, RealtimePresence, @@ -348,6 +350,26 @@ function registerAblyModularTests(Helper) { }); }); + describe('decodeEncryptedPresenceMessage', async () => { + /** @nospec */ + it('decrypts a presence message', async function () { + const helper = this.test.helper; + const testData = await loadTestData(helper, helper.testResourcesPath + 'crypto-data-128.json'); + + const key = BufferUtils.base64Decode(testData.key); + const iv = BufferUtils.base64Decode(testData.iv); + + for (const item of testData.items) { + const [decodedFromEncoded, decodedFromEncrypted] = await Promise.all([ + decodePresenceMessage(item.encoded), + decodeEncryptedPresenceMessage(item.encrypted, { cipher: { key, iv } }), + ]); + + this.test.helper.testMessageEquality(decodedFromEncoded, decodedFromEncrypted); + } + }); + }); + async function testDecodesMessagesData(helper, functionUnderTest) { const testData = await loadTestData(helper, helper.testResourcesPath + 'crypto-data-128.json'); @@ -418,6 +440,29 @@ function registerAblyModularTests(Helper) { } }); }); + + describe('decodeEncryptedPresenceMessages', () => { + /** @nospec */ + it('decrypts messages', async function () { + const helper = this.test.helper; + const testData = await loadTestData(helper, helper.testResourcesPath + 'crypto-data-128.json'); + + const key = BufferUtils.base64Decode(testData.key); + const iv = BufferUtils.base64Decode(testData.iv); + + const [decodedFromEncoded, decodedFromEncrypted] = await Promise.all([ + decodePresenceMessages(testData.items.map((item) => item.encoded)), + decodeEncryptedPresenceMessages( + testData.items.map((item) => item.encrypted), + { cipher: { key, iv } }, + ), + ]); + + for (let i = 0; i < decodedFromEncoded.length; i++) { + this.test.helper.testMessageEquality(decodedFromEncoded[i], decodedFromEncrypted[i]); + } + }); + }); }); describe('Crypto', () => { diff --git a/test/realtime/crypto.test.js b/test/realtime/crypto.test.js index 18df2f11e..9eeca0eef 100644 --- a/test/realtime/crypto.test.js +++ b/test/realtime/crypto.test.js @@ -393,9 +393,8 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, Helper, async var msgpackFromEncrypted = msgpack.encode(encryptedMessage); helper.recordPrivateApi('call.BufferUtils.base64Decode'); helper.recordPrivateApi('call.msgpack.decode'); - var messageFromMsgpack = Message.fromValues( + var messageFromMsgpack = Message.fromWireProtocol( msgpack.decode(BufferUtils.base64Decode(msgpackEncodedMessage)), - { stringifyAction: true }, ); try { @@ -438,9 +437,8 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, Helper, async var msgpackFromEncrypted = msgpack.encode(encryptedMessage); helper.recordPrivateApi('call.BufferUtils.base64Decode'); helper.recordPrivateApi('call.msgpack.decode'); - var messageFromMsgpack = Message.fromValues( + var messageFromMsgpack = Message.fromWireProtocol( msgpack.decode(BufferUtils.base64Decode(msgpackEncodedMessage)), - { stringifyAction: true }, ); try { diff --git a/test/realtime/message.test.js b/test/realtime/message.test.js index 6cb2cfd0a..9f54cce7d 100644 --- a/test/realtime/message.test.js +++ b/test/realtime/message.test.js @@ -1275,40 +1275,29 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, Helper, async /** * @spec TM2j */ - describe('DefaultMessage.fromValues stringify action', function () { + describe('DefaultMessage.fromWireProtocol', function () { const testCases = [ { description: 'should stringify the numeric action', action: 1, - options: { stringifyAction: true }, expectedString: '[Message; action=message.create]', expectedJSON: { action: 1 }, }, - { - description: 'should not stringify the numeric action', - action: 1, - options: { stringifyAction: false }, - expectedString: '[Message; action=1]', - expectedJSON: { action: 1 }, - }, { description: 'should accept an already stringified action', action: 'message.update', - options: { stringifyAction: true }, expectedString: '[Message; action=message.update]', expectedJSON: { action: 2 }, }, { description: 'should handle no action provided', action: undefined, - options: { stringifyAction: true }, expectedString: '[Message]', expectedJSON: { action: undefined }, }, { description: 'should handle unknown action provided', action: 10, - options: { stringifyAction: true }, expectedString: '[Message; action=10]', expectedJSON: { action: 10 }, }, @@ -1316,11 +1305,30 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, Helper, async testCases.forEach(({ description, action, options, expectedString, expectedJSON }) => { it(description, function () { const values = { action }; - const message = Message.fromValues(values, options); + const message = Message.fromWireProtocol(values); expect(message.toString()).to.equal(expectedString); expect(message.toJSON()).to.deep.contains(expectedJSON); }); }); + + /** + * @spec TM2k + * @spec TM2o + */ + it('create message should fill out serial and createdAt from version/timestamp', function () { + const values = { action: 1, timestamp: 12345, version: 'foo' }; + const message = Message.fromWireProtocol(values); + expect(message.timestamp).to.equal(12345); + expect(message.createdAt).to.equal(12345); + expect(message.version).to.equal('foo'); + expect(message.serial).to.equal('foo'); + + // should only apply to creates + const update = { action: 2, timestamp: 12345, version: 'foo' }; + const updateMessage = Message.fromWireProtocol(update); + expect(updateMessage.createdAt).to.equal(undefined); + expect(updateMessage.serial).to.equal(undefined); + }); }); /**