Skip to content

Commit 879347f

Browse files
committed
Decode bytes and JSON objects in expanded REST Objects responses
1 parent 7981a0b commit 879347f

File tree

1 file changed

+180
-14
lines changed

1 file changed

+180
-14
lines changed

src/plugins/objects/restobject.ts

Lines changed: 180 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,62 @@ import type RestChannel from 'common/lib/client/restchannel';
22
import type * as Utils from 'common/lib/util/utils';
33
import type {
44
GetObjectParams,
5+
ObjectsMapSemantics,
56
PrimitiveOrObjectReference,
67
RestCompactObjectData,
78
RestLiveObject,
9+
RestObjectData,
10+
RestObjectMapEntry,
811
RestObjectOperation,
912
RestObjectPublishResult,
1013
} from '../../../ably';
1114

12-
export class RestObject {
13-
channel: RestChannel;
15+
enum WireObjectsMapSemantics {
16+
LWW = 'LWW',
17+
}
1418

15-
constructor(channel: RestChannel) {
16-
this.channel = channel;
17-
}
19+
const mapSemantics: Record<WireObjectsMapSemantics, ObjectsMapSemantics> = {
20+
[WireObjectsMapSemantics.LWW]: 'lww',
21+
};
22+
23+
type WireRestLiveObject = WireRestLiveMap | WireRestLiveCounter | WireAnyRestLiveObject;
24+
25+
interface WireRestLiveMap {
26+
objectId: string;
27+
map: {
28+
semantics: WireObjectsMapSemantics;
29+
entries: Record<string, WireRestObjectMapEntry>;
30+
};
31+
}
32+
33+
export interface WireRestObjectMapEntry {
34+
data: WireRestLiveObject | WireRestObjectData;
35+
}
36+
37+
export interface WireRestObjectData {
38+
objectId?: string;
39+
number?: number;
40+
boolean?: boolean;
41+
string?: string;
42+
bytes?: string | Buffer | ArrayBuffer;
43+
json?: string;
44+
}
45+
46+
export interface WireRestLiveCounter {
47+
objectId: string;
48+
counter: {
49+
data: {
50+
number: number;
51+
};
52+
};
53+
}
54+
55+
export type WireAnyRestLiveObject = {
56+
objectId: string;
57+
};
58+
59+
export class RestObject {
60+
constructor(private _channel: RestChannel) {}
1861

1962
/**
2063
* Read object data. Defaults to { compact: true } and entrypoint=root when no id is provided.
@@ -23,15 +66,15 @@ export class RestObject {
2366
async get(params?: Omit<GetObjectParams, 'compact'> & { compact?: true }): Promise<RestCompactObjectData | undefined>;
2467
async get(params: Omit<GetObjectParams, 'compact'> & { compact: false }): Promise<RestLiveObject | undefined>;
2568
async get(params?: GetObjectParams): Promise<RestCompactObjectData | RestLiveObject | undefined> {
26-
const client = this.channel.client;
69+
const client = this._channel.client;
2770
const format = client.options.useBinaryProtocol ? client.Utils.Format.msgpack : client.Utils.Format.json;
2871
const envelope = client.http.supportsLinkHeaders ? null : format;
2972
const headers = client.Defaults.defaultGetHeaders(client.options);
3073

3174
client.Utils.mixin(headers, client.options.headers);
3275

3376
try {
34-
const response = await client.rest.Resource.get<RestCompactObjectData | RestLiveObject>(
77+
const response = await client.rest.Resource.get<RestCompactObjectData | WireRestLiveObject>(
3578
client,
3679
this._basePath(params?.objectId ?? 'root'),
3780
headers,
@@ -40,13 +83,30 @@ export class RestObject {
4083
true,
4184
);
4285

86+
let decodedBody: RestCompactObjectData | WireRestLiveObject | undefined;
4387
if (format) {
44-
return client.Utils.decodeBody(response.body, client._MsgPack, format);
88+
decodedBody = client.Utils.decodeBody(response.body, client._MsgPack, format);
89+
} else {
90+
decodedBody = response.body;
91+
}
92+
93+
if (decodedBody == undefined) {
94+
return undefined;
95+
}
96+
97+
// non-object primitive values
98+
if (typeof decodedBody !== 'object') {
99+
return decodedBody;
45100
}
46101

47-
return response.body;
102+
// live counter or JSON object
103+
if (!this._isWireLiveMap(decodedBody)) {
104+
return decodedBody;
105+
}
106+
107+
return this._decodeWireLiveObject(decodedBody, format);
48108
} catch (error) {
49-
if (this.channel.client.Utils.isErrorInfoOrPartialErrorInfo(error) && error.code === 40400) {
109+
if (this._channel.client.Utils.isErrorInfoOrPartialErrorInfo(error) && error.code === 40400) {
50110
// ignore object resolution errors and return undefined
51111
return undefined;
52112
}
@@ -59,7 +119,7 @@ export class RestObject {
59119
* Publish one or more operations.
60120
*/
61121
async publish(op: RestObjectOperation | RestObjectOperation[]): Promise<RestObjectPublishResult> {
62-
const client = this.channel.client;
122+
const client = this._channel.client;
63123
const options = client.options;
64124
const format = options.useBinaryProtocol ? client.Utils.Format.msgpack : client.Utils.Format.json;
65125
const headers = client.Defaults.defaultPostHeaders(client.options);
@@ -91,7 +151,7 @@ export class RestObject {
91151

92152
private _basePath(objectId?: string): string {
93153
return (
94-
this.channel.client.rest.channelMixin.basePath(this.channel) +
154+
this._channel.client.rest.channelMixin.basePath(this._channel) +
95155
'/object' +
96156
(objectId ? '/' + encodeURIComponent(objectId) : '')
97157
);
@@ -101,6 +161,10 @@ export class RestObject {
101161
const operation = op.operation;
102162
switch (operation) {
103163
case 'map.create':
164+
if (op.path == null) {
165+
throw new this._channel.client.ErrorInfo('Path must be provided for "map.create" operation', 40003, 400);
166+
}
167+
104168
return {
105169
operation: 'MAP_CREATE',
106170
id: op.id,
@@ -115,6 +179,7 @@ export class RestObject {
115179
{} as Record<string, any>,
116180
),
117181
};
182+
118183
case 'map.set':
119184
return {
120185
operation: 'MAP_SET',
@@ -130,6 +195,7 @@ export class RestObject {
130195
},
131196
},
132197
};
198+
133199
case 'map.remove':
134200
return {
135201
operation: 'MAP_REMOVE',
@@ -139,7 +205,12 @@ export class RestObject {
139205
path: op.path,
140206
data: { key: op.key },
141207
};
208+
142209
case 'counter.create':
210+
if (op.path == null) {
211+
throw new this._channel.client.ErrorInfo('Path must be provided for "counter.create" operation', 40003, 400);
212+
}
213+
143214
return {
144215
operation: 'COUNTER_CREATE',
145216
id: op.id,
@@ -148,6 +219,7 @@ export class RestObject {
148219
path: op.path,
149220
data: { number: op.count },
150221
};
222+
151223
case 'counter.inc':
152224
return {
153225
operation: 'COUNTER_INC',
@@ -159,12 +231,12 @@ export class RestObject {
159231
};
160232

161233
default:
162-
throw new this.channel.client.ErrorInfo('Unsupported publish operation action: ' + operation, 40003, 400);
234+
throw new this._channel.client.ErrorInfo('Unsupported publish operation action: ' + operation, 40003, 400);
163235
}
164236
}
165237

166238
private _encodePrimitive(value: PrimitiveOrObjectReference, format: Utils.Format): any {
167-
const client = this.channel.client;
239+
const client = this._channel.client;
168240
if (client.Platform.BufferUtils.isBuffer(value)) {
169241
return {
170242
bytes: client.MessageEncoding.encodeDataForWire(value, null, format).data,
@@ -185,4 +257,98 @@ export class RestObject {
185257

186258
return value;
187259
}
260+
261+
private _decodeWireLiveObject(obj: WireRestLiveObject, format: Utils.Format): RestLiveObject {
262+
// expanded live map object which needs decoding
263+
if (this._isWireLiveMap(obj)) {
264+
return this._decodeWireRestLiveMap(obj, format);
265+
}
266+
267+
// live counter or JSON object
268+
return obj;
269+
}
270+
271+
private _decodeWireRestLiveMap(obj: WireRestLiveMap, format: Utils.Format): RestLiveObject {
272+
return {
273+
objectId: obj.objectId,
274+
map: {
275+
semantics: (mapSemantics[obj.map.semantics] ?? 'unknown') as ObjectsMapSemantics,
276+
entries:
277+
typeof obj.map.entries === 'object'
278+
? Object.entries(obj.map.entries).reduce(
279+
(acc, entry) => {
280+
const [key, value] = entry;
281+
const entryData = value.data;
282+
acc[key] = {
283+
data: this._isWireObjectData(entryData)
284+
? this._decodeWireObjectData(entryData, format)
285+
: this._decodeWireLiveObject(entryData, format),
286+
};
287+
return acc;
288+
},
289+
{} as Record<string, RestObjectMapEntry>,
290+
)
291+
: obj.map.entries,
292+
},
293+
};
294+
}
295+
296+
private _decodeWireObjectData(obj: WireRestObjectData, format: Utils.Format): RestObjectData {
297+
const client = this._channel.client;
298+
299+
if (obj.objectId != null) {
300+
return { objectId: obj.objectId };
301+
}
302+
if (obj.number != null) {
303+
return { number: obj.number };
304+
}
305+
if (obj.boolean != null) {
306+
return { boolean: obj.boolean };
307+
}
308+
if (obj.string != null) {
309+
return { string: obj.string };
310+
}
311+
if (obj.bytes != null) {
312+
const decodedBytes =
313+
format === 'msgpack'
314+
? // connection is using msgpack protocol, bytes are already a buffer
315+
(obj.bytes as Buffer | ArrayBuffer)
316+
: // connection is using JSON protocol, Base64-decode bytes value
317+
client.Platform.BufferUtils.base64Decode(String(obj.bytes));
318+
return { bytes: decodedBytes };
319+
}
320+
if (obj.json != null) {
321+
return { json: JSON.parse(obj.json) };
322+
}
323+
324+
client.Logger.logAction(
325+
client.logger,
326+
client.Logger.LOG_ERROR,
327+
'RestObject._decodeWireObjectData: Unknown object data format, returning empty object; object data: ' +
328+
client.Platform.Config.inspect(obj),
329+
);
330+
return {};
331+
}
332+
333+
private _isWireLiveMap(obj: unknown): obj is WireRestLiveMap {
334+
return (
335+
typeof obj === 'object' &&
336+
obj != undefined &&
337+
'objectId' in obj &&
338+
'map' in obj &&
339+
typeof obj.map === 'object' &&
340+
obj.map != undefined &&
341+
'semantics' in obj.map &&
342+
'entries' in obj.map
343+
);
344+
}
345+
346+
private _isWireObjectData(obj: unknown): obj is WireRestObjectData {
347+
return (
348+
typeof obj === 'object' &&
349+
obj != undefined &&
350+
Object.keys(obj).length === 1 &&
351+
['objectId', 'number', 'boolean', 'string', 'bytes', 'json'].includes(Object.keys(obj)[0])
352+
);
353+
}
188354
}

0 commit comments

Comments
 (0)