-
Notifications
You must be signed in to change notification settings - Fork 59
Expand file tree
/
Copy pathlivemapvaluetype.ts
More file actions
168 lines (150 loc) · 6.61 KB
/
livemapvaluetype.ts
File metadata and controls
168 lines (150 loc) · 6.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import { __livetype } from '../../../ably';
import { LiveMap as PublicLiveMap, Primitive, Value } from '../../../liveobjects';
import { LiveCounterValueType } from './livecountervaluetype';
import { LiveMap, LiveMapObjectData, ObjectIdObjectData } from './livemap';
import { ObjectId } from './objectid';
import {
encodePartialObjectOperationForWire,
MapCreate,
ObjectData,
ObjectMessage,
ObjectOperation,
ObjectOperationAction,
ObjectsMapEntry,
ObjectsMapSemantics,
primitiveToObjectData,
} from './objectmessage';
import { RealtimeObject } from './realtimeobject';
/**
* A value type class that serves as a simple container for LiveMap data.
* Contains sufficient information for the client to produce a MAP_CREATE operation
* for the LiveMap object.
*
* Properties of this class are immutable after construction and the instance
* will be frozen to prevent mutation.
*
* Note: We do not deep freeze or deep copy the entries data for the following reasons:
* 1. It adds substantial complexity, especially for handling Buffer/ArrayBuffer values
* 2. Cross-platform buffer copying would require reimplementing BufferUtils logic
* to handle browser vs Node.js environments and check availability of Buffer/ArrayBuffer
* 3. The protection isn't critical - if users mutate the data after creating the value type,
* nothing breaks since we create separate live objects each time the value type is used
* 4. This behavior should be documented and it's the user's responsibility to understand
* how they mutate their data when working with value type classes
*/
export class LiveMapValueType<T extends Record<string, Value> = Record<string, Value>> implements PublicLiveMap<T> {
declare readonly [__livetype]: 'LiveMap'; // type-only, unique symbol to satisfy branded interfaces, no JS emitted
private readonly _livetype = 'LiveMap'; // use a runtime property to provide a reliable cross-bundle type identification instead of `instanceof` operator
private readonly _entries: T | undefined;
private constructor(entries: T | undefined) {
this._entries = entries;
Object.freeze(this);
}
static create<T extends Record<string, Value>>(
initialEntries?: T,
): PublicLiveMap<T extends Record<string, Value> ? T : {}> {
// We can't directly import the ErrorInfo class from the core library into the plugin (as this would bloat the plugin size),
// and, since we're in a user-facing static method, we can't expect a user to pass a client library instance, as this would make the API ugly.
// Since we can't use ErrorInfo here, we won't do any validation at this step; instead, validation will happen in the mutation methods
// when we try to create this object.
return new LiveMapValueType(initialEntries);
}
/**
* @internal
*/
static instanceof(value: unknown): value is LiveMapValueType {
return typeof value === 'object' && value !== null && (value as LiveMapValueType)._livetype === 'LiveMap';
}
/**
* @internal
*/
static async createMapCreateMessage(
realtimeObject: RealtimeObject,
value: LiveMapValueType,
): Promise<{ mapCreateMsg: ObjectMessage; nestedObjectsCreateMsgs: ObjectMessage[] }> {
const client = realtimeObject.getClient();
const entries = value._entries;
if (entries !== undefined && (entries === null || typeof entries !== 'object')) {
throw new client.ErrorInfo('Map entries should be a key-value object', 40003, 400);
}
Object.entries(entries ?? {}).forEach(([key, value]) => LiveMap.validateKeyValue(realtimeObject, key, value));
const { mapCreate, nestedObjectsCreateMsgs } = await LiveMapValueType._getMapCreate(realtimeObject, entries); // RTO11f14
const { mapCreate: encodedMapCreate } = encodePartialObjectOperationForWire(
{ mapCreate },
client,
client.Utils.Format.json,
); // RTO11f15a
const initialValueJSONString = JSON.stringify(encodedMapCreate); // RTO11f15b
const nonce = client.Utils.cheapRandStr(); // RTO11f6
const msTimestamp = await client.getTimestamp(true); // RTO11f7
// RTO11f8
const objectId = ObjectId.fromInitialValue(
client.Platform,
'map',
initialValueJSONString,
nonce,
msTimestamp,
).toString();
const mapCreateMsg = ObjectMessage.fromValues(
{
operation: {
action: ObjectOperationAction.MAP_CREATE, // RTO11f9
objectId, // RTO11f10
mapCreateWithObjectId: {
nonce, // RTO11f16
initialValue: initialValueJSONString, // RTO11f17
// RTO11f18 - retain the source MapCreate for local use (size calculation and apply-on-ACK)
_derivedFrom: mapCreate,
},
} as ObjectOperation<ObjectData>,
},
client.Utils,
client.MessageEncoding,
);
return {
mapCreateMsg,
nestedObjectsCreateMsgs,
};
}
private static async _getMapCreate(
realtimeObject: RealtimeObject,
entries?: Record<string, Value>,
): Promise<{
mapCreate: MapCreate<ObjectData>;
nestedObjectsCreateMsgs: ObjectMessage[];
}> {
const mapEntries: Record<string, ObjectsMapEntry<ObjectData>> = {}; // RTO11f14b - empty map by default
const nestedObjectsCreateMsgs: ObjectMessage[] = [];
// RTO11f14c
for (const [key, value] of Object.entries(entries ?? {})) {
let objectData: LiveMapObjectData;
if (LiveMapValueType.instanceof(value)) {
const { mapCreateMsg, nestedObjectsCreateMsgs: childNestedObjs } =
await LiveMapValueType.createMapCreateMessage(realtimeObject, value);
nestedObjectsCreateMsgs.push(...childNestedObjs, mapCreateMsg);
const typedObjectData: ObjectIdObjectData = { objectId: mapCreateMsg.operation?.objectId! };
objectData = typedObjectData;
} else if (LiveCounterValueType.instanceof(value)) {
const counterCreateMsg = await LiveCounterValueType.createCounterCreateMessage(realtimeObject, value);
nestedObjectsCreateMsgs.push(counterCreateMsg);
const typedObjectData: ObjectIdObjectData = { objectId: counterCreateMsg.operation?.objectId! };
objectData = typedObjectData;
} else {
// RTO11f14c1b, RTO11f14c1c, RTO11f14c1d, RTO11f14c1e, RTO11f14c1f - Handle primitive values
objectData = primitiveToObjectData(value as Primitive, realtimeObject.getClient());
}
// RTO11f14c1, RTO11f14c2
mapEntries[key] = {
data: objectData,
};
}
const mapCreate: MapCreate<ObjectData> = {
semantics: ObjectsMapSemantics.LWW, // RTO11f14a
entries: mapEntries, // RTO11f14b, RTO11f14c
};
return {
mapCreate,
nestedObjectsCreateMsgs,
};
}
}