Skip to content

Commit bef0fff

Browse files
authored
Merge pull request #2052 from ably/PUB-1667/map-set-objects
[PUB-1667] Add support for encoding/decoding JSON objects for LiveMap values
2 parents 8dfda2e + 6d43429 commit bef0fff

File tree

6 files changed

+270
-292
lines changed

6 files changed

+270
-292
lines changed

ably.d.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2440,7 +2440,7 @@ export declare interface BatchContextLiveMap<T extends LiveMapType> {
24402440
* Mirrors the {@link LiveMap.get} method and returns the value associated with a key in the map.
24412441
*
24422442
* @param key - The key to retrieve the value for.
2443-
* @returns A {@link LiveObject}, a primitive type (string, number, boolean, or binary data) or `undefined` if the key doesn't exist in a map or the associated {@link LiveObject} has been deleted. Always `undefined` if this map object is deleted.
2443+
* @returns A {@link LiveObject}, a primitive type (string, number, boolean, JSON-serializable object or array, or binary data) or `undefined` if the key doesn't exist in a map or the associated {@link LiveObject} has been deleted. Always `undefined` if this map object is deleted.
24442444
* @experimental
24452445
*/
24462446
get<TKey extends keyof T & string>(key: TKey): T[TKey] | undefined;
@@ -2515,7 +2515,7 @@ export declare interface BatchContextLiveCounter {
25152515
* Conflicts in a LiveMap are automatically resolved with last-write-wins (LWW) semantics,
25162516
* meaning that if two clients update the same key in the map, the update with the most recent timestamp wins.
25172517
*
2518-
* Keys must be strings. Values can be another {@link LiveObject}, or a primitive type, such as a string, number, boolean, or binary data (see {@link PrimitiveObjectValue}).
2518+
* Keys must be strings. Values can be another {@link LiveObject}, or a primitive type, such as a string, number, boolean, JSON-serializable object or array, or binary data (see {@link PrimitiveObjectValue}).
25192519
*/
25202520
export declare interface LiveMap<T extends LiveMapType> extends LiveObject<LiveMapUpdate<T>> {
25212521
/**
@@ -2524,7 +2524,7 @@ export declare interface LiveMap<T extends LiveMapType> extends LiveObject<LiveM
25242524
* Always returns undefined if this map object is deleted.
25252525
*
25262526
* @param key - The key to retrieve the value for.
2527-
* @returns A {@link LiveObject}, a primitive type (string, number, boolean, or binary data) or `undefined` if the key doesn't exist in a map or the associated {@link LiveObject} has been deleted. Always `undefined` if this map object is deleted.
2527+
* @returns A {@link LiveObject}, a primitive type (string, number, boolean, JSON-serializable object or array, or binary data) or `undefined` if the key doesn't exist in a map or the associated {@link LiveObject} has been deleted. Always `undefined` if this map object is deleted.
25282528
* @experimental
25292529
*/
25302530
get<TKey extends keyof T & string>(key: TKey): T[TKey] | undefined;
@@ -2602,7 +2602,27 @@ export declare interface LiveMapUpdate<T extends LiveMapType> extends LiveObject
26022602
*
26032603
* For binary data, the resulting type depends on the platform (`Buffer` in Node.js, `ArrayBuffer` elsewhere).
26042604
*/
2605-
export type PrimitiveObjectValue = string | number | boolean | Buffer | ArrayBuffer;
2605+
export type PrimitiveObjectValue = string | number | boolean | Buffer | ArrayBuffer | JsonArray | JsonObject;
2606+
2607+
/**
2608+
* Represents a JSON-encodable value.
2609+
*/
2610+
export type Json = JsonScalar | JsonArray | JsonObject;
2611+
2612+
/**
2613+
* Represents a JSON-encodable scalar value.
2614+
*/
2615+
export type JsonScalar = null | boolean | number | string;
2616+
2617+
/**
2618+
* Represents a JSON-encodable array.
2619+
*/
2620+
export type JsonArray = Json[];
2621+
2622+
/**
2623+
* Represents a JSON-encodable object.
2624+
*/
2625+
export type JsonObject = { [prop: string]: Json | undefined };
26062626

26072627
/**
26082628
* The `LiveCounter` class represents a counter that can be incremented or decremented and is synchronized across clients in realtime.

src/common/lib/types/basemessage.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,7 @@ export async function encryptData(
9595
* Implements RSL4 and RSL5.
9696
*/
9797
export async function encode<T extends BaseMessage>(msg: T, options: unknown): Promise<T> {
98-
// RSL4a, supported types
99-
const isNativeDataType =
100-
typeof msg.data == 'string' ||
101-
Platform.BufferUtils.isBuffer(msg.data) ||
102-
msg.data === null ||
103-
msg.data === undefined;
104-
const { data, encoding } = encodeData(msg.data, msg.encoding, isNativeDataType);
98+
const { data, encoding } = encodeData(msg.data, msg.encoding);
10599

106100
msg.data = data;
107101
msg.encoding = encoding;
@@ -116,9 +110,12 @@ export async function encode<T extends BaseMessage>(msg: T, options: unknown): P
116110
export function encodeData(
117111
data: any,
118112
encoding: string | null | undefined,
119-
isNativeDataType: boolean,
120113
): { data: any; encoding: string | null | undefined } {
121-
if (isNativeDataType) {
114+
// RSL4a, supported types
115+
const nativeDataType =
116+
typeof data == 'string' || Platform.BufferUtils.isBuffer(data) || data === null || data === undefined;
117+
118+
if (nativeDataType) {
122119
// nothing to do with the native data types at this point
123120
return {
124121
data,

src/plugins/objects/livemap.ts

Lines changed: 17 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,18 @@ import {
1313
ObjectsMapEntry,
1414
ObjectsMapOp,
1515
ObjectsMapSemantics,
16+
PrimitiveObjectValue,
1617
} from './objectmessage';
1718
import { Objects } from './objects';
1819

19-
export type PrimitiveObjectValue = string | number | boolean | Bufferlike;
20-
2120
export interface ObjectIdObjectData {
2221
/** A reference to another object, used to support composable object structures. */
2322
objectId: string;
2423
}
2524

2625
export interface ValueObjectData {
27-
/** Can be set by the client to indicate that value in `string` or `bytes` field have an encoding. */
28-
encoding?: string;
29-
/** A primitive boolean leaf value in the object graph. Only one value field can be set. */
30-
boolean?: boolean;
31-
/** A primitive binary leaf value in the object graph. Only one value field can be set. */
32-
bytes?: Bufferlike;
33-
/** A primitive number leaf value in the object graph. Only one value field can be set. */
34-
number?: number;
35-
/** A primitive string leaf value in the object graph. Only one value field can be set. */
36-
string?: string;
26+
/** A decoded leaf value from {@link WireObjectData}. */
27+
value: string | number | boolean | Bufferlike | API.JsonArray | API.JsonObject;
3728
}
3829

3930
export type LiveMapObjectData = ObjectIdObjectData | ValueObjectData;
@@ -118,16 +109,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
118109
const typedObjectData: ObjectIdObjectData = { objectId: value.getObjectId() };
119110
objectData = typedObjectData;
120111
} else {
121-
const typedObjectData: ValueObjectData = {};
122-
if (typeof value === 'string') {
123-
typedObjectData.string = value;
124-
} else if (typeof value === 'number') {
125-
typedObjectData.number = value;
126-
} else if (typeof value === 'boolean') {
127-
typedObjectData.boolean = value;
128-
} else {
129-
typedObjectData.bytes = value as Bufferlike;
130-
}
112+
const typedObjectData: ValueObjectData = { value: value as PrimitiveObjectValue };
131113
objectData = typedObjectData;
132114
}
133115

@@ -193,11 +175,11 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
193175
}
194176

195177
if (
196-
typeof value !== 'string' &&
197-
typeof value !== 'number' &&
198-
typeof value !== 'boolean' &&
199-
!client.Platform.BufferUtils.isBuffer(value) &&
200-
!(value instanceof LiveObject)
178+
value === null ||
179+
(typeof value !== 'string' &&
180+
typeof value !== 'number' &&
181+
typeof value !== 'boolean' &&
182+
typeof value !== 'object')
201183
) {
202184
throw new client.ErrorInfo('Map value data type is unsupported', 40013, 400); // OD4a
203185
}
@@ -258,16 +240,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
258240
const typedObjectData: ObjectIdObjectData = { objectId: value.getObjectId() };
259241
objectData = typedObjectData;
260242
} else {
261-
const typedObjectData: ValueObjectData = {};
262-
if (typeof value === 'string') {
263-
typedObjectData.string = value;
264-
} else if (typeof value === 'number') {
265-
typedObjectData.number = value;
266-
} else if (typeof value === 'boolean') {
267-
typedObjectData.boolean = value;
268-
} else {
269-
typedObjectData.bytes = value as Bufferlike;
270-
}
243+
const typedObjectData: ValueObjectData = { value: value as PrimitiveObjectValue };
271244
objectData = typedObjectData;
272245
}
273246

@@ -714,14 +687,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
714687
return { noop: true };
715688
}
716689

717-
if (
718-
Utils.isNil(op.data) ||
719-
(Utils.isNil(op.data.objectId) &&
720-
Utils.isNil(op.data.boolean) &&
721-
Utils.isNil(op.data.bytes) &&
722-
Utils.isNil(op.data.number) &&
723-
Utils.isNil(op.data.string))
724-
) {
690+
if (Utils.isNil(op.data) || (Utils.isNil(op.data.objectId) && Utils.isNil(op.data.value))) {
725691
throw new ErrorInfo(
726692
`Invalid object data for MAP_SET op on objectId=${this.getObjectId()} on key="${op.key}"`,
727693
92000,
@@ -739,13 +705,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
739705
// so instead we create a zero-value object for that object id if it not exists.
740706
this._objects.getPool().createZeroValueObjectIfNotExists(op.data.objectId); // RTLM7c1
741707
} else {
742-
liveData = {
743-
encoding: op.data.encoding,
744-
boolean: op.data.boolean,
745-
bytes: op.data.bytes,
746-
number: op.data.number,
747-
string: op.data.string,
748-
} as ValueObjectData;
708+
liveData = { value: op.data.value } as ValueObjectData;
749709
}
750710

751711
if (existingEntry) {
@@ -870,13 +830,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
870830
if (!this._client.Utils.isNil(entry.data.objectId)) {
871831
liveData = { objectId: entry.data.objectId } as ObjectIdObjectData;
872832
} else {
873-
liveData = {
874-
encoding: entry.data.encoding,
875-
boolean: entry.data.boolean,
876-
bytes: entry.data.bytes,
877-
number: entry.data.number,
878-
string: entry.data.string,
879-
} as ValueObjectData;
833+
liveData = { value: entry.data.value } as ValueObjectData;
880834
}
881835
}
882836

@@ -913,19 +867,10 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
913867
* Returns value as is if object data stores a primitive type, or a reference to another LiveObject from the pool if it stores an objectId.
914868
*/
915869
private _getResolvedValueFromObjectData(data: LiveMapObjectData): PrimitiveObjectValue | LiveObject | undefined {
916-
// if object data stores one of the primitive values, just return it as is.
917-
const asValueObject = data as ValueObjectData;
918-
if (asValueObject.boolean !== undefined) {
919-
return asValueObject.boolean; // RTLM5d2b
920-
}
921-
if (asValueObject.bytes !== undefined) {
922-
return asValueObject.bytes; // RTLM5d2c
923-
}
924-
if (asValueObject.number !== undefined) {
925-
return asValueObject.number; // RTLM5d2d
926-
}
927-
if (asValueObject.string !== undefined) {
928-
return asValueObject.string; // RTLM5d2e
870+
// if object data stores primitive value, just return it as is.
871+
const primitiveValue = (data as ValueObjectData).value;
872+
if (primitiveValue != null) {
873+
return primitiveValue; // RTLM5d2b, RTLM5d2c, RTLM5d2d, RTLM5d2e
929874
}
930875

931876
// RTLM5d2f - otherwise, it has an objectId reference, and we should get the actual object from the pool

0 commit comments

Comments
 (0)