Skip to content

Commit b031dbb

Browse files
committed
Add support for encoding/decoding JSON objects for LiveMap values
This enables to set and get JSON objects and arrays via LiveMap.set and LiveMap.get methods. Resolves PUB-1667
1 parent 9474406 commit b031dbb

7 files changed

Lines changed: 188 additions & 215 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -688,13 +688,15 @@ for (const value of root.values()) {
688688
await root.set('foo', 'Alice');
689689
await root.set('bar', 1);
690690
await root.set('baz', true);
691-
await root.set('qux', new Uint8Array([21, 31]));
691+
await root.set('qux', { hello: 'world' });
692+
await root.set('quux', [ 42 ]);
693+
await root.set('corge', new Uint8Array([21, 31]));
692694
// as well as other objects
693695
const counter = await objects.createCounter();
694-
await root.set('quux', counter);
696+
await root.set('grault', counter);
695697

696698
// and you can remove keys with .remove
697-
await root.remove('name');
699+
await root.remove('foo');
698700
```
699701
700702
`LiveCounter` - A counter that can be incremented or decremented and is synchronized across clients in realtime

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/plugins/objects/livemap.ts

Lines changed: 17 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,18 @@ import {
1515
ObjectsMapOp,
1616
ObjectsMapSemantics,
1717
ObjectState,
18+
PrimitiveObjectValue,
1819
} from './objectmessage';
1920
import { Objects } from './objects';
2021

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

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

4132
export type LiveMapObjectData = ObjectIdObjectData | ValueObjectData;
@@ -126,16 +117,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
126117
const typedObjectData: ObjectIdObjectData = { objectId: value.getObjectId() };
127118
objectData = typedObjectData;
128119
} else {
129-
const typedObjectData: ValueObjectData = {};
130-
if (typeof value === 'string') {
131-
typedObjectData.string = value;
132-
} else if (typeof value === 'number') {
133-
typedObjectData.number = value;
134-
} else if (typeof value === 'boolean') {
135-
typedObjectData.boolean = value;
136-
} else {
137-
typedObjectData.bytes = value as Bufferlike;
138-
}
120+
const typedObjectData: ValueObjectData = { value: value as PrimitiveObjectValue };
139121
objectData = typedObjectData;
140122
}
141123

@@ -201,11 +183,11 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
201183
}
202184

203185
if (
204-
typeof value !== 'string' &&
205-
typeof value !== 'number' &&
206-
typeof value !== 'boolean' &&
207-
!client.Platform.BufferUtils.isBuffer(value) &&
208-
!(value instanceof LiveObject)
186+
value === null ||
187+
(typeof value !== 'string' &&
188+
typeof value !== 'number' &&
189+
typeof value !== 'boolean' &&
190+
typeof value !== 'object')
209191
) {
210192
throw new client.ErrorInfo('Map value data type is unsupported', 40013, 400); // OD4a
211193
}
@@ -266,16 +248,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
266248
const typedObjectData: ObjectIdObjectData = { objectId: value.getObjectId() };
267249
objectData = typedObjectData;
268250
} else {
269-
const typedObjectData: ValueObjectData = {};
270-
if (typeof value === 'string') {
271-
typedObjectData.string = value;
272-
} else if (typeof value === 'number') {
273-
typedObjectData.number = value;
274-
} else if (typeof value === 'boolean') {
275-
typedObjectData.boolean = value;
276-
} else {
277-
typedObjectData.bytes = value as Bufferlike;
278-
}
251+
const typedObjectData: ValueObjectData = { value: value as PrimitiveObjectValue };
279252
objectData = typedObjectData;
280253
}
281254

@@ -717,14 +690,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
717690
return { noop: true };
718691
}
719692

720-
if (
721-
Utils.isNil(op.data) ||
722-
(Utils.isNil(op.data.objectId) &&
723-
Utils.isNil(op.data.boolean) &&
724-
Utils.isNil(op.data.bytes) &&
725-
Utils.isNil(op.data.number) &&
726-
Utils.isNil(op.data.string))
727-
) {
693+
if (Utils.isNil(op.data) || (Utils.isNil(op.data.objectId) && Utils.isNil(op.data.value))) {
728694
throw new ErrorInfo(
729695
`Invalid object data for MAP_SET op on objectId=${this.getObjectId()} on key=${op.key}`,
730696
92000,
@@ -742,13 +708,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
742708
// so instead we create a zero-value object for that object id if it not exists.
743709
this._objects.getPool().createZeroValueObjectIfNotExists(op.data.objectId); // RTLM7c1
744710
} else {
745-
liveData = {
746-
encoding: op.data.encoding,
747-
boolean: op.data.boolean,
748-
bytes: op.data.bytes,
749-
number: op.data.number,
750-
string: op.data.string,
751-
} as ValueObjectData;
711+
liveData = { value: op.data.value } as ValueObjectData;
752712
}
753713

754714
if (existingEntry) {
@@ -859,13 +819,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
859819
if (!this._client.Utils.isNil(entry.data.objectId)) {
860820
liveData = { objectId: entry.data.objectId } as ObjectIdObjectData;
861821
} else {
862-
liveData = {
863-
encoding: entry.data.encoding,
864-
boolean: entry.data.boolean,
865-
bytes: entry.data.bytes,
866-
number: entry.data.number,
867-
string: entry.data.string,
868-
} as ValueObjectData;
822+
liveData = { value: entry.data.value } as ValueObjectData;
869823
}
870824
}
871825

@@ -887,19 +841,10 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
887841
* 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.
888842
*/
889843
private _getResolvedValueFromObjectData(data: LiveMapObjectData): PrimitiveObjectValue | LiveObject | undefined {
890-
// if object data stores one of the primitive values, just return it as is.
891-
const asValueObject = data as ValueObjectData;
892-
if (asValueObject.boolean !== undefined) {
893-
return asValueObject.boolean; // RTLM5d2b
894-
}
895-
if (asValueObject.bytes !== undefined) {
896-
return asValueObject.bytes; // RTLM5d2c
897-
}
898-
if (asValueObject.number !== undefined) {
899-
return asValueObject.number; // RTLM5d2d
900-
}
901-
if (asValueObject.string !== undefined) {
902-
return asValueObject.string; // RTLM5d2e
844+
// if object data stores primitive value, just return it as is.
845+
const primitiveValue = (data as ValueObjectData).value;
846+
if (primitiveValue != null) {
847+
return primitiveValue; // RTLM5d2b, RTLM5d2c, RTLM5d2d, RTLM5d2e
903848
}
904849

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

0 commit comments

Comments
 (0)