@liveblocks/client 0.16.4 → 0.16.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +815 -1540
- package/index.mjs +778 -1295
- package/internal.d.ts +37 -5
- package/internal.js +152 -337
- package/internal.mjs +127 -299
- package/package.json +8 -7
- package/shared.d.ts +15 -3
- package/shared.js +1276 -2380
- package/shared.mjs +1183 -1887
package/index.mjs
CHANGED
|
@@ -1,1324 +1,807 @@
|
|
|
1
|
-
import { A as AbstractCrdt, r as remove, S as ServerMessageType, m as mergeStorageUpdates, W as WebsocketCloseCodes, C as ClientMessageType, i as isTokenValid, a as isSameNodeOrChildOf, L as LiveObject, g as getTreesDiffOperations, O as OpType, b as LiveList, p as parseJson, c as isJsonArray, d as compact, e as isJsonObject, f as deprecateIf } from
|
|
2
|
-
export { b as LiveList, h as LiveMap, L as LiveObject } from './shared.mjs';
|
|
1
|
+
import { A as AbstractCrdt, r as remove, S as ServerMessageType, m as mergeStorageUpdates, W as WebsocketCloseCodes, C as ClientMessageType, i as isTokenValid, a as isSameNodeOrChildOf, L as LiveObject, g as getTreesDiffOperations, O as OpType, b as LiveList, p as parseJson, c as isJsonArray, d as compact, e as isJsonObject, f as deprecateIf } from "./shared.mjs";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
8
|
-
purpose with or without fee is hereby granted.
|
|
9
|
-
|
|
10
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
11
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
12
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
13
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
14
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
15
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
16
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
17
|
-
***************************************************************************** */
|
|
18
|
-
|
|
19
|
-
function __rest(s, e) {
|
|
20
|
-
var t = {};
|
|
21
|
-
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
22
|
-
t[p] = s[p];
|
|
23
|
-
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
24
|
-
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
25
|
-
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
26
|
-
t[p[i]] = s[p[i]];
|
|
27
|
-
}
|
|
28
|
-
return t;
|
|
29
|
-
}
|
|
3
|
+
export { b as LiveList, h as LiveMap, L as LiveObject } from "./shared.mjs";
|
|
4
|
+
|
|
5
|
+
const BACKOFF_RETRY_DELAYS = [ 250, 500, 1e3, 2e3, 4e3, 8e3, 1e4 ], BACKOFF_RETRY_DELAYS_SLOW = [ 2e3, 3e4, 6e4, 3e5 ];
|
|
30
6
|
|
|
31
|
-
const BACKOFF_RETRY_DELAYS = [250, 500, 1000, 2000, 4000, 8000, 10000];
|
|
32
|
-
const BACKOFF_RETRY_DELAYS_SLOW = [2000, 30000, 60000, 300000];
|
|
33
|
-
const HEARTBEAT_INTERVAL = 30000;
|
|
34
|
-
// const WAKE_UP_CHECK_INTERVAL = 2000;
|
|
35
|
-
const PONG_TIMEOUT = 2000;
|
|
36
7
|
function isValidRoomEventType(value) {
|
|
37
|
-
|
|
38
|
-
value === "others" ||
|
|
39
|
-
value === "event" ||
|
|
40
|
-
value === "error" ||
|
|
41
|
-
value === "connection");
|
|
42
|
-
}
|
|
43
|
-
function makeIdFactory(connectionId) {
|
|
44
|
-
let count = 0;
|
|
45
|
-
return () => `${connectionId}:${count++}`;
|
|
8
|
+
return "my-presence" === value || "others" === value || "event" === value || "error" === value || "connection" === value;
|
|
46
9
|
}
|
|
10
|
+
|
|
47
11
|
function makeOthers(userMap) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
};
|
|
12
|
+
const users = Object.values(userMap).map((user => function(s, e) {
|
|
13
|
+
var t = {};
|
|
14
|
+
for (var p in s) Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0 && (t[p] = s[p]);
|
|
15
|
+
if (null != s && "function" == typeof Object.getOwnPropertySymbols) {
|
|
16
|
+
var i = 0;
|
|
17
|
+
for (p = Object.getOwnPropertySymbols(s); i < p.length; i++) e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]) && (t[p[i]] = s[p[i]]);
|
|
18
|
+
}
|
|
19
|
+
return t;
|
|
20
|
+
}(user, [ "_hasReceivedInitialPresence" ])));
|
|
21
|
+
return {
|
|
22
|
+
get count() {
|
|
23
|
+
return users.length;
|
|
24
|
+
},
|
|
25
|
+
[Symbol.iterator]: () => users[Symbol.iterator](),
|
|
26
|
+
map: callback => users.map(callback),
|
|
27
|
+
toArray: () => users
|
|
28
|
+
};
|
|
66
29
|
}
|
|
30
|
+
|
|
67
31
|
function makeStateMachine(state, context, mockedEffects) {
|
|
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
|
-
|
|
32
|
+
const effects = mockedEffects || {
|
|
33
|
+
authenticate(auth, createWebSocket) {
|
|
34
|
+
const token = state.token;
|
|
35
|
+
if (!token || !isTokenValid(token)) return auth(context.roomId).then((({token: token}) => {
|
|
36
|
+
if ("authenticating" !== state.connection.state) return;
|
|
37
|
+
authenticationSuccess(parseToken(token), createWebSocket(token)), state.token = token;
|
|
38
|
+
})).catch((er => function(error) {
|
|
39
|
+
"production" !== process.env.NODE_ENV && console.error("Call to authentication endpoint failed", error);
|
|
40
|
+
state.token = null, updateConnection({
|
|
41
|
+
state: "unavailable"
|
|
42
|
+
}), state.numberOfRetry++, state.timeoutHandles.reconnect = effects.scheduleReconnect(getRetryDelay());
|
|
43
|
+
}(er)));
|
|
44
|
+
authenticationSuccess(parseToken(token), createWebSocket(token));
|
|
45
|
+
},
|
|
46
|
+
send(messageOrMessages) {
|
|
47
|
+
if (null == state.socket) throw new Error("Can't send message if socket is null");
|
|
48
|
+
state.socket.send(JSON.stringify(messageOrMessages));
|
|
49
|
+
},
|
|
50
|
+
delayFlush: delay => setTimeout(tryFlushing, delay),
|
|
51
|
+
startHeartbeatInterval: () => setInterval(heartbeat, 3e4),
|
|
52
|
+
schedulePongTimeout: () => setTimeout(pongTimeout, 2e3),
|
|
53
|
+
scheduleReconnect: delay => setTimeout(connect, delay)
|
|
54
|
+
};
|
|
55
|
+
function genericSubscribe(callback) {
|
|
56
|
+
return state.listeners.storage.push(callback), () => remove(state.listeners.storage, callback);
|
|
57
|
+
}
|
|
58
|
+
function createOrUpdateRootFromMessage(message) {
|
|
59
|
+
if (0 === message.items.length) throw new Error("Internal error: cannot load storage without items");
|
|
60
|
+
state.root ? function(items) {
|
|
61
|
+
if (!state.root) return;
|
|
62
|
+
const currentItems = new Map;
|
|
63
|
+
state.items.forEach(((liveCrdt, id) => {
|
|
64
|
+
currentItems.set(id, liveCrdt._toSerializedCrdt());
|
|
65
|
+
}));
|
|
66
|
+
const ops = getTreesDiffOperations(currentItems, new Map(items));
|
|
67
|
+
notify(apply(ops, !1).updates);
|
|
68
|
+
}(message.items) : state.root = function(items) {
|
|
69
|
+
const [root, parentToChildren] = function(items) {
|
|
70
|
+
const parentToChildren = new Map;
|
|
71
|
+
let root = null;
|
|
72
|
+
for (const tuple of items) {
|
|
73
|
+
const parentId = tuple[1].parentId;
|
|
74
|
+
if (null == parentId) root = tuple; else {
|
|
75
|
+
const children = parentToChildren.get(parentId);
|
|
76
|
+
null != children ? children.push(tuple) : parentToChildren.set(parentId, [ tuple ]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (null == root) throw new Error("Root can't be null");
|
|
80
|
+
return [ root, parentToChildren ];
|
|
81
|
+
}(items);
|
|
82
|
+
return LiveObject._deserialize(root, parentToChildren, {
|
|
83
|
+
getItem: getItem,
|
|
84
|
+
addItem: addItem,
|
|
85
|
+
deleteItem: deleteItem,
|
|
86
|
+
generateId: generateId,
|
|
87
|
+
generateOpId: generateOpId,
|
|
88
|
+
dispatch: storageDispatch,
|
|
89
|
+
roomId: context.roomId
|
|
90
|
+
});
|
|
91
|
+
}(message.items);
|
|
92
|
+
for (const key in state.defaultStorageRoot) null == state.root.get(key) && state.root.set(key, state.defaultStorageRoot[key]);
|
|
93
|
+
}
|
|
94
|
+
function addItem(id, item) {
|
|
95
|
+
state.items.set(id, item);
|
|
96
|
+
}
|
|
97
|
+
function deleteItem(id) {
|
|
98
|
+
state.items.delete(id);
|
|
99
|
+
}
|
|
100
|
+
function getItem(id) {
|
|
101
|
+
return state.items.get(id);
|
|
102
|
+
}
|
|
103
|
+
function addToUndoStack(historyItem) {
|
|
104
|
+
state.undoStack.length >= 50 && state.undoStack.shift(), state.isHistoryPaused ? state.pausedHistory.unshift(...historyItem) : state.undoStack.push(historyItem);
|
|
105
|
+
}
|
|
106
|
+
function storageDispatch(ops, reverse, storageUpdates) {
|
|
107
|
+
state.isBatching ? (state.batch.ops.push(...ops), storageUpdates.forEach(((value, key) => {
|
|
108
|
+
state.batch.updates.storageUpdates.set(key, mergeStorageUpdates(state.batch.updates.storageUpdates.get(key), value));
|
|
109
|
+
})), state.batch.reverseOps.push(...reverse)) : (addToUndoStack(reverse), state.redoStack = [],
|
|
110
|
+
dispatch(ops), notify({
|
|
111
|
+
storageUpdates: storageUpdates
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
function notify({storageUpdates: storageUpdates = new Map, presence: presence = !1, others: otherEvents = []}) {
|
|
115
|
+
if (otherEvents.length > 0) {
|
|
116
|
+
state.others = makeOthers(state.users);
|
|
117
|
+
for (const event of otherEvents) for (const listener of state.listeners.others) listener(state.others, event);
|
|
118
|
+
}
|
|
119
|
+
if (presence) for (const listener of state.listeners["my-presence"]) listener(state.me);
|
|
120
|
+
if (storageUpdates.size > 0) for (const subscriber of state.listeners.storage) subscriber(Array.from(storageUpdates.values()));
|
|
121
|
+
}
|
|
122
|
+
function getConnectionId() {
|
|
123
|
+
if ("open" === state.connection.state || "connecting" === state.connection.state) return state.connection.id;
|
|
124
|
+
if (null !== state.lastConnectionId) return state.lastConnectionId;
|
|
125
|
+
throw new Error("Internal. Tried to get connection id but connection was never open");
|
|
126
|
+
}
|
|
127
|
+
function generateId() {
|
|
128
|
+
return `${getConnectionId()}:${state.clock++}`;
|
|
129
|
+
}
|
|
130
|
+
function generateOpId() {
|
|
131
|
+
return `${getConnectionId()}:${state.opClock++}`;
|
|
132
|
+
}
|
|
133
|
+
function apply(item, isLocal) {
|
|
134
|
+
var _a;
|
|
135
|
+
const result = {
|
|
136
|
+
reverse: [],
|
|
137
|
+
updates: {
|
|
138
|
+
storageUpdates: new Map,
|
|
139
|
+
presence: !1
|
|
140
|
+
}
|
|
141
|
+
}, createdNodeIds = new Set;
|
|
142
|
+
for (const op of item) if ("presence" === op.type) {
|
|
143
|
+
const reverse = {
|
|
144
|
+
type: "presence",
|
|
145
|
+
data: {}
|
|
146
|
+
};
|
|
147
|
+
for (const key in op.data) reverse.data[key] = state.me[key];
|
|
148
|
+
if (state.me = Object.assign(Object.assign({}, state.me), op.data), null == state.buffer.presence) state.buffer.presence = op.data; else for (const key in op.data) state.buffer.presence[key] = op.data[key];
|
|
149
|
+
result.reverse.unshift(reverse), result.updates.presence = !0;
|
|
150
|
+
} else {
|
|
151
|
+
isLocal && !op.opId && (op.opId = generateOpId());
|
|
152
|
+
const applyOpResult = applyOp(op, isLocal);
|
|
153
|
+
if (applyOpResult.modified) {
|
|
154
|
+
const parentId = null === (_a = applyOpResult.modified.node._parent) || void 0 === _a ? void 0 : _a._id;
|
|
155
|
+
createdNodeIds.has(parentId) || (result.updates.storageUpdates.set(applyOpResult.modified.node._id, mergeStorageUpdates(result.updates.storageUpdates.get(applyOpResult.modified.node._id), applyOpResult.modified)),
|
|
156
|
+
result.reverse.unshift(...applyOpResult.reverse)), op.type !== OpType.CreateList && op.type !== OpType.CreateMap && op.type !== OpType.CreateObject || createdNodeIds.add(applyOpResult.modified.node._id);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
function applyOp(op, isLocal) {
|
|
162
|
+
switch (op.opId && state.offlineOperations.delete(op.opId), op.type) {
|
|
163
|
+
case OpType.DeleteObjectKey:
|
|
164
|
+
case OpType.UpdateObject:
|
|
165
|
+
case OpType.DeleteCrdt:
|
|
166
|
+
{
|
|
167
|
+
const item = state.items.get(op.id);
|
|
168
|
+
return null == item ? {
|
|
169
|
+
modified: !1
|
|
170
|
+
} : item._apply(op, isLocal);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case OpType.SetParentKey:
|
|
174
|
+
{
|
|
175
|
+
const item = state.items.get(op.id);
|
|
176
|
+
if (null == item) return {
|
|
177
|
+
modified: !1
|
|
108
178
|
};
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const cb = (updates) => {
|
|
115
|
-
const relatedUpdates = [];
|
|
116
|
-
for (const update of updates) {
|
|
117
|
-
if ((options === null || options === void 0 ? void 0 : options.isDeep) && isSameNodeOrChildOf(update.node, crdt)) {
|
|
118
|
-
relatedUpdates.push(update);
|
|
119
|
-
}
|
|
120
|
-
else if (update.node._id === crdt._id) {
|
|
121
|
-
innerCallback(update.node);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if ((options === null || options === void 0 ? void 0 : options.isDeep) && relatedUpdates.length > 0) {
|
|
125
|
-
innerCallback(relatedUpdates);
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
return genericSubscribe(cb);
|
|
129
|
-
}
|
|
130
|
-
function createOrUpdateRootFromMessage(message) {
|
|
131
|
-
if (message.items.length === 0) {
|
|
132
|
-
throw new Error("Internal error: cannot load storage without items");
|
|
133
|
-
}
|
|
134
|
-
if (state.root) {
|
|
135
|
-
updateRoot(message.items);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
state.root = load(message.items);
|
|
139
|
-
}
|
|
140
|
-
for (const key in state.defaultStorageRoot) {
|
|
141
|
-
if (state.root.get(key) == null) {
|
|
142
|
-
state.root.set(key, state.defaultStorageRoot[key]);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function buildRootAndParentToChildren(items) {
|
|
147
|
-
const parentToChildren = new Map();
|
|
148
|
-
let root = null;
|
|
149
|
-
for (const tuple of items) {
|
|
150
|
-
const parentId = tuple[1].parentId;
|
|
151
|
-
if (parentId == null) {
|
|
152
|
-
root = tuple;
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
const children = parentToChildren.get(parentId);
|
|
156
|
-
if (children != null) {
|
|
157
|
-
children.push(tuple);
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
parentToChildren.set(parentId, [tuple]);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (root == null) {
|
|
165
|
-
throw new Error("Root can't be null");
|
|
166
|
-
}
|
|
167
|
-
return [root, parentToChildren];
|
|
168
|
-
}
|
|
169
|
-
function updateRoot(items) {
|
|
170
|
-
if (!state.root) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const currentItems = new Map();
|
|
174
|
-
state.items.forEach((liveCrdt, id) => {
|
|
175
|
-
currentItems.set(id, liveCrdt._toSerializedCrdt());
|
|
176
|
-
});
|
|
177
|
-
// Get operations that represent the diff between 2 states.
|
|
178
|
-
const ops = getTreesDiffOperations(currentItems, new Map(items));
|
|
179
|
-
const result = apply(ops, false);
|
|
180
|
-
notify(result.updates);
|
|
181
|
-
}
|
|
182
|
-
function load(items) {
|
|
183
|
-
const [root, parentToChildren] = buildRootAndParentToChildren(items);
|
|
184
|
-
return LiveObject._deserialize(root, parentToChildren, {
|
|
185
|
-
getItem,
|
|
186
|
-
addItem,
|
|
187
|
-
deleteItem,
|
|
188
|
-
generateId,
|
|
189
|
-
generateOpId,
|
|
190
|
-
dispatch: storageDispatch,
|
|
191
|
-
roomId: context.roomId,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
function addItem(id, item) {
|
|
195
|
-
state.items.set(id, item);
|
|
196
|
-
}
|
|
197
|
-
function deleteItem(id) {
|
|
198
|
-
state.items.delete(id);
|
|
199
|
-
}
|
|
200
|
-
function getItem(id) {
|
|
201
|
-
return state.items.get(id);
|
|
202
|
-
}
|
|
203
|
-
function addToUndoStack(historyItem) {
|
|
204
|
-
// If undo stack is too large, we remove the older item
|
|
205
|
-
if (state.undoStack.length >= 50) {
|
|
206
|
-
state.undoStack.shift();
|
|
207
|
-
}
|
|
208
|
-
if (state.isHistoryPaused) {
|
|
209
|
-
state.pausedHistory.unshift(...historyItem);
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
state.undoStack.push(historyItem);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
function storageDispatch(ops, reverse, storageUpdates) {
|
|
216
|
-
if (state.isBatching) {
|
|
217
|
-
state.batch.ops.push(...ops);
|
|
218
|
-
storageUpdates.forEach((value, key) => {
|
|
219
|
-
state.batch.updates.storageUpdates.set(key, mergeStorageUpdates(state.batch.updates.storageUpdates.get(key), // FIXME
|
|
220
|
-
value));
|
|
221
|
-
});
|
|
222
|
-
state.batch.reverseOps.push(...reverse);
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
addToUndoStack(reverse);
|
|
226
|
-
state.redoStack = [];
|
|
227
|
-
dispatch(ops);
|
|
228
|
-
notify({ storageUpdates: storageUpdates });
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
function notify({ storageUpdates = new Map(), presence = false, others: otherEvents = [], }) {
|
|
232
|
-
if (otherEvents.length > 0) {
|
|
233
|
-
state.others = makeOthers(state.users);
|
|
234
|
-
for (const event of otherEvents) {
|
|
235
|
-
for (const listener of state.listeners.others) {
|
|
236
|
-
listener(state.others, event);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
if (presence) {
|
|
241
|
-
for (const listener of state.listeners["my-presence"]) {
|
|
242
|
-
listener(state.me);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
if (storageUpdates.size > 0) {
|
|
246
|
-
for (const subscriber of state.listeners.storage) {
|
|
247
|
-
subscriber(Array.from(storageUpdates.values()));
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
function getConnectionId() {
|
|
252
|
-
if (state.connection.state === "open" ||
|
|
253
|
-
state.connection.state === "connecting") {
|
|
254
|
-
return state.connection.id;
|
|
255
|
-
}
|
|
256
|
-
else if (state.lastConnectionId !== null) {
|
|
257
|
-
return state.lastConnectionId;
|
|
258
|
-
}
|
|
259
|
-
throw new Error("Internal. Tried to get connection id but connection was never open");
|
|
260
|
-
}
|
|
261
|
-
function generateId() {
|
|
262
|
-
return `${getConnectionId()}:${state.clock++}`;
|
|
263
|
-
}
|
|
264
|
-
function generateOpId() {
|
|
265
|
-
return `${getConnectionId()}:${state.opClock++}`;
|
|
266
|
-
}
|
|
267
|
-
function apply(item, isLocal) {
|
|
268
|
-
var _a;
|
|
269
|
-
const result = {
|
|
270
|
-
reverse: [],
|
|
271
|
-
updates: {
|
|
272
|
-
storageUpdates: new Map(),
|
|
273
|
-
presence: false,
|
|
274
|
-
},
|
|
275
|
-
};
|
|
276
|
-
const createdNodeIds = new Set();
|
|
277
|
-
for (const op of item) {
|
|
278
|
-
if (op.type === "presence") {
|
|
279
|
-
const reverse = {
|
|
280
|
-
type: "presence",
|
|
281
|
-
data: {},
|
|
282
|
-
};
|
|
283
|
-
for (const key in op.data) {
|
|
284
|
-
reverse.data[key] = state.me[key];
|
|
285
|
-
}
|
|
286
|
-
state.me = Object.assign(Object.assign({}, state.me), op.data);
|
|
287
|
-
if (state.buffer.presence == null) {
|
|
288
|
-
state.buffer.presence = op.data;
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
for (const key in op.data) {
|
|
292
|
-
state.buffer.presence[key] = op.data[key];
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
result.reverse.unshift(reverse);
|
|
296
|
-
result.updates.presence = true;
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
// Ops applied after undo/redo don't have an opId.
|
|
300
|
-
if (isLocal && !op.opId) {
|
|
301
|
-
op.opId = generateOpId();
|
|
302
|
-
}
|
|
303
|
-
const applyOpResult = applyOp(op, isLocal);
|
|
304
|
-
if (applyOpResult.modified) {
|
|
305
|
-
const parentId = (_a = applyOpResult.modified.node._parent) === null || _a === void 0 ? void 0 : _a._id;
|
|
306
|
-
// If the parent was created in the same batch, we don't want to notify
|
|
307
|
-
// storage updates for the children.
|
|
308
|
-
if (!createdNodeIds.has(parentId)) {
|
|
309
|
-
result.updates.storageUpdates.set(applyOpResult.modified.node._id, mergeStorageUpdates(result.updates.storageUpdates.get(applyOpResult.modified.node._id), // FIXME
|
|
310
|
-
applyOpResult.modified));
|
|
311
|
-
result.reverse.unshift(...applyOpResult.reverse);
|
|
312
|
-
}
|
|
313
|
-
if (op.type === OpType.CreateList ||
|
|
314
|
-
op.type === OpType.CreateMap ||
|
|
315
|
-
op.type === OpType.CreateObject) {
|
|
316
|
-
createdNodeIds.add(applyOpResult.modified.node._id);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return result;
|
|
322
|
-
}
|
|
323
|
-
function applyOp(op, isLocal) {
|
|
324
|
-
if (op.opId) {
|
|
325
|
-
state.offlineOperations.delete(op.opId);
|
|
326
|
-
}
|
|
327
|
-
switch (op.type) {
|
|
328
|
-
case OpType.DeleteObjectKey:
|
|
329
|
-
case OpType.UpdateObject:
|
|
330
|
-
case OpType.DeleteCrdt: {
|
|
331
|
-
const item = state.items.get(op.id);
|
|
332
|
-
if (item == null) {
|
|
333
|
-
return { modified: false };
|
|
334
|
-
}
|
|
335
|
-
return item._apply(op, isLocal);
|
|
336
|
-
}
|
|
337
|
-
case OpType.SetParentKey: {
|
|
338
|
-
const item = state.items.get(op.id);
|
|
339
|
-
if (item == null) {
|
|
340
|
-
return { modified: false };
|
|
341
|
-
}
|
|
342
|
-
if (item._parent instanceof LiveList) {
|
|
343
|
-
const previousKey = item._parentKey;
|
|
344
|
-
if (previousKey === op.parentKey) {
|
|
345
|
-
return { modified: false };
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
return item._parent._setChildKey(op.parentKey, item, previousKey);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return { modified: false };
|
|
352
|
-
}
|
|
353
|
-
case OpType.CreateObject:
|
|
354
|
-
case OpType.CreateList:
|
|
355
|
-
case OpType.CreateMap:
|
|
356
|
-
case OpType.CreateRegister: {
|
|
357
|
-
const parent = state.items.get(op.parentId);
|
|
358
|
-
if (parent == null) {
|
|
359
|
-
return { modified: false };
|
|
360
|
-
}
|
|
361
|
-
return parent._attachChild(op, isLocal);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
function subscribe(firstParam, listener, options) {
|
|
366
|
-
if (firstParam instanceof AbstractCrdt) {
|
|
367
|
-
return crdtSubscribe(firstParam, listener, options);
|
|
368
|
-
}
|
|
369
|
-
else if (typeof firstParam === "function") {
|
|
370
|
-
return genericSubscribe(firstParam);
|
|
371
|
-
}
|
|
372
|
-
else if (!isValidRoomEventType(firstParam)) {
|
|
373
|
-
throw new Error(`"${firstParam}" is not a valid event name`);
|
|
374
|
-
}
|
|
375
|
-
state.listeners[firstParam].push(listener);
|
|
376
|
-
return () => {
|
|
377
|
-
const callbacks = state.listeners[firstParam];
|
|
378
|
-
remove(callbacks, listener);
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
function unsubscribe(event, callback) {
|
|
382
|
-
console.warn(`unsubscribe is depreacted and will be removed in a future version.
|
|
383
|
-
use the callback returned by subscribe instead.
|
|
384
|
-
See v0.13 release notes for more information.
|
|
385
|
-
`);
|
|
386
|
-
if (!isValidRoomEventType(event)) {
|
|
387
|
-
throw new Error(`"${event}" is not a valid event name`);
|
|
388
|
-
}
|
|
389
|
-
const callbacks = state.listeners[event];
|
|
390
|
-
remove(callbacks, callback);
|
|
391
|
-
}
|
|
392
|
-
function getConnectionState() {
|
|
393
|
-
return state.connection.state;
|
|
394
|
-
}
|
|
395
|
-
function getSelf() {
|
|
396
|
-
return state.connection.state === "open" ||
|
|
397
|
-
state.connection.state === "connecting"
|
|
398
|
-
? {
|
|
399
|
-
connectionId: state.connection.id,
|
|
400
|
-
id: state.connection.userId,
|
|
401
|
-
info: state.connection.userInfo,
|
|
402
|
-
presence: getPresence(),
|
|
403
|
-
}
|
|
404
|
-
: null;
|
|
405
|
-
}
|
|
406
|
-
function connect() {
|
|
407
|
-
if (state.connection.state !== "closed" &&
|
|
408
|
-
state.connection.state !== "unavailable") {
|
|
409
|
-
return null;
|
|
410
|
-
}
|
|
411
|
-
const auth = prepareAuthEndpoint(context.authentication, context.fetchPolyfill);
|
|
412
|
-
const createWebSocket = prepareCreateWebSocket(context.liveblocksServer, context.WebSocketPolyfill);
|
|
413
|
-
updateConnection({ state: "authenticating" });
|
|
414
|
-
effects.authenticate(auth, createWebSocket);
|
|
415
|
-
}
|
|
416
|
-
function updatePresence(overrides, options) {
|
|
417
|
-
const oldValues = {};
|
|
418
|
-
if (state.buffer.presence == null) {
|
|
419
|
-
state.buffer.presence = {};
|
|
420
|
-
}
|
|
421
|
-
for (const key in overrides) {
|
|
422
|
-
state.buffer.presence[key] = overrides[key];
|
|
423
|
-
oldValues[key] = state.me[key];
|
|
424
|
-
}
|
|
425
|
-
state.me = Object.assign(Object.assign({}, state.me), overrides);
|
|
426
|
-
if (state.isBatching) {
|
|
427
|
-
if (options === null || options === void 0 ? void 0 : options.addToHistory) {
|
|
428
|
-
state.batch.reverseOps.push({ type: "presence", data: oldValues });
|
|
429
|
-
}
|
|
430
|
-
state.batch.updates.presence = true;
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
tryFlushing();
|
|
434
|
-
if (options === null || options === void 0 ? void 0 : options.addToHistory) {
|
|
435
|
-
addToUndoStack([{ type: "presence", data: oldValues }]);
|
|
436
|
-
}
|
|
437
|
-
notify({ presence: true });
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
function authenticationSuccess(token, socket) {
|
|
441
|
-
socket.addEventListener("message", onMessage);
|
|
442
|
-
socket.addEventListener("open", onOpen);
|
|
443
|
-
socket.addEventListener("close", onClose);
|
|
444
|
-
socket.addEventListener("error", onError);
|
|
445
|
-
updateConnection({
|
|
446
|
-
state: "connecting",
|
|
447
|
-
id: token.actor,
|
|
448
|
-
userInfo: token.info,
|
|
449
|
-
userId: token.id,
|
|
450
|
-
});
|
|
451
|
-
state.idFactory = makeIdFactory(token.actor);
|
|
452
|
-
state.socket = socket;
|
|
453
|
-
}
|
|
454
|
-
function authenticationFailure(error) {
|
|
455
|
-
if (process.env.NODE_ENV !== "production") {
|
|
456
|
-
console.error("Call to authentication endpoint failed", error);
|
|
457
|
-
}
|
|
458
|
-
state.token = null;
|
|
459
|
-
updateConnection({ state: "unavailable" });
|
|
460
|
-
state.numberOfRetry++;
|
|
461
|
-
state.timeoutHandles.reconnect = effects.scheduleReconnect(getRetryDelay());
|
|
462
|
-
}
|
|
463
|
-
function onVisibilityChange(visibilityState) {
|
|
464
|
-
if (visibilityState === "visible" && state.connection.state === "open") {
|
|
465
|
-
heartbeat();
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
function onUpdatePresenceMessage(message) {
|
|
469
|
-
const user = state.users[message.actor];
|
|
470
|
-
// If the other user initial presence hasn't been received yet, we discard the presence update.
|
|
471
|
-
// The initial presence update message contains the property "targetActor".
|
|
472
|
-
if (message.targetActor === undefined &&
|
|
473
|
-
user != null &&
|
|
474
|
-
!user._hasReceivedInitialPresence) {
|
|
475
|
-
return undefined;
|
|
476
|
-
}
|
|
477
|
-
if (user == null) {
|
|
478
|
-
state.users[message.actor] = {
|
|
479
|
-
connectionId: message.actor,
|
|
480
|
-
presence: message.data,
|
|
481
|
-
_hasReceivedInitialPresence: true,
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
else {
|
|
485
|
-
state.users[message.actor] = {
|
|
486
|
-
id: user.id,
|
|
487
|
-
info: user.info,
|
|
488
|
-
connectionId: message.actor,
|
|
489
|
-
presence: Object.assign(Object.assign({}, user.presence), message.data),
|
|
490
|
-
_hasReceivedInitialPresence: true,
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
return {
|
|
494
|
-
type: "update",
|
|
495
|
-
updates: message.data,
|
|
496
|
-
user: state.users[message.actor],
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
function onUserLeftMessage(message) {
|
|
500
|
-
const userLeftMessage = message;
|
|
501
|
-
const user = state.users[userLeftMessage.actor];
|
|
502
|
-
if (user) {
|
|
503
|
-
delete state.users[userLeftMessage.actor];
|
|
504
|
-
return { type: "leave", user };
|
|
505
|
-
}
|
|
506
|
-
return null;
|
|
507
|
-
}
|
|
508
|
-
function onRoomStateMessage(message) {
|
|
509
|
-
const newUsers = {};
|
|
510
|
-
for (const key in message.users) {
|
|
511
|
-
const connectionId = Number.parseInt(key);
|
|
512
|
-
const user = message.users[key];
|
|
513
|
-
newUsers[connectionId] = {
|
|
514
|
-
connectionId,
|
|
515
|
-
info: user.info,
|
|
516
|
-
id: user.id,
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
state.users = newUsers;
|
|
520
|
-
return { type: "reset" };
|
|
521
|
-
}
|
|
522
|
-
function onNavigatorOnline() {
|
|
523
|
-
if (state.connection.state === "unavailable") {
|
|
524
|
-
reconnect();
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
function onEvent(message) {
|
|
528
|
-
for (const listener of state.listeners.event) {
|
|
529
|
-
listener({ connectionId: message.actor, event: message.event });
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
function onUserJoinedMessage(message) {
|
|
533
|
-
state.users[message.actor] = {
|
|
534
|
-
connectionId: message.actor,
|
|
535
|
-
info: message.info,
|
|
536
|
-
id: message.id,
|
|
537
|
-
_hasReceivedInitialPresence: true,
|
|
538
|
-
};
|
|
539
|
-
if (state.me) {
|
|
540
|
-
// Send current presence to new user
|
|
541
|
-
// TODO: Consider storing it on the backend
|
|
542
|
-
state.buffer.messages.push({
|
|
543
|
-
type: ClientMessageType.UpdatePresence,
|
|
544
|
-
data: state.me,
|
|
545
|
-
targetActor: message.actor,
|
|
546
|
-
});
|
|
547
|
-
tryFlushing();
|
|
548
|
-
}
|
|
549
|
-
return { type: "enter", user: state.users[message.actor] };
|
|
550
|
-
}
|
|
551
|
-
function parseServerMessage(data) {
|
|
552
|
-
if (!isJsonObject(data)) {
|
|
553
|
-
return null;
|
|
554
|
-
}
|
|
555
|
-
return data;
|
|
556
|
-
// ^^^^^^^^^^^^^^^^ FIXME: Properly validate incoming external data instead!
|
|
557
|
-
}
|
|
558
|
-
function parseServerMessages(text) {
|
|
559
|
-
const data = parseJson(text);
|
|
560
|
-
if (data === undefined) {
|
|
561
|
-
return null;
|
|
562
|
-
}
|
|
563
|
-
else if (isJsonArray(data)) {
|
|
564
|
-
return compact(data.map((item) => parseServerMessage(item)));
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
return compact([parseServerMessage(data)]);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
function onMessage(event) {
|
|
571
|
-
if (event.data === "pong") {
|
|
572
|
-
clearTimeout(state.timeoutHandles.pongTimeout);
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
const messages = parseServerMessages(event.data);
|
|
576
|
-
if (messages === null || messages.length === 0) {
|
|
577
|
-
// Unknown incoming message... ignore it
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
const updates = {
|
|
581
|
-
storageUpdates: new Map(),
|
|
582
|
-
others: [],
|
|
583
|
-
};
|
|
584
|
-
for (const message of messages) {
|
|
585
|
-
switch (message.type) {
|
|
586
|
-
case ServerMessageType.UserJoined: {
|
|
587
|
-
updates.others.push(onUserJoinedMessage(message));
|
|
588
|
-
break;
|
|
589
|
-
}
|
|
590
|
-
case ServerMessageType.UpdatePresence: {
|
|
591
|
-
const othersPresenceUpdate = onUpdatePresenceMessage(message);
|
|
592
|
-
if (othersPresenceUpdate) {
|
|
593
|
-
updates.others.push(othersPresenceUpdate);
|
|
594
|
-
}
|
|
595
|
-
break;
|
|
596
|
-
}
|
|
597
|
-
case ServerMessageType.Event: {
|
|
598
|
-
onEvent(message);
|
|
599
|
-
break;
|
|
600
|
-
}
|
|
601
|
-
case ServerMessageType.UserLeft: {
|
|
602
|
-
const event = onUserLeftMessage(message);
|
|
603
|
-
if (event) {
|
|
604
|
-
updates.others.push(event);
|
|
605
|
-
}
|
|
606
|
-
break;
|
|
607
|
-
}
|
|
608
|
-
case ServerMessageType.RoomState: {
|
|
609
|
-
updates.others.push(onRoomStateMessage(message));
|
|
610
|
-
break;
|
|
611
|
-
}
|
|
612
|
-
case ServerMessageType.InitialStorageState: {
|
|
613
|
-
// createOrUpdateRootFromMessage function could add ops to offlineOperations.
|
|
614
|
-
// Client shouldn't resend these ops as part of the offline ops sending after reconnect.
|
|
615
|
-
const offlineOps = new Map(state.offlineOperations);
|
|
616
|
-
createOrUpdateRootFromMessage(message);
|
|
617
|
-
applyAndSendOfflineOps(offlineOps);
|
|
618
|
-
_getInitialStateResolver === null || _getInitialStateResolver === void 0 ? void 0 : _getInitialStateResolver();
|
|
619
|
-
break;
|
|
620
|
-
}
|
|
621
|
-
case ServerMessageType.UpdateStorage: {
|
|
622
|
-
const applyResult = apply(message.ops, false);
|
|
623
|
-
applyResult.updates.storageUpdates.forEach((value, key) => {
|
|
624
|
-
updates.storageUpdates.set(key, mergeStorageUpdates(updates.storageUpdates.get(key), // FIXME
|
|
625
|
-
value));
|
|
626
|
-
});
|
|
627
|
-
break;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
notify(updates);
|
|
632
|
-
}
|
|
633
|
-
// function onWakeUp() {
|
|
634
|
-
// // Sometimes, the browser can put the webpage on pause (computer is on sleep mode for example)
|
|
635
|
-
// // The client will not know that the server has probably close the connection even if the readyState is Open
|
|
636
|
-
// // One way to detect this kind of pause is to ensure that a setInterval is not taking more than the delay it was configured with
|
|
637
|
-
// if (state.connection.state === "open") {
|
|
638
|
-
// log("Try to reconnect after laptop wake up");
|
|
639
|
-
// reconnect();
|
|
640
|
-
// }
|
|
641
|
-
// }
|
|
642
|
-
function onClose(event) {
|
|
643
|
-
state.socket = null;
|
|
644
|
-
clearTimeout(state.timeoutHandles.pongTimeout);
|
|
645
|
-
clearInterval(state.intervalHandles.heartbeat);
|
|
646
|
-
if (state.timeoutHandles.flush) {
|
|
647
|
-
clearTimeout(state.timeoutHandles.flush);
|
|
648
|
-
}
|
|
649
|
-
clearTimeout(state.timeoutHandles.reconnect);
|
|
650
|
-
state.users = {};
|
|
651
|
-
notify({ others: [{ type: "reset" }] });
|
|
652
|
-
if (event.code >= 4000 && event.code <= 4100) {
|
|
653
|
-
updateConnection({ state: "failed" });
|
|
654
|
-
const error = new LiveblocksError(event.reason, event.code);
|
|
655
|
-
for (const listener of state.listeners.error) {
|
|
656
|
-
listener(error);
|
|
657
|
-
}
|
|
658
|
-
const delay = getRetryDelay(true);
|
|
659
|
-
state.numberOfRetry++;
|
|
660
|
-
if (process.env.NODE_ENV !== "production") {
|
|
661
|
-
console.error(`Connection to Liveblocks websocket server closed. Reason: ${error.message} (code: ${error.code}). Retrying in ${delay}ms.`);
|
|
662
|
-
}
|
|
663
|
-
updateConnection({ state: "unavailable" });
|
|
664
|
-
state.timeoutHandles.reconnect = effects.scheduleReconnect(delay);
|
|
665
|
-
}
|
|
666
|
-
else if (event.code === WebsocketCloseCodes.CLOSE_WITHOUT_RETRY) {
|
|
667
|
-
updateConnection({ state: "closed" });
|
|
668
|
-
}
|
|
669
|
-
else {
|
|
670
|
-
const delay = getRetryDelay();
|
|
671
|
-
state.numberOfRetry++;
|
|
672
|
-
if (process.env.NODE_ENV !== "production") {
|
|
673
|
-
console.warn(`Connection to Liveblocks websocket server closed (code: ${event.code}). Retrying in ${delay}ms.`);
|
|
674
|
-
}
|
|
675
|
-
updateConnection({ state: "unavailable" });
|
|
676
|
-
state.timeoutHandles.reconnect = effects.scheduleReconnect(delay);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
function updateConnection(connection) {
|
|
680
|
-
state.connection = connection;
|
|
681
|
-
for (const listener of state.listeners.connection) {
|
|
682
|
-
listener(connection.state);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
function getRetryDelay(slow = false) {
|
|
686
|
-
if (slow) {
|
|
687
|
-
return BACKOFF_RETRY_DELAYS_SLOW[state.numberOfRetry < BACKOFF_RETRY_DELAYS_SLOW.length
|
|
688
|
-
? state.numberOfRetry
|
|
689
|
-
: BACKOFF_RETRY_DELAYS_SLOW.length - 1];
|
|
690
|
-
}
|
|
691
|
-
return BACKOFF_RETRY_DELAYS[state.numberOfRetry < BACKOFF_RETRY_DELAYS.length
|
|
692
|
-
? state.numberOfRetry
|
|
693
|
-
: BACKOFF_RETRY_DELAYS.length - 1];
|
|
694
|
-
}
|
|
695
|
-
function onError() { }
|
|
696
|
-
function onOpen() {
|
|
697
|
-
clearInterval(state.intervalHandles.heartbeat);
|
|
698
|
-
state.intervalHandles.heartbeat = effects.startHeartbeatInterval();
|
|
699
|
-
if (state.connection.state === "connecting") {
|
|
700
|
-
updateConnection(Object.assign(Object.assign({}, state.connection), { state: "open" }));
|
|
701
|
-
state.numberOfRetry = 0;
|
|
702
|
-
// Re-broadcast the user presence during a reconnect.
|
|
703
|
-
if (state.lastConnectionId !== undefined) {
|
|
704
|
-
state.buffer.presence = state.me;
|
|
705
|
-
tryFlushing();
|
|
706
|
-
}
|
|
707
|
-
state.lastConnectionId = state.connection.id;
|
|
708
|
-
if (state.root) {
|
|
709
|
-
state.buffer.messages.push({ type: ClientMessageType.FetchStorage });
|
|
710
|
-
}
|
|
711
|
-
tryFlushing();
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
function heartbeat() {
|
|
715
|
-
if (state.socket == null) {
|
|
716
|
-
// Should never happen, because we clear the pong timeout when the connection is dropped explictly
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
clearTimeout(state.timeoutHandles.pongTimeout);
|
|
720
|
-
state.timeoutHandles.pongTimeout = effects.schedulePongTimeout();
|
|
721
|
-
if (state.socket.readyState === state.socket.OPEN) {
|
|
722
|
-
state.socket.send("ping");
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
function pongTimeout() {
|
|
726
|
-
reconnect();
|
|
727
|
-
}
|
|
728
|
-
function reconnect() {
|
|
729
|
-
if (state.socket) {
|
|
730
|
-
state.socket.removeEventListener("open", onOpen);
|
|
731
|
-
state.socket.removeEventListener("message", onMessage);
|
|
732
|
-
state.socket.removeEventListener("close", onClose);
|
|
733
|
-
state.socket.removeEventListener("error", onError);
|
|
734
|
-
state.socket.close();
|
|
735
|
-
state.socket = null;
|
|
736
|
-
}
|
|
737
|
-
updateConnection({ state: "unavailable" });
|
|
738
|
-
clearTimeout(state.timeoutHandles.pongTimeout);
|
|
739
|
-
if (state.timeoutHandles.flush) {
|
|
740
|
-
clearTimeout(state.timeoutHandles.flush);
|
|
741
|
-
}
|
|
742
|
-
clearTimeout(state.timeoutHandles.reconnect);
|
|
743
|
-
clearInterval(state.intervalHandles.heartbeat);
|
|
744
|
-
connect();
|
|
745
|
-
}
|
|
746
|
-
function applyAndSendOfflineOps(offlineOps) {
|
|
747
|
-
if (offlineOps.size === 0) {
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
const messages = [];
|
|
751
|
-
const ops = Array.from(offlineOps.values());
|
|
752
|
-
const result = apply(ops, true);
|
|
753
|
-
messages.push({
|
|
754
|
-
type: ClientMessageType.UpdateStorage,
|
|
755
|
-
ops: ops,
|
|
756
|
-
});
|
|
757
|
-
notify(result.updates);
|
|
758
|
-
effects.send(messages);
|
|
759
|
-
}
|
|
760
|
-
function tryFlushing() {
|
|
761
|
-
const storageOps = state.buffer.storageOperations;
|
|
762
|
-
if (storageOps.length > 0) {
|
|
763
|
-
storageOps.forEach((op) => {
|
|
764
|
-
state.offlineOperations.set(op.opId, op);
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
if (state.socket == null || state.socket.readyState !== state.socket.OPEN) {
|
|
768
|
-
state.buffer.storageOperations = [];
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
const now = Date.now();
|
|
772
|
-
const elapsedTime = now - state.lastFlushTime;
|
|
773
|
-
if (elapsedTime > context.throttleDelay) {
|
|
774
|
-
const messages = flushDataToMessages(state);
|
|
775
|
-
if (messages.length === 0) {
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
effects.send(messages);
|
|
779
|
-
state.buffer = {
|
|
780
|
-
messages: [],
|
|
781
|
-
storageOperations: [],
|
|
782
|
-
presence: null,
|
|
783
|
-
};
|
|
784
|
-
state.lastFlushTime = now;
|
|
785
|
-
}
|
|
786
|
-
else {
|
|
787
|
-
if (state.timeoutHandles.flush != null) {
|
|
788
|
-
clearTimeout(state.timeoutHandles.flush);
|
|
789
|
-
}
|
|
790
|
-
state.timeoutHandles.flush = effects.delayFlush(context.throttleDelay - (now - state.lastFlushTime));
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
function flushDataToMessages(state) {
|
|
794
|
-
const messages = [];
|
|
795
|
-
if (state.buffer.presence) {
|
|
796
|
-
messages.push({
|
|
797
|
-
type: ClientMessageType.UpdatePresence,
|
|
798
|
-
data: state.buffer.presence,
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
for (const event of state.buffer.messages) {
|
|
802
|
-
messages.push(event);
|
|
803
|
-
}
|
|
804
|
-
if (state.buffer.storageOperations.length > 0) {
|
|
805
|
-
messages.push({
|
|
806
|
-
type: ClientMessageType.UpdateStorage,
|
|
807
|
-
ops: state.buffer.storageOperations,
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
return messages;
|
|
811
|
-
}
|
|
812
|
-
function disconnect() {
|
|
813
|
-
if (state.socket) {
|
|
814
|
-
state.socket.removeEventListener("open", onOpen);
|
|
815
|
-
state.socket.removeEventListener("message", onMessage);
|
|
816
|
-
state.socket.removeEventListener("close", onClose);
|
|
817
|
-
state.socket.removeEventListener("error", onError);
|
|
818
|
-
state.socket.close();
|
|
819
|
-
state.socket = null;
|
|
820
|
-
}
|
|
821
|
-
updateConnection({ state: "closed" });
|
|
822
|
-
if (state.timeoutHandles.flush) {
|
|
823
|
-
clearTimeout(state.timeoutHandles.flush);
|
|
824
|
-
}
|
|
825
|
-
clearTimeout(state.timeoutHandles.reconnect);
|
|
826
|
-
clearTimeout(state.timeoutHandles.pongTimeout);
|
|
827
|
-
clearInterval(state.intervalHandles.heartbeat);
|
|
828
|
-
state.users = {};
|
|
829
|
-
notify({ others: [{ type: "reset" }] });
|
|
830
|
-
clearListeners();
|
|
831
|
-
}
|
|
832
|
-
function clearListeners() {
|
|
833
|
-
for (const key in state.listeners) {
|
|
834
|
-
state.listeners[key] = [];
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
function getPresence() {
|
|
838
|
-
return state.me;
|
|
839
|
-
}
|
|
840
|
-
function getOthers() {
|
|
841
|
-
return state.others;
|
|
842
|
-
}
|
|
843
|
-
function broadcastEvent(event, options = {
|
|
844
|
-
shouldQueueEventIfNotReady: false,
|
|
845
|
-
}) {
|
|
846
|
-
if (state.socket == null && options.shouldQueueEventIfNotReady == false) {
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
state.buffer.messages.push({
|
|
850
|
-
type: ClientMessageType.ClientEvent,
|
|
851
|
-
event,
|
|
852
|
-
});
|
|
853
|
-
tryFlushing();
|
|
179
|
+
if (item._parent instanceof LiveList) {
|
|
180
|
+
const previousKey = item._parentKey;
|
|
181
|
+
return previousKey === op.parentKey ? {
|
|
182
|
+
modified: !1
|
|
183
|
+
} : item._parent._setChildKey(op.parentKey, item, previousKey);
|
|
854
184
|
}
|
|
855
|
-
function dispatch(ops) {
|
|
856
|
-
state.buffer.storageOperations.push(...ops);
|
|
857
|
-
tryFlushing();
|
|
858
|
-
}
|
|
859
|
-
let _getInitialStatePromise = null;
|
|
860
|
-
let _getInitialStateResolver = null;
|
|
861
|
-
function getStorage() {
|
|
862
|
-
if (state.root) {
|
|
863
|
-
return new Promise((resolve) => resolve({
|
|
864
|
-
root: state.root,
|
|
865
|
-
}));
|
|
866
|
-
}
|
|
867
|
-
if (_getInitialStatePromise == null) {
|
|
868
|
-
state.buffer.messages.push({ type: ClientMessageType.FetchStorage });
|
|
869
|
-
tryFlushing();
|
|
870
|
-
_getInitialStatePromise = new Promise((resolve) => (_getInitialStateResolver = resolve));
|
|
871
|
-
}
|
|
872
|
-
return _getInitialStatePromise.then(() => {
|
|
873
|
-
return {
|
|
874
|
-
root: state.root,
|
|
875
|
-
};
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
function undo() {
|
|
879
|
-
if (state.isBatching) {
|
|
880
|
-
throw new Error("undo is not allowed during a batch");
|
|
881
|
-
}
|
|
882
|
-
const historyItem = state.undoStack.pop();
|
|
883
|
-
if (historyItem == null) {
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
state.isHistoryPaused = false;
|
|
887
|
-
const result = apply(historyItem, true);
|
|
888
|
-
notify(result.updates);
|
|
889
|
-
state.redoStack.push(result.reverse);
|
|
890
|
-
for (const op of historyItem) {
|
|
891
|
-
if (op.type !== "presence") {
|
|
892
|
-
state.buffer.storageOperations.push(op);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
tryFlushing();
|
|
896
|
-
}
|
|
897
|
-
function redo() {
|
|
898
|
-
if (state.isBatching) {
|
|
899
|
-
throw new Error("redo is not allowed during a batch");
|
|
900
|
-
}
|
|
901
|
-
const historyItem = state.redoStack.pop();
|
|
902
|
-
if (historyItem == null) {
|
|
903
|
-
return;
|
|
904
|
-
}
|
|
905
|
-
state.isHistoryPaused = false;
|
|
906
|
-
const result = apply(historyItem, true);
|
|
907
|
-
notify(result.updates);
|
|
908
|
-
state.undoStack.push(result.reverse);
|
|
909
|
-
for (const op of historyItem) {
|
|
910
|
-
if (op.type !== "presence") {
|
|
911
|
-
state.buffer.storageOperations.push(op);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
tryFlushing();
|
|
915
|
-
}
|
|
916
|
-
function batch(callback) {
|
|
917
|
-
if (state.isBatching) {
|
|
918
|
-
throw new Error("batch should not be called during a batch");
|
|
919
|
-
}
|
|
920
|
-
state.isBatching = true;
|
|
921
|
-
try {
|
|
922
|
-
callback();
|
|
923
|
-
}
|
|
924
|
-
finally {
|
|
925
|
-
state.isBatching = false;
|
|
926
|
-
if (state.batch.reverseOps.length > 0) {
|
|
927
|
-
addToUndoStack(state.batch.reverseOps);
|
|
928
|
-
}
|
|
929
|
-
if (state.batch.ops.length > 0) {
|
|
930
|
-
// Only clear the redo stack if something has changed during a batch
|
|
931
|
-
// Clear the redo stack because batch is always called from a local operation
|
|
932
|
-
state.redoStack = [];
|
|
933
|
-
}
|
|
934
|
-
if (state.batch.ops.length > 0) {
|
|
935
|
-
dispatch(state.batch.ops);
|
|
936
|
-
}
|
|
937
|
-
notify(state.batch.updates);
|
|
938
|
-
state.batch = {
|
|
939
|
-
ops: [],
|
|
940
|
-
reverseOps: [],
|
|
941
|
-
updates: {
|
|
942
|
-
others: [],
|
|
943
|
-
storageUpdates: new Map(),
|
|
944
|
-
presence: false,
|
|
945
|
-
},
|
|
946
|
-
};
|
|
947
|
-
tryFlushing();
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
function pauseHistory() {
|
|
951
|
-
state.pausedHistory = [];
|
|
952
|
-
state.isHistoryPaused = true;
|
|
953
|
-
}
|
|
954
|
-
function resumeHistory() {
|
|
955
|
-
state.isHistoryPaused = false;
|
|
956
|
-
if (state.pausedHistory.length > 0) {
|
|
957
|
-
addToUndoStack(state.pausedHistory);
|
|
958
|
-
}
|
|
959
|
-
state.pausedHistory = [];
|
|
960
|
-
}
|
|
961
|
-
function simulateSocketClose() {
|
|
962
|
-
if (state.socket) {
|
|
963
|
-
state.socket.close();
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
function simulateSendCloseEvent(event) {
|
|
967
|
-
if (state.socket) {
|
|
968
|
-
onClose(event);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
return {
|
|
972
|
-
// Internal
|
|
973
|
-
onClose,
|
|
974
|
-
onMessage,
|
|
975
|
-
authenticationSuccess,
|
|
976
|
-
heartbeat,
|
|
977
|
-
onNavigatorOnline,
|
|
978
|
-
// Internal dev tools
|
|
979
|
-
simulateSocketClose,
|
|
980
|
-
simulateSendCloseEvent,
|
|
981
|
-
// onWakeUp,
|
|
982
|
-
onVisibilityChange,
|
|
983
|
-
getUndoStack: () => state.undoStack,
|
|
984
|
-
getItemsCount: () => state.items.size,
|
|
985
|
-
// Core
|
|
986
|
-
connect,
|
|
987
|
-
disconnect,
|
|
988
|
-
subscribe,
|
|
989
|
-
unsubscribe,
|
|
990
|
-
// Presence
|
|
991
|
-
updatePresence,
|
|
992
|
-
broadcastEvent,
|
|
993
|
-
batch,
|
|
994
|
-
undo,
|
|
995
|
-
redo,
|
|
996
|
-
pauseHistory,
|
|
997
|
-
resumeHistory,
|
|
998
|
-
getStorage,
|
|
999
|
-
selectors: {
|
|
1000
|
-
// Core
|
|
1001
|
-
getConnectionState,
|
|
1002
|
-
getSelf,
|
|
1003
|
-
// Presence
|
|
1004
|
-
getPresence,
|
|
1005
|
-
getOthers,
|
|
1006
|
-
},
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
function defaultState(initialPresence, initialStorage) {
|
|
1010
185
|
return {
|
|
1011
|
-
|
|
1012
|
-
token: null,
|
|
1013
|
-
lastConnectionId: null,
|
|
1014
|
-
socket: null,
|
|
1015
|
-
listeners: {
|
|
1016
|
-
event: [],
|
|
1017
|
-
others: [],
|
|
1018
|
-
"my-presence": [],
|
|
1019
|
-
error: [],
|
|
1020
|
-
connection: [],
|
|
1021
|
-
storage: [],
|
|
1022
|
-
},
|
|
1023
|
-
numberOfRetry: 0,
|
|
1024
|
-
lastFlushTime: 0,
|
|
1025
|
-
timeoutHandles: {
|
|
1026
|
-
flush: null,
|
|
1027
|
-
reconnect: 0,
|
|
1028
|
-
pongTimeout: 0,
|
|
1029
|
-
},
|
|
1030
|
-
buffer: {
|
|
1031
|
-
presence: initialPresence == null ? {} : initialPresence,
|
|
1032
|
-
messages: [],
|
|
1033
|
-
storageOperations: [],
|
|
1034
|
-
},
|
|
1035
|
-
intervalHandles: {
|
|
1036
|
-
heartbeat: 0,
|
|
1037
|
-
},
|
|
1038
|
-
me: initialPresence == null ? {} : initialPresence,
|
|
1039
|
-
users: {},
|
|
1040
|
-
others: makeOthers({}),
|
|
1041
|
-
defaultStorageRoot: initialStorage,
|
|
1042
|
-
idFactory: null,
|
|
1043
|
-
// Storage
|
|
1044
|
-
clock: 0,
|
|
1045
|
-
opClock: 0,
|
|
1046
|
-
items: new Map(),
|
|
1047
|
-
root: undefined,
|
|
1048
|
-
undoStack: [],
|
|
1049
|
-
redoStack: [],
|
|
1050
|
-
isHistoryPaused: false,
|
|
1051
|
-
pausedHistory: [],
|
|
1052
|
-
isBatching: false,
|
|
1053
|
-
batch: {
|
|
1054
|
-
ops: [],
|
|
1055
|
-
updates: {
|
|
1056
|
-
storageUpdates: new Map(),
|
|
1057
|
-
presence: false,
|
|
1058
|
-
others: [],
|
|
1059
|
-
},
|
|
1060
|
-
reverseOps: [],
|
|
1061
|
-
},
|
|
1062
|
-
offlineOperations: new Map(),
|
|
186
|
+
modified: !1
|
|
1063
187
|
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
case OpType.CreateObject:
|
|
191
|
+
case OpType.CreateList:
|
|
192
|
+
case OpType.CreateMap:
|
|
193
|
+
case OpType.CreateRegister:
|
|
194
|
+
{
|
|
195
|
+
const parent = state.items.get(op.parentId);
|
|
196
|
+
return null == parent ? {
|
|
197
|
+
modified: !1
|
|
198
|
+
} : parent._attachChild(op, isLocal);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function connect() {
|
|
203
|
+
if ("closed" !== state.connection.state && "unavailable" !== state.connection.state) return null;
|
|
204
|
+
const auth = function(authentication, fetchPolyfill) {
|
|
205
|
+
if ("public" === authentication.type) {
|
|
206
|
+
if ("undefined" == typeof window && null == fetchPolyfill) throw new Error("To use Liveblocks client in a non-dom environment with a publicApiKey, you need to provide a fetch polyfill.");
|
|
207
|
+
return room => fetchAuthEndpoint(fetchPolyfill || fetch, authentication.url, {
|
|
208
|
+
room: room,
|
|
209
|
+
publicApiKey: authentication.publicApiKey
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if ("private" === authentication.type) {
|
|
213
|
+
if ("undefined" == typeof window && null == fetchPolyfill) throw new Error("To use Liveblocks client in a non-dom environment with a url as auth endpoint, you need to provide a fetch polyfill.");
|
|
214
|
+
return room => fetchAuthEndpoint(fetchPolyfill || fetch, authentication.url, {
|
|
215
|
+
room: room
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if ("custom" === authentication.type) return authentication.callback;
|
|
219
|
+
throw new Error("Internal error. Unexpected authentication type");
|
|
220
|
+
}(context.authentication, context.fetchPolyfill), createWebSocket = function(liveblocksServer, WebSocketPolyfill) {
|
|
221
|
+
if ("undefined" == typeof window && null == WebSocketPolyfill) throw new Error("To use Liveblocks client in a non-dom environment, you need to provide a WebSocket polyfill.");
|
|
222
|
+
const ws = WebSocketPolyfill || WebSocket;
|
|
223
|
+
return token => new ws(`${liveblocksServer}/?token=${token}`);
|
|
224
|
+
}(context.liveblocksServer, context.WebSocketPolyfill);
|
|
225
|
+
updateConnection({
|
|
226
|
+
state: "authenticating"
|
|
227
|
+
}), effects.authenticate(auth, createWebSocket);
|
|
228
|
+
}
|
|
229
|
+
function authenticationSuccess(token, socket) {
|
|
230
|
+
socket.addEventListener("message", onMessage), socket.addEventListener("open", onOpen),
|
|
231
|
+
socket.addEventListener("close", onClose), socket.addEventListener("error", onError),
|
|
232
|
+
updateConnection({
|
|
233
|
+
state: "connecting",
|
|
234
|
+
id: token.actor,
|
|
235
|
+
userInfo: token.info,
|
|
236
|
+
userId: token.id
|
|
237
|
+
}), state.idFactory = function(connectionId) {
|
|
238
|
+
let count = 0;
|
|
239
|
+
return () => `${connectionId}:${count++}`;
|
|
240
|
+
}(token.actor), state.socket = socket;
|
|
241
|
+
}
|
|
242
|
+
function onUpdatePresenceMessage(message) {
|
|
243
|
+
const user = state.users[message.actor];
|
|
244
|
+
if (void 0 !== message.targetActor || null == user || user._hasReceivedInitialPresence) return state.users[message.actor] = null == user ? {
|
|
245
|
+
connectionId: message.actor,
|
|
246
|
+
presence: message.data,
|
|
247
|
+
_hasReceivedInitialPresence: !0
|
|
248
|
+
} : {
|
|
249
|
+
id: user.id,
|
|
250
|
+
info: user.info,
|
|
251
|
+
connectionId: message.actor,
|
|
252
|
+
presence: Object.assign(Object.assign({}, user.presence), message.data),
|
|
253
|
+
_hasReceivedInitialPresence: !0
|
|
254
|
+
}, {
|
|
255
|
+
type: "update",
|
|
256
|
+
updates: message.data,
|
|
257
|
+
user: state.users[message.actor]
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function onUserLeftMessage(message) {
|
|
261
|
+
const userLeftMessage = message, user = state.users[userLeftMessage.actor];
|
|
262
|
+
return user ? (delete state.users[userLeftMessage.actor], {
|
|
263
|
+
type: "leave",
|
|
264
|
+
user: user
|
|
265
|
+
}) : null;
|
|
266
|
+
}
|
|
267
|
+
function onRoomStateMessage(message) {
|
|
268
|
+
const newUsers = {};
|
|
269
|
+
for (const key in message.users) {
|
|
270
|
+
const connectionId = Number.parseInt(key), user = message.users[key];
|
|
271
|
+
newUsers[connectionId] = {
|
|
272
|
+
connectionId: connectionId,
|
|
273
|
+
info: user.info,
|
|
274
|
+
id: user.id
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return state.users = newUsers, {
|
|
278
|
+
type: "reset"
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function onEvent(message) {
|
|
282
|
+
for (const listener of state.listeners.event) listener({
|
|
283
|
+
connectionId: message.actor,
|
|
284
|
+
event: message.event
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function onUserJoinedMessage(message) {
|
|
288
|
+
return state.users[message.actor] = {
|
|
289
|
+
connectionId: message.actor,
|
|
290
|
+
info: message.info,
|
|
291
|
+
id: message.id,
|
|
292
|
+
_hasReceivedInitialPresence: !0
|
|
293
|
+
}, state.me && (state.buffer.messages.push({
|
|
294
|
+
type: ClientMessageType.UpdatePresence,
|
|
295
|
+
data: state.me,
|
|
296
|
+
targetActor: message.actor
|
|
297
|
+
}), tryFlushing()), {
|
|
298
|
+
type: "enter",
|
|
299
|
+
user: state.users[message.actor]
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function parseServerMessage(data) {
|
|
303
|
+
return isJsonObject(data) ? data : null;
|
|
304
|
+
}
|
|
305
|
+
function onMessage(event) {
|
|
306
|
+
if ("pong" === event.data) return void clearTimeout(state.timeoutHandles.pongTimeout);
|
|
307
|
+
const messages = function(text) {
|
|
308
|
+
const data = parseJson(text);
|
|
309
|
+
return void 0 === data ? null : isJsonArray(data) ? compact(data.map((item => parseServerMessage(item)))) : compact([ parseServerMessage(data) ]);
|
|
310
|
+
}(event.data);
|
|
311
|
+
if (null === messages || 0 === messages.length) return;
|
|
312
|
+
const updates = {
|
|
313
|
+
storageUpdates: new Map,
|
|
314
|
+
others: []
|
|
315
|
+
};
|
|
316
|
+
for (const message of messages) switch (message.type) {
|
|
317
|
+
case ServerMessageType.UserJoined:
|
|
318
|
+
updates.others.push(onUserJoinedMessage(message));
|
|
319
|
+
break;
|
|
320
|
+
|
|
321
|
+
case ServerMessageType.UpdatePresence:
|
|
322
|
+
{
|
|
323
|
+
const othersPresenceUpdate = onUpdatePresenceMessage(message);
|
|
324
|
+
othersPresenceUpdate && updates.others.push(othersPresenceUpdate);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
case ServerMessageType.Event:
|
|
329
|
+
onEvent(message);
|
|
330
|
+
break;
|
|
331
|
+
|
|
332
|
+
case ServerMessageType.UserLeft:
|
|
333
|
+
{
|
|
334
|
+
const event = onUserLeftMessage(message);
|
|
335
|
+
event && updates.others.push(event);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
case ServerMessageType.RoomState:
|
|
340
|
+
updates.others.push(onRoomStateMessage(message));
|
|
341
|
+
break;
|
|
342
|
+
|
|
343
|
+
case ServerMessageType.InitialStorageState:
|
|
344
|
+
{
|
|
345
|
+
const offlineOps = new Map(state.offlineOperations);
|
|
346
|
+
createOrUpdateRootFromMessage(message), applyAndSendOfflineOps(offlineOps), null == _getInitialStateResolver || _getInitialStateResolver();
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
case ServerMessageType.UpdateStorage:
|
|
351
|
+
apply(message.ops, !1).updates.storageUpdates.forEach(((value, key) => {
|
|
352
|
+
updates.storageUpdates.set(key, mergeStorageUpdates(updates.storageUpdates.get(key), value));
|
|
353
|
+
}));
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
notify(updates);
|
|
357
|
+
}
|
|
358
|
+
function onClose(event) {
|
|
359
|
+
if (state.socket = null, clearTimeout(state.timeoutHandles.pongTimeout), clearInterval(state.intervalHandles.heartbeat),
|
|
360
|
+
state.timeoutHandles.flush && clearTimeout(state.timeoutHandles.flush), clearTimeout(state.timeoutHandles.reconnect),
|
|
361
|
+
state.users = {}, notify({
|
|
362
|
+
others: [ {
|
|
363
|
+
type: "reset"
|
|
364
|
+
} ]
|
|
365
|
+
}), event.code >= 4e3 && event.code <= 4100) {
|
|
366
|
+
updateConnection({
|
|
367
|
+
state: "failed"
|
|
368
|
+
});
|
|
369
|
+
const error = new LiveblocksError(event.reason, event.code);
|
|
370
|
+
for (const listener of state.listeners.error) listener(error);
|
|
371
|
+
const delay = getRetryDelay(!0);
|
|
372
|
+
state.numberOfRetry++, "production" !== process.env.NODE_ENV && console.error(`Connection to Liveblocks websocket server closed. Reason: ${error.message} (code: ${error.code}). Retrying in ${delay}ms.`),
|
|
373
|
+
updateConnection({
|
|
374
|
+
state: "unavailable"
|
|
375
|
+
}), state.timeoutHandles.reconnect = effects.scheduleReconnect(delay);
|
|
376
|
+
} else if (event.code === WebsocketCloseCodes.CLOSE_WITHOUT_RETRY) updateConnection({
|
|
377
|
+
state: "closed"
|
|
378
|
+
}); else {
|
|
379
|
+
const delay = getRetryDelay();
|
|
380
|
+
state.numberOfRetry++, "production" !== process.env.NODE_ENV && console.warn(`Connection to Liveblocks websocket server closed (code: ${event.code}). Retrying in ${delay}ms.`),
|
|
381
|
+
updateConnection({
|
|
382
|
+
state: "unavailable"
|
|
383
|
+
}), state.timeoutHandles.reconnect = effects.scheduleReconnect(delay);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function updateConnection(connection) {
|
|
387
|
+
state.connection = connection;
|
|
388
|
+
for (const listener of state.listeners.connection) listener(connection.state);
|
|
389
|
+
}
|
|
390
|
+
function getRetryDelay(slow = !1) {
|
|
391
|
+
return slow ? BACKOFF_RETRY_DELAYS_SLOW[state.numberOfRetry < BACKOFF_RETRY_DELAYS_SLOW.length ? state.numberOfRetry : BACKOFF_RETRY_DELAYS_SLOW.length - 1] : BACKOFF_RETRY_DELAYS[state.numberOfRetry < BACKOFF_RETRY_DELAYS.length ? state.numberOfRetry : BACKOFF_RETRY_DELAYS.length - 1];
|
|
392
|
+
}
|
|
393
|
+
function onError() {}
|
|
394
|
+
function onOpen() {
|
|
395
|
+
clearInterval(state.intervalHandles.heartbeat), state.intervalHandles.heartbeat = effects.startHeartbeatInterval(),
|
|
396
|
+
"connecting" === state.connection.state && (updateConnection(Object.assign(Object.assign({}, state.connection), {
|
|
397
|
+
state: "open"
|
|
398
|
+
})), state.numberOfRetry = 0, void 0 !== state.lastConnectionId && (state.buffer.presence = state.me,
|
|
399
|
+
tryFlushing()), state.lastConnectionId = state.connection.id, state.root && state.buffer.messages.push({
|
|
400
|
+
type: ClientMessageType.FetchStorage
|
|
401
|
+
}), tryFlushing());
|
|
402
|
+
}
|
|
403
|
+
function heartbeat() {
|
|
404
|
+
null != state.socket && (clearTimeout(state.timeoutHandles.pongTimeout), state.timeoutHandles.pongTimeout = effects.schedulePongTimeout(),
|
|
405
|
+
state.socket.readyState === state.socket.OPEN && state.socket.send("ping"));
|
|
406
|
+
}
|
|
407
|
+
function pongTimeout() {
|
|
408
|
+
reconnect();
|
|
409
|
+
}
|
|
410
|
+
function reconnect() {
|
|
411
|
+
state.socket && (state.socket.removeEventListener("open", onOpen), state.socket.removeEventListener("message", onMessage),
|
|
412
|
+
state.socket.removeEventListener("close", onClose), state.socket.removeEventListener("error", onError),
|
|
413
|
+
state.socket.close(), state.socket = null), updateConnection({
|
|
414
|
+
state: "unavailable"
|
|
415
|
+
}), clearTimeout(state.timeoutHandles.pongTimeout), state.timeoutHandles.flush && clearTimeout(state.timeoutHandles.flush),
|
|
416
|
+
clearTimeout(state.timeoutHandles.reconnect), clearInterval(state.intervalHandles.heartbeat),
|
|
417
|
+
connect();
|
|
418
|
+
}
|
|
419
|
+
function applyAndSendOfflineOps(offlineOps) {
|
|
420
|
+
if (0 === offlineOps.size) return;
|
|
421
|
+
const messages = [], ops = Array.from(offlineOps.values()), result = apply(ops, !0);
|
|
422
|
+
messages.push({
|
|
423
|
+
type: ClientMessageType.UpdateStorage,
|
|
424
|
+
ops: ops
|
|
425
|
+
}), notify(result.updates), effects.send(messages);
|
|
426
|
+
}
|
|
427
|
+
function tryFlushing() {
|
|
428
|
+
const storageOps = state.buffer.storageOperations;
|
|
429
|
+
if (storageOps.length > 0 && storageOps.forEach((op => {
|
|
430
|
+
state.offlineOperations.set(op.opId, op);
|
|
431
|
+
})), null == state.socket || state.socket.readyState !== state.socket.OPEN) return void (state.buffer.storageOperations = []);
|
|
432
|
+
const now = Date.now();
|
|
433
|
+
if (now - state.lastFlushTime > context.throttleDelay) {
|
|
434
|
+
const messages = function(state) {
|
|
435
|
+
const messages = [];
|
|
436
|
+
state.buffer.presence && messages.push({
|
|
437
|
+
type: ClientMessageType.UpdatePresence,
|
|
438
|
+
data: state.buffer.presence
|
|
439
|
+
});
|
|
440
|
+
for (const event of state.buffer.messages) messages.push(event);
|
|
441
|
+
state.buffer.storageOperations.length > 0 && messages.push({
|
|
442
|
+
type: ClientMessageType.UpdateStorage,
|
|
443
|
+
ops: state.buffer.storageOperations
|
|
444
|
+
});
|
|
445
|
+
return messages;
|
|
446
|
+
}(state);
|
|
447
|
+
if (0 === messages.length) return;
|
|
448
|
+
effects.send(messages), state.buffer = {
|
|
449
|
+
messages: [],
|
|
450
|
+
storageOperations: [],
|
|
451
|
+
presence: null
|
|
452
|
+
}, state.lastFlushTime = now;
|
|
453
|
+
} else null != state.timeoutHandles.flush && clearTimeout(state.timeoutHandles.flush),
|
|
454
|
+
state.timeoutHandles.flush = effects.delayFlush(context.throttleDelay - (now - state.lastFlushTime));
|
|
455
|
+
}
|
|
456
|
+
function getPresence() {
|
|
457
|
+
return state.me;
|
|
458
|
+
}
|
|
459
|
+
function dispatch(ops) {
|
|
460
|
+
state.buffer.storageOperations.push(...ops), tryFlushing();
|
|
461
|
+
}
|
|
462
|
+
let _getInitialStatePromise = null, _getInitialStateResolver = null;
|
|
463
|
+
return {
|
|
464
|
+
onClose: onClose,
|
|
465
|
+
onMessage: onMessage,
|
|
466
|
+
authenticationSuccess: authenticationSuccess,
|
|
467
|
+
heartbeat: heartbeat,
|
|
468
|
+
onNavigatorOnline: function() {
|
|
469
|
+
"unavailable" === state.connection.state && reconnect();
|
|
470
|
+
},
|
|
471
|
+
simulateSocketClose: function() {
|
|
472
|
+
state.socket && state.socket.close();
|
|
473
|
+
},
|
|
474
|
+
simulateSendCloseEvent: function(event) {
|
|
475
|
+
state.socket && onClose(event);
|
|
476
|
+
},
|
|
477
|
+
onVisibilityChange: function(visibilityState) {
|
|
478
|
+
"visible" === visibilityState && "open" === state.connection.state && heartbeat();
|
|
479
|
+
},
|
|
480
|
+
getUndoStack: () => state.undoStack,
|
|
481
|
+
getItemsCount: () => state.items.size,
|
|
482
|
+
connect: connect,
|
|
483
|
+
disconnect: function() {
|
|
484
|
+
state.socket && (state.socket.removeEventListener("open", onOpen), state.socket.removeEventListener("message", onMessage),
|
|
485
|
+
state.socket.removeEventListener("close", onClose), state.socket.removeEventListener("error", onError),
|
|
486
|
+
state.socket.close(), state.socket = null), updateConnection({
|
|
487
|
+
state: "closed"
|
|
488
|
+
}), state.timeoutHandles.flush && clearTimeout(state.timeoutHandles.flush), clearTimeout(state.timeoutHandles.reconnect),
|
|
489
|
+
clearTimeout(state.timeoutHandles.pongTimeout), clearInterval(state.intervalHandles.heartbeat),
|
|
490
|
+
state.users = {}, notify({
|
|
491
|
+
others: [ {
|
|
492
|
+
type: "reset"
|
|
493
|
+
} ]
|
|
494
|
+
}), function() {
|
|
495
|
+
for (const key in state.listeners) state.listeners[key] = [];
|
|
496
|
+
}();
|
|
497
|
+
},
|
|
498
|
+
subscribe: function(firstParam, listener, options) {
|
|
499
|
+
if (firstParam instanceof AbstractCrdt) return function(crdt, innerCallback, options) {
|
|
500
|
+
return genericSubscribe((updates => {
|
|
501
|
+
const relatedUpdates = [];
|
|
502
|
+
for (const update of updates) (null == options ? void 0 : options.isDeep) && isSameNodeOrChildOf(update.node, crdt) ? relatedUpdates.push(update) : update.node._id === crdt._id && innerCallback(update.node);
|
|
503
|
+
(null == options ? void 0 : options.isDeep) && relatedUpdates.length > 0 && innerCallback(relatedUpdates);
|
|
504
|
+
}));
|
|
505
|
+
}(firstParam, listener, options);
|
|
506
|
+
if ("function" == typeof firstParam) return genericSubscribe(firstParam);
|
|
507
|
+
if (!isValidRoomEventType(firstParam)) throw new Error(`"${firstParam}" is not a valid event name`);
|
|
508
|
+
return state.listeners[firstParam].push(listener), () => {
|
|
509
|
+
const callbacks = state.listeners[firstParam];
|
|
510
|
+
remove(callbacks, listener);
|
|
511
|
+
};
|
|
512
|
+
},
|
|
513
|
+
unsubscribe: function(event, callback) {
|
|
514
|
+
if (console.warn("unsubscribe is depreacted and will be removed in a future version.\nuse the callback returned by subscribe instead.\nSee v0.13 release notes for more information.\n"),
|
|
515
|
+
!isValidRoomEventType(event)) throw new Error(`"${event}" is not a valid event name`);
|
|
516
|
+
const callbacks = state.listeners[event];
|
|
517
|
+
remove(callbacks, callback);
|
|
518
|
+
},
|
|
519
|
+
updatePresence: function(overrides, options) {
|
|
520
|
+
const oldValues = {};
|
|
521
|
+
null == state.buffer.presence && (state.buffer.presence = {});
|
|
522
|
+
for (const key in overrides) state.buffer.presence[key] = overrides[key], oldValues[key] = state.me[key];
|
|
523
|
+
state.me = Object.assign(Object.assign({}, state.me), overrides), state.isBatching ? ((null == options ? void 0 : options.addToHistory) && state.batch.reverseOps.push({
|
|
524
|
+
type: "presence",
|
|
525
|
+
data: oldValues
|
|
526
|
+
}), state.batch.updates.presence = !0) : (tryFlushing(), (null == options ? void 0 : options.addToHistory) && addToUndoStack([ {
|
|
527
|
+
type: "presence",
|
|
528
|
+
data: oldValues
|
|
529
|
+
} ]), notify({
|
|
530
|
+
presence: !0
|
|
531
|
+
}));
|
|
532
|
+
},
|
|
533
|
+
broadcastEvent: function(event, options = {
|
|
534
|
+
shouldQueueEventIfNotReady: !1
|
|
535
|
+
}) {
|
|
536
|
+
null == state.socket && 0 == options.shouldQueueEventIfNotReady || (state.buffer.messages.push({
|
|
537
|
+
type: ClientMessageType.ClientEvent,
|
|
538
|
+
event: event
|
|
539
|
+
}), tryFlushing());
|
|
540
|
+
},
|
|
541
|
+
batch: function(callback) {
|
|
542
|
+
if (state.isBatching) throw new Error("batch should not be called during a batch");
|
|
543
|
+
state.isBatching = !0;
|
|
544
|
+
try {
|
|
545
|
+
callback();
|
|
546
|
+
} finally {
|
|
547
|
+
state.isBatching = !1, state.batch.reverseOps.length > 0 && addToUndoStack(state.batch.reverseOps),
|
|
548
|
+
state.batch.ops.length > 0 && (state.redoStack = []), state.batch.ops.length > 0 && dispatch(state.batch.ops),
|
|
549
|
+
notify(state.batch.updates), state.batch = {
|
|
550
|
+
ops: [],
|
|
551
|
+
reverseOps: [],
|
|
552
|
+
updates: {
|
|
553
|
+
others: [],
|
|
554
|
+
storageUpdates: new Map,
|
|
555
|
+
presence: !1
|
|
556
|
+
}
|
|
557
|
+
}, tryFlushing();
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
undo: function() {
|
|
561
|
+
if (state.isBatching) throw new Error("undo is not allowed during a batch");
|
|
562
|
+
const historyItem = state.undoStack.pop();
|
|
563
|
+
if (null == historyItem) return;
|
|
564
|
+
state.isHistoryPaused = !1;
|
|
565
|
+
const result = apply(historyItem, !0);
|
|
566
|
+
notify(result.updates), state.redoStack.push(result.reverse);
|
|
567
|
+
for (const op of historyItem) "presence" !== op.type && state.buffer.storageOperations.push(op);
|
|
568
|
+
tryFlushing();
|
|
569
|
+
},
|
|
570
|
+
redo: function() {
|
|
571
|
+
if (state.isBatching) throw new Error("redo is not allowed during a batch");
|
|
572
|
+
const historyItem = state.redoStack.pop();
|
|
573
|
+
if (null == historyItem) return;
|
|
574
|
+
state.isHistoryPaused = !1;
|
|
575
|
+
const result = apply(historyItem, !0);
|
|
576
|
+
notify(result.updates), state.undoStack.push(result.reverse);
|
|
577
|
+
for (const op of historyItem) "presence" !== op.type && state.buffer.storageOperations.push(op);
|
|
578
|
+
tryFlushing();
|
|
579
|
+
},
|
|
580
|
+
pauseHistory: function() {
|
|
581
|
+
state.pausedHistory = [], state.isHistoryPaused = !0;
|
|
582
|
+
},
|
|
583
|
+
resumeHistory: function() {
|
|
584
|
+
state.isHistoryPaused = !1, state.pausedHistory.length > 0 && addToUndoStack(state.pausedHistory),
|
|
585
|
+
state.pausedHistory = [];
|
|
586
|
+
},
|
|
587
|
+
getStorage: function() {
|
|
588
|
+
return state.root ? new Promise((resolve => resolve({
|
|
589
|
+
root: state.root
|
|
590
|
+
}))) : (null == _getInitialStatePromise && (state.buffer.messages.push({
|
|
591
|
+
type: ClientMessageType.FetchStorage
|
|
592
|
+
}), tryFlushing(), _getInitialStatePromise = new Promise((resolve => _getInitialStateResolver = resolve))),
|
|
593
|
+
_getInitialStatePromise.then((() => ({
|
|
594
|
+
root: state.root
|
|
595
|
+
}))));
|
|
596
|
+
},
|
|
597
|
+
selectors: {
|
|
598
|
+
getConnectionState: function() {
|
|
599
|
+
return state.connection.state;
|
|
600
|
+
},
|
|
601
|
+
getSelf: function() {
|
|
602
|
+
return "open" === state.connection.state || "connecting" === state.connection.state ? {
|
|
603
|
+
connectionId: state.connection.id,
|
|
604
|
+
id: state.connection.userId,
|
|
605
|
+
info: state.connection.userInfo,
|
|
606
|
+
presence: getPresence()
|
|
607
|
+
} : null;
|
|
608
|
+
},
|
|
609
|
+
getPresence: getPresence,
|
|
610
|
+
getOthers: function() {
|
|
611
|
+
return state.others;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
1064
615
|
}
|
|
616
|
+
|
|
1065
617
|
function createRoom(options, context) {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
618
|
+
var _a, _b;
|
|
619
|
+
const initialPresence = null !== (_a = options.initialPresence) && void 0 !== _a ? _a : options.defaultPresence, initialStorage = null !== (_b = options.initialStorage) && void 0 !== _b ? _b : options.defaultStorageRoot, machine = makeStateMachine(function(initialPresence, initialStorage) {
|
|
620
|
+
return {
|
|
621
|
+
connection: {
|
|
622
|
+
state: "closed"
|
|
623
|
+
},
|
|
624
|
+
token: null,
|
|
625
|
+
lastConnectionId: null,
|
|
626
|
+
socket: null,
|
|
627
|
+
listeners: {
|
|
628
|
+
event: [],
|
|
629
|
+
others: [],
|
|
630
|
+
"my-presence": [],
|
|
631
|
+
error: [],
|
|
632
|
+
connection: [],
|
|
633
|
+
storage: []
|
|
634
|
+
},
|
|
635
|
+
numberOfRetry: 0,
|
|
636
|
+
lastFlushTime: 0,
|
|
637
|
+
timeoutHandles: {
|
|
638
|
+
flush: null,
|
|
639
|
+
reconnect: 0,
|
|
640
|
+
pongTimeout: 0
|
|
641
|
+
},
|
|
642
|
+
buffer: {
|
|
643
|
+
presence: null == initialPresence ? {} : initialPresence,
|
|
644
|
+
messages: [],
|
|
645
|
+
storageOperations: []
|
|
646
|
+
},
|
|
647
|
+
intervalHandles: {
|
|
648
|
+
heartbeat: 0
|
|
649
|
+
},
|
|
650
|
+
me: null == initialPresence ? {} : initialPresence,
|
|
651
|
+
users: {},
|
|
652
|
+
others: makeOthers({}),
|
|
653
|
+
defaultStorageRoot: initialStorage,
|
|
654
|
+
idFactory: null,
|
|
655
|
+
clock: 0,
|
|
656
|
+
opClock: 0,
|
|
657
|
+
items: new Map,
|
|
658
|
+
root: void 0,
|
|
659
|
+
undoStack: [],
|
|
660
|
+
redoStack: [],
|
|
661
|
+
isHistoryPaused: !1,
|
|
662
|
+
pausedHistory: [],
|
|
663
|
+
isBatching: !1,
|
|
664
|
+
batch: {
|
|
665
|
+
ops: [],
|
|
666
|
+
updates: {
|
|
667
|
+
storageUpdates: new Map,
|
|
668
|
+
presence: !1,
|
|
669
|
+
others: []
|
|
670
|
+
},
|
|
671
|
+
reverseOps: []
|
|
672
|
+
},
|
|
673
|
+
offlineOperations: new Map
|
|
674
|
+
};
|
|
675
|
+
}("function" == typeof initialPresence ? initialPresence(context.roomId) : initialPresence, "function" == typeof initialStorage ? initialStorage(context.roomId) : initialStorage), context), room = {
|
|
676
|
+
id: context.roomId,
|
|
677
|
+
getConnectionState: machine.selectors.getConnectionState,
|
|
678
|
+
getSelf: machine.selectors.getSelf,
|
|
679
|
+
subscribe: machine.subscribe,
|
|
680
|
+
unsubscribe: machine.unsubscribe,
|
|
681
|
+
getPresence: machine.selectors.getPresence,
|
|
682
|
+
updatePresence: machine.updatePresence,
|
|
683
|
+
getOthers: machine.selectors.getOthers,
|
|
684
|
+
broadcastEvent: machine.broadcastEvent,
|
|
685
|
+
getStorage: machine.getStorage,
|
|
686
|
+
batch: machine.batch,
|
|
687
|
+
history: {
|
|
688
|
+
undo: machine.undo,
|
|
689
|
+
redo: machine.redo,
|
|
690
|
+
pause: machine.pauseHistory,
|
|
691
|
+
resume: machine.resumeHistory
|
|
692
|
+
},
|
|
693
|
+
internalDevTools: {
|
|
694
|
+
closeWebsocket: machine.simulateSocketClose,
|
|
695
|
+
sendCloseEvent: machine.simulateSendCloseEvent
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
return {
|
|
699
|
+
connect: machine.connect,
|
|
700
|
+
disconnect: machine.disconnect,
|
|
701
|
+
onNavigatorOnline: machine.onNavigatorOnline,
|
|
702
|
+
onVisibilityChange: machine.onVisibilityChange,
|
|
703
|
+
room: room
|
|
704
|
+
};
|
|
1115
705
|
}
|
|
706
|
+
|
|
1116
707
|
class LiveblocksError extends Error {
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
708
|
+
constructor(message, code) {
|
|
709
|
+
super(message), this.code = code;
|
|
710
|
+
}
|
|
1121
711
|
}
|
|
712
|
+
|
|
1122
713
|
function parseToken(token) {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
return {
|
|
1133
|
-
actor: data.actor,
|
|
1134
|
-
id: data.id,
|
|
1135
|
-
info: data.info,
|
|
1136
|
-
};
|
|
1137
|
-
}
|
|
1138
|
-
throw new Error(`Authentication error. Liveblocks could not parse the response of your authentication endpoint`);
|
|
1139
|
-
}
|
|
1140
|
-
function prepareCreateWebSocket(liveblocksServer, WebSocketPolyfill) {
|
|
1141
|
-
if (typeof window === "undefined" && WebSocketPolyfill == null) {
|
|
1142
|
-
throw new Error("To use Liveblocks client in a non-dom environment, you need to provide a WebSocket polyfill.");
|
|
1143
|
-
}
|
|
1144
|
-
const ws = WebSocketPolyfill || WebSocket;
|
|
1145
|
-
return (token) => {
|
|
1146
|
-
return new ws(`${liveblocksServer}/?token=${token}`);
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
function prepareAuthEndpoint(authentication, fetchPolyfill) {
|
|
1150
|
-
if (authentication.type === "public") {
|
|
1151
|
-
if (typeof window === "undefined" && fetchPolyfill == null) {
|
|
1152
|
-
throw new Error("To use Liveblocks client in a non-dom environment with a publicApiKey, you need to provide a fetch polyfill.");
|
|
1153
|
-
}
|
|
1154
|
-
return (room) => fetchAuthEndpoint(fetchPolyfill || fetch, authentication.url, {
|
|
1155
|
-
room,
|
|
1156
|
-
publicApiKey: authentication.publicApiKey,
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
if (authentication.type === "private") {
|
|
1160
|
-
if (typeof window === "undefined" && fetchPolyfill == null) {
|
|
1161
|
-
throw new Error("To use Liveblocks client in a non-dom environment with a url as auth endpoint, you need to provide a fetch polyfill.");
|
|
1162
|
-
}
|
|
1163
|
-
return (room) => fetchAuthEndpoint(fetchPolyfill || fetch, authentication.url, {
|
|
1164
|
-
room,
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
if (authentication.type === "custom") {
|
|
1168
|
-
return authentication.callback;
|
|
1169
|
-
}
|
|
1170
|
-
throw new Error("Internal error. Unexpected authentication type");
|
|
714
|
+
const tokenParts = token.split(".");
|
|
715
|
+
if (3 !== tokenParts.length) throw new Error("Authentication error. Liveblocks could not parse the response of your authentication endpoint");
|
|
716
|
+
const data = parseJson(atob(tokenParts[1]));
|
|
717
|
+
if (void 0 !== data && isJsonObject(data) && "number" == typeof data.actor && (void 0 === data.id || "string" == typeof data.id)) return {
|
|
718
|
+
actor: data.actor,
|
|
719
|
+
id: data.id,
|
|
720
|
+
info: data.info
|
|
721
|
+
};
|
|
722
|
+
throw new Error("Authentication error. Liveblocks could not parse the response of your authentication endpoint");
|
|
1171
723
|
}
|
|
724
|
+
|
|
1172
725
|
function fetchAuthEndpoint(fetch, endpoint, body) {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
.then((authResponse) => {
|
|
1189
|
-
if (typeof authResponse.token !== "string") {
|
|
1190
|
-
throw new AuthenticationError(`Expected a json with a string token when doing a POST request on "${endpoint}", but got ${JSON.stringify(authResponse)}`);
|
|
1191
|
-
}
|
|
1192
|
-
return authResponse;
|
|
1193
|
-
});
|
|
726
|
+
return fetch(endpoint, {
|
|
727
|
+
method: "POST",
|
|
728
|
+
headers: {
|
|
729
|
+
"Content-Type": "application/json"
|
|
730
|
+
},
|
|
731
|
+
body: JSON.stringify(body)
|
|
732
|
+
}).then((res => {
|
|
733
|
+
if (!res.ok) throw new AuthenticationError(`Expected a status 200 but got ${res.status} when doing a POST request on "${endpoint}"`);
|
|
734
|
+
return res.json().catch((er => {
|
|
735
|
+
throw new AuthenticationError(`Expected a json when doing a POST request on "${endpoint}". ${er}`);
|
|
736
|
+
}));
|
|
737
|
+
})).then((authResponse => {
|
|
738
|
+
if ("string" != typeof authResponse.token) throw new AuthenticationError(`Expected a json with a string token when doing a POST request on "${endpoint}", but got ${JSON.stringify(authResponse)}`);
|
|
739
|
+
return authResponse;
|
|
740
|
+
}));
|
|
1194
741
|
}
|
|
742
|
+
|
|
1195
743
|
class AuthenticationError extends Error {
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
744
|
+
constructor(message) {
|
|
745
|
+
super(message);
|
|
746
|
+
}
|
|
1199
747
|
}
|
|
1200
748
|
|
|
1201
|
-
/**
|
|
1202
|
-
* Create a client that will be responsible to communicate with liveblocks servers.
|
|
1203
|
-
*
|
|
1204
|
-
* @example
|
|
1205
|
-
* const client = createClient({
|
|
1206
|
-
* authEndpoint: "/api/auth"
|
|
1207
|
-
* });
|
|
1208
|
-
*
|
|
1209
|
-
* // It's also possible to use a function to call your authentication endpoint.
|
|
1210
|
-
* // Useful to add additional headers or use an API wrapper (like Firebase functions)
|
|
1211
|
-
* const client = createClient({
|
|
1212
|
-
* authEndpoint: async (room) => {
|
|
1213
|
-
* const response = await fetch("/api/auth", {
|
|
1214
|
-
* method: "POST",
|
|
1215
|
-
* headers: {
|
|
1216
|
-
* Authentication: "token",
|
|
1217
|
-
* "Content-Type": "application/json"
|
|
1218
|
-
* },
|
|
1219
|
-
* body: JSON.stringify({ room })
|
|
1220
|
-
* });
|
|
1221
|
-
*
|
|
1222
|
-
* return await response.json();
|
|
1223
|
-
* }
|
|
1224
|
-
* });
|
|
1225
|
-
*/
|
|
1226
749
|
function createClient(options) {
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
if (typeof window !== "undefined") {
|
|
1268
|
-
// TODO: Expose a way to clear these
|
|
1269
|
-
window.addEventListener("online", () => {
|
|
1270
|
-
for (const [, room] of rooms) {
|
|
1271
|
-
room.onNavigatorOnline();
|
|
1272
|
-
}
|
|
1273
|
-
});
|
|
1274
|
-
}
|
|
1275
|
-
if (typeof document !== "undefined") {
|
|
1276
|
-
document.addEventListener("visibilitychange", () => {
|
|
1277
|
-
for (const [, room] of rooms) {
|
|
1278
|
-
room.onVisibilityChange(document.visibilityState);
|
|
1279
|
-
}
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
1282
|
-
return {
|
|
1283
|
-
getRoom,
|
|
1284
|
-
enter,
|
|
1285
|
-
leave,
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
function getThrottleDelayFromOptions(options) {
|
|
1289
|
-
if (options.throttle === undefined) {
|
|
1290
|
-
return 100;
|
|
1291
|
-
}
|
|
1292
|
-
if (typeof options.throttle !== "number" ||
|
|
1293
|
-
options.throttle < 80 ||
|
|
1294
|
-
options.throttle > 1000) {
|
|
1295
|
-
throw new Error("throttle should be a number between 80 and 1000.");
|
|
1296
|
-
}
|
|
1297
|
-
return options.throttle;
|
|
750
|
+
const clientOptions = options, throttleDelay = function(options) {
|
|
751
|
+
if (void 0 === options.throttle) return 100;
|
|
752
|
+
if ("number" != typeof options.throttle || options.throttle < 80 || options.throttle > 1e3) throw new Error("throttle should be a number between 80 and 1000.");
|
|
753
|
+
return options.throttle;
|
|
754
|
+
}(options), rooms = new Map;
|
|
755
|
+
return "undefined" != typeof window && window.addEventListener("online", (() => {
|
|
756
|
+
for (const [, room] of rooms) room.onNavigatorOnline();
|
|
757
|
+
})), "undefined" != typeof document && document.addEventListener("visibilitychange", (() => {
|
|
758
|
+
for (const [, room] of rooms) room.onVisibilityChange(document.visibilityState);
|
|
759
|
+
})), {
|
|
760
|
+
getRoom: function(roomId) {
|
|
761
|
+
const internalRoom = rooms.get(roomId);
|
|
762
|
+
return internalRoom ? internalRoom.room : null;
|
|
763
|
+
},
|
|
764
|
+
enter: function(roomId, options = {}) {
|
|
765
|
+
let internalRoom = rooms.get(roomId);
|
|
766
|
+
return internalRoom || (deprecateIf(options.defaultPresence, "Argument `defaultPresence` will be removed in @liveblocks/client 0.18. Please use `initialPresence` instead. For more info, see https://bit.ly/3Niy5aP", "defaultPresence"),
|
|
767
|
+
deprecateIf(options.defaultStorageRoot, "Argument `defaultStorageRoot` will be removed in @liveblocks/client 0.18. Please use `initialStorage` instead. For more info, see https://bit.ly/3Niy5aP", "defaultStorageRoot"),
|
|
768
|
+
internalRoom = createRoom({
|
|
769
|
+
initialPresence: options.initialPresence,
|
|
770
|
+
initialStorage: options.initialStorage,
|
|
771
|
+
defaultPresence: options.defaultPresence,
|
|
772
|
+
defaultStorageRoot: options.defaultStorageRoot
|
|
773
|
+
}, {
|
|
774
|
+
roomId: roomId,
|
|
775
|
+
throttleDelay: throttleDelay,
|
|
776
|
+
WebSocketPolyfill: clientOptions.WebSocketPolyfill,
|
|
777
|
+
fetchPolyfill: clientOptions.fetchPolyfill,
|
|
778
|
+
liveblocksServer: clientOptions.liveblocksServer || "wss://liveblocks.net/v5",
|
|
779
|
+
authentication: prepareAuthentication(clientOptions)
|
|
780
|
+
}), rooms.set(roomId, internalRoom), options.DO_NOT_USE_withoutConnecting || internalRoom.connect()),
|
|
781
|
+
internalRoom.room;
|
|
782
|
+
},
|
|
783
|
+
leave: function(roomId) {
|
|
784
|
+
const room = rooms.get(roomId);
|
|
785
|
+
room && (room.disconnect(), rooms.delete(roomId));
|
|
786
|
+
}
|
|
787
|
+
};
|
|
1298
788
|
}
|
|
789
|
+
|
|
1299
790
|
function prepareAuthentication(clientOptions) {
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
}
|
|
1315
|
-
else if (typeof clientOptions.authEndpoint === "function") {
|
|
1316
|
-
return {
|
|
1317
|
-
type: "custom",
|
|
1318
|
-
callback: clientOptions.authEndpoint,
|
|
1319
|
-
};
|
|
1320
|
-
}
|
|
1321
|
-
throw new Error("Invalid Liveblocks client options. For more information: https://liveblocks.io/docs/api-reference/liveblocks-client#createClient");
|
|
791
|
+
if ("string" == typeof clientOptions.publicApiKey) return {
|
|
792
|
+
type: "public",
|
|
793
|
+
publicApiKey: clientOptions.publicApiKey,
|
|
794
|
+
url: clientOptions.publicAuthorizeEndpoint || "https://liveblocks.io/api/public/authorize"
|
|
795
|
+
};
|
|
796
|
+
if ("string" == typeof clientOptions.authEndpoint) return {
|
|
797
|
+
type: "private",
|
|
798
|
+
url: clientOptions.authEndpoint
|
|
799
|
+
};
|
|
800
|
+
if ("function" == typeof clientOptions.authEndpoint) return {
|
|
801
|
+
type: "custom",
|
|
802
|
+
callback: clientOptions.authEndpoint
|
|
803
|
+
};
|
|
804
|
+
throw new Error("Invalid Liveblocks client options. For more information: https://liveblocks.io/docs/api-reference/liveblocks-client#createClient");
|
|
1322
805
|
}
|
|
1323
806
|
|
|
1324
807
|
export { createClient };
|