@liveblocks/client 0.12.3 → 0.13.2
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/README.md +34 -6
- package/lib/cjs/AbstractCrdt.d.ts +61 -0
- package/lib/cjs/AbstractCrdt.js +98 -0
- package/lib/cjs/LiveList.d.ts +133 -0
- package/lib/cjs/LiveList.js +374 -0
- package/lib/cjs/LiveMap.d.ts +83 -0
- package/lib/cjs/LiveMap.js +272 -0
- package/lib/cjs/LiveObject.d.ts +66 -0
- package/lib/cjs/LiveObject.js +368 -0
- package/lib/cjs/LiveRegister.d.ts +21 -0
- package/lib/cjs/LiveRegister.js +69 -0
- package/lib/cjs/immutable/index.d.ts +7 -0
- package/lib/cjs/immutable/index.js +214 -0
- package/lib/cjs/index.d.ts +3 -1
- package/lib/cjs/index.js +7 -5
- package/lib/cjs/room.d.ts +50 -9
- package/lib/cjs/room.js +477 -85
- package/lib/cjs/types.d.ts +220 -40
- package/lib/cjs/utils.d.ts +7 -0
- package/lib/cjs/utils.js +64 -1
- package/lib/esm/AbstractCrdt.d.ts +61 -0
- package/lib/esm/AbstractCrdt.js +94 -0
- package/lib/esm/LiveList.d.ts +133 -0
- package/lib/esm/LiveList.js +370 -0
- package/lib/esm/LiveMap.d.ts +83 -0
- package/lib/esm/LiveMap.js +268 -0
- package/lib/esm/LiveObject.d.ts +66 -0
- package/lib/esm/LiveObject.js +364 -0
- package/lib/esm/LiveRegister.d.ts +21 -0
- package/lib/esm/LiveRegister.js +65 -0
- package/lib/esm/immutable/index.d.ts +7 -0
- package/lib/esm/immutable/index.js +207 -0
- package/lib/esm/index.d.ts +3 -1
- package/lib/esm/index.js +3 -1
- package/lib/esm/room.d.ts +50 -9
- package/lib/esm/room.js +479 -84
- package/lib/esm/types.d.ts +220 -40
- package/lib/esm/utils.d.ts +7 -0
- package/lib/esm/utils.js +58 -0
- package/package.json +3 -3
- package/lib/cjs/doc.d.ts +0 -347
- package/lib/cjs/doc.js +0 -1349
- package/lib/cjs/storage.d.ts +0 -21
- package/lib/cjs/storage.js +0 -68
- package/lib/esm/doc.d.ts +0 -347
- package/lib/esm/doc.js +0 -1342
- package/lib/esm/storage.d.ts +0 -21
- package/lib/esm/storage.js +0 -65
package/lib/esm/room.js
CHANGED
|
@@ -7,10 +7,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { remove } from "./utils";
|
|
10
|
+
import { isSameNodeOrChildOf, remove } from "./utils";
|
|
11
11
|
import auth, { parseToken } from "./authentication";
|
|
12
|
-
import { ClientMessageType, ServerMessageType, } from "./live";
|
|
13
|
-
import
|
|
12
|
+
import { ClientMessageType, ServerMessageType, OpType, } from "./live";
|
|
13
|
+
import { LiveMap } from "./LiveMap";
|
|
14
|
+
import { LiveObject } from "./LiveObject";
|
|
15
|
+
import { LiveList } from "./LiveList";
|
|
16
|
+
import { AbstractCrdt } from "./AbstractCrdt";
|
|
17
|
+
import { LiveRegister } from "./LiveRegister";
|
|
14
18
|
const BACKOFF_RETRY_DELAYS = [250, 500, 1000, 2000, 4000, 8000, 10000];
|
|
15
19
|
const HEARTBEAT_INTERVAL = 30000;
|
|
16
20
|
// const WAKE_UP_CHECK_INTERVAL = 2000;
|
|
@@ -82,13 +86,275 @@ export function makeStateMachine(state, context, mockedEffects) {
|
|
|
82
86
|
return setTimeout(connect, delay);
|
|
83
87
|
},
|
|
84
88
|
};
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
function genericSubscribe(callback) {
|
|
90
|
+
state.listeners.storage.push(callback);
|
|
91
|
+
return () => remove(state.listeners.storage, callback);
|
|
92
|
+
}
|
|
93
|
+
function crdtSubscribe(crdt, innerCallback, options) {
|
|
94
|
+
const cb = (updates) => {
|
|
95
|
+
const relatedUpdates = [];
|
|
96
|
+
for (const update of updates) {
|
|
97
|
+
if ((options === null || options === void 0 ? void 0 : options.isDeep) && isSameNodeOrChildOf(update.node, crdt)) {
|
|
98
|
+
relatedUpdates.push(update);
|
|
99
|
+
}
|
|
100
|
+
else if (update.node._id === crdt._id) {
|
|
101
|
+
innerCallback(update.node);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if ((options === null || options === void 0 ? void 0 : options.isDeep) && relatedUpdates.length > 0) {
|
|
105
|
+
innerCallback(relatedUpdates);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
return genericSubscribe(cb);
|
|
109
|
+
}
|
|
110
|
+
function createRootFromMessage(message) {
|
|
111
|
+
state.root = load(message.items);
|
|
112
|
+
for (const key in state.defaultStorageRoot) {
|
|
113
|
+
if (state.root.get(key) == null) {
|
|
114
|
+
state.root.set(key, state.defaultStorageRoot[key]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function load(items) {
|
|
119
|
+
if (items.length === 0) {
|
|
120
|
+
throw new Error("Internal error: cannot load storage without items");
|
|
121
|
+
}
|
|
122
|
+
const parentToChildren = new Map();
|
|
123
|
+
let root = null;
|
|
124
|
+
for (const tuple of items) {
|
|
125
|
+
const parentId = tuple[1].parentId;
|
|
126
|
+
if (parentId == null) {
|
|
127
|
+
root = tuple;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const children = parentToChildren.get(parentId);
|
|
131
|
+
if (children != null) {
|
|
132
|
+
children.push(tuple);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
parentToChildren.set(parentId, [tuple]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (root == null) {
|
|
140
|
+
throw new Error("Root can't be null");
|
|
141
|
+
}
|
|
142
|
+
return LiveObject._deserialize(root, parentToChildren, {
|
|
143
|
+
addItem,
|
|
144
|
+
deleteItem,
|
|
145
|
+
generateId,
|
|
146
|
+
generateOpId,
|
|
147
|
+
dispatch: storageDispatch,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function addItem(id, item) {
|
|
151
|
+
state.items.set(id, item);
|
|
152
|
+
}
|
|
153
|
+
function deleteItem(id) {
|
|
154
|
+
state.items.delete(id);
|
|
155
|
+
}
|
|
156
|
+
function getItem(id) {
|
|
157
|
+
return state.items.get(id);
|
|
158
|
+
}
|
|
159
|
+
function addToUndoStack(historyItem) {
|
|
160
|
+
// If undo stack is too large, we remove the older item
|
|
161
|
+
if (state.undoStack.length >= 50) {
|
|
162
|
+
state.undoStack.shift();
|
|
163
|
+
}
|
|
164
|
+
if (state.isHistoryPaused) {
|
|
165
|
+
state.pausedHistory.unshift(...historyItem);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
state.undoStack.push(historyItem);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function storageDispatch(ops, reverse, modified) {
|
|
172
|
+
if (state.isBatching) {
|
|
173
|
+
state.batch.ops.push(...ops);
|
|
174
|
+
for (const item of modified) {
|
|
175
|
+
state.batch.updates.nodes.add(item);
|
|
176
|
+
}
|
|
177
|
+
state.batch.reverseOps.push(...reverse);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
addToUndoStack(reverse);
|
|
181
|
+
state.redoStack = [];
|
|
182
|
+
dispatch(ops);
|
|
183
|
+
notify({ nodes: new Set(modified) });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function notify({ nodes = new Set(), presence = false, others = [], }) {
|
|
187
|
+
if (others.length > 0) {
|
|
188
|
+
state.others = makeOthers(state.users);
|
|
189
|
+
for (const event of others) {
|
|
190
|
+
for (const listener of state.listeners["others"]) {
|
|
191
|
+
listener(state.others, event);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (presence) {
|
|
196
|
+
for (const listener of state.listeners["my-presence"]) {
|
|
197
|
+
listener(state.me);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (nodes.size > 0) {
|
|
201
|
+
for (const subscriber of state.listeners.storage) {
|
|
202
|
+
subscriber(Array.from(nodes).map((m) => {
|
|
203
|
+
if (m instanceof LiveObject) {
|
|
204
|
+
return {
|
|
205
|
+
type: "LiveObject",
|
|
206
|
+
node: m,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
else if (m instanceof LiveList) {
|
|
210
|
+
return {
|
|
211
|
+
type: "LiveList",
|
|
212
|
+
node: m,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
return {
|
|
217
|
+
type: "LiveMap",
|
|
218
|
+
node: m,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function getConnectionId() {
|
|
226
|
+
if (state.connection.state === "open" ||
|
|
227
|
+
state.connection.state === "connecting") {
|
|
228
|
+
return state.connection.id;
|
|
229
|
+
}
|
|
230
|
+
throw new Error("Internal. Tried to get connection id but connection is not open");
|
|
231
|
+
}
|
|
232
|
+
function generateId() {
|
|
233
|
+
return `${getConnectionId()}:${state.clock++}`;
|
|
234
|
+
}
|
|
235
|
+
function generateOpId() {
|
|
236
|
+
return `${getConnectionId()}:${state.opClock++}`;
|
|
237
|
+
}
|
|
238
|
+
function apply(item) {
|
|
239
|
+
const result = {
|
|
240
|
+
reverse: [],
|
|
241
|
+
updates: { nodes: new Set(), presence: false },
|
|
242
|
+
};
|
|
243
|
+
for (const op of item) {
|
|
244
|
+
if (op.type === "presence") {
|
|
245
|
+
const reverse = {
|
|
246
|
+
type: "presence",
|
|
247
|
+
data: {},
|
|
248
|
+
};
|
|
249
|
+
for (const key in op.data) {
|
|
250
|
+
reverse.data[key] = state.me[key];
|
|
251
|
+
}
|
|
252
|
+
state.me = Object.assign(Object.assign({}, state.me), op.data);
|
|
253
|
+
if (state.buffer.presence == null) {
|
|
254
|
+
state.buffer.presence = op.data;
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
for (const key in op.data) {
|
|
258
|
+
state.buffer.presence[key] = op.data;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
result.reverse.unshift(reverse);
|
|
262
|
+
result.updates.presence = true;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const applyOpResult = applyOp(op);
|
|
266
|
+
if (applyOpResult.modified) {
|
|
267
|
+
result.updates.nodes.add(applyOpResult.modified);
|
|
268
|
+
result.reverse.unshift(...applyOpResult.reverse);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
function applyOp(op) {
|
|
275
|
+
switch (op.type) {
|
|
276
|
+
case OpType.DeleteObjectKey:
|
|
277
|
+
case OpType.UpdateObject:
|
|
278
|
+
case OpType.DeleteCrdt: {
|
|
279
|
+
const item = state.items.get(op.id);
|
|
280
|
+
if (item == null) {
|
|
281
|
+
return { modified: false };
|
|
282
|
+
}
|
|
283
|
+
return item._apply(op);
|
|
284
|
+
}
|
|
285
|
+
case OpType.SetParentKey: {
|
|
286
|
+
const item = state.items.get(op.id);
|
|
287
|
+
if (item == null) {
|
|
288
|
+
return { modified: false };
|
|
289
|
+
}
|
|
290
|
+
if (item._parent instanceof LiveList) {
|
|
291
|
+
const previousKey = item._parentKey;
|
|
292
|
+
item._parent._setChildKey(op.parentKey, item);
|
|
293
|
+
return {
|
|
294
|
+
reverse: [
|
|
295
|
+
{
|
|
296
|
+
type: OpType.SetParentKey,
|
|
297
|
+
id: item._id,
|
|
298
|
+
parentKey: previousKey,
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
modified: item._parent,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return { modified: false };
|
|
305
|
+
}
|
|
306
|
+
case OpType.CreateObject: {
|
|
307
|
+
const parent = state.items.get(op.parentId);
|
|
308
|
+
if (parent == null || getItem(op.id) != null) {
|
|
309
|
+
return { modified: false };
|
|
310
|
+
}
|
|
311
|
+
return parent._attachChild(op.id, op.parentKey, new LiveObject(op.data));
|
|
312
|
+
}
|
|
313
|
+
case OpType.CreateList: {
|
|
314
|
+
const parent = state.items.get(op.parentId);
|
|
315
|
+
if (parent == null || getItem(op.id) != null) {
|
|
316
|
+
return { modified: false };
|
|
317
|
+
}
|
|
318
|
+
return parent._attachChild(op.id, op.parentKey, new LiveList());
|
|
319
|
+
}
|
|
320
|
+
case OpType.CreateRegister: {
|
|
321
|
+
const parent = state.items.get(op.parentId);
|
|
322
|
+
if (parent == null || getItem(op.id) != null) {
|
|
323
|
+
return { modified: false };
|
|
324
|
+
}
|
|
325
|
+
return parent._attachChild(op.id, op.parentKey, new LiveRegister(op.data));
|
|
326
|
+
}
|
|
327
|
+
case OpType.CreateMap: {
|
|
328
|
+
const parent = state.items.get(op.parentId);
|
|
329
|
+
if (parent == null || getItem(op.id) != null) {
|
|
330
|
+
return { modified: false };
|
|
331
|
+
}
|
|
332
|
+
return parent._attachChild(op.id, op.parentKey, new LiveMap());
|
|
333
|
+
}
|
|
88
334
|
}
|
|
89
|
-
|
|
335
|
+
return { modified: false };
|
|
336
|
+
}
|
|
337
|
+
function subscribe(firstParam, listener, options) {
|
|
338
|
+
if (firstParam instanceof AbstractCrdt) {
|
|
339
|
+
return crdtSubscribe(firstParam, listener, options);
|
|
340
|
+
}
|
|
341
|
+
else if (typeof firstParam === "function") {
|
|
342
|
+
return genericSubscribe(firstParam);
|
|
343
|
+
}
|
|
344
|
+
else if (!isValidRoomEventType(firstParam)) {
|
|
345
|
+
throw new Error(`"${firstParam}" is not a valid event name`);
|
|
346
|
+
}
|
|
347
|
+
state.listeners[firstParam].push(listener);
|
|
348
|
+
return () => {
|
|
349
|
+
const callbacks = state.listeners[firstParam];
|
|
350
|
+
remove(callbacks, listener);
|
|
351
|
+
};
|
|
90
352
|
}
|
|
91
353
|
function unsubscribe(event, callback) {
|
|
354
|
+
console.warn(`unsubscribe is depreacted and will be removed in a future version.
|
|
355
|
+
use the callback returned by subscribe instead.
|
|
356
|
+
See v0.13 release notes for more information.
|
|
357
|
+
`);
|
|
92
358
|
if (!isValidRoomEventType(event)) {
|
|
93
359
|
throw new Error(`"${event}" is not a valid event name`);
|
|
94
360
|
}
|
|
@@ -120,20 +386,28 @@ export function makeStateMachine(state, context, mockedEffects) {
|
|
|
120
386
|
updateConnection({ state: "authenticating" });
|
|
121
387
|
effects.authenticate();
|
|
122
388
|
}
|
|
123
|
-
function updatePresence(overrides) {
|
|
124
|
-
const
|
|
125
|
-
if (state.
|
|
126
|
-
state.
|
|
389
|
+
function updatePresence(overrides, options) {
|
|
390
|
+
const oldValues = {};
|
|
391
|
+
if (state.buffer.presence == null) {
|
|
392
|
+
state.buffer.presence = {};
|
|
127
393
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
394
|
+
for (const key in overrides) {
|
|
395
|
+
state.buffer.presence[key] = overrides[key];
|
|
396
|
+
oldValues[key] = state.me[key];
|
|
397
|
+
}
|
|
398
|
+
state.me = Object.assign(Object.assign({}, state.me), overrides);
|
|
399
|
+
if (state.isBatching) {
|
|
400
|
+
if (options === null || options === void 0 ? void 0 : options.addToHistory) {
|
|
401
|
+
state.batch.reverseOps.push({ type: "presence", data: oldValues });
|
|
131
402
|
}
|
|
403
|
+
state.batch.updates.presence = true;
|
|
132
404
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
405
|
+
else {
|
|
406
|
+
tryFlushing();
|
|
407
|
+
if (options === null || options === void 0 ? void 0 : options.addToHistory) {
|
|
408
|
+
addToUndoStack([{ type: "presence", data: oldValues }]);
|
|
409
|
+
}
|
|
410
|
+
notify({ presence: true });
|
|
137
411
|
}
|
|
138
412
|
}
|
|
139
413
|
function authenticationSuccess(token, socket) {
|
|
@@ -174,25 +448,20 @@ export function makeStateMachine(state, context, mockedEffects) {
|
|
|
174
448
|
presence: Object.assign(Object.assign({}, user.presence), message.data),
|
|
175
449
|
};
|
|
176
450
|
}
|
|
177
|
-
|
|
451
|
+
return {
|
|
178
452
|
type: "update",
|
|
179
453
|
updates: message.data,
|
|
180
454
|
user: state.users[message.actor],
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
function updateUsers(event) {
|
|
184
|
-
state.others = makeOthers(state.users);
|
|
185
|
-
for (const listener of state.listeners["others"]) {
|
|
186
|
-
listener(state.others, event);
|
|
187
|
-
}
|
|
455
|
+
};
|
|
188
456
|
}
|
|
189
457
|
function onUserLeftMessage(message) {
|
|
190
458
|
const userLeftMessage = message;
|
|
191
459
|
const user = state.users[userLeftMessage.actor];
|
|
192
460
|
if (user) {
|
|
193
461
|
delete state.users[userLeftMessage.actor];
|
|
194
|
-
|
|
462
|
+
return { type: "leave", user };
|
|
195
463
|
}
|
|
464
|
+
return null;
|
|
196
465
|
}
|
|
197
466
|
function onRoomStateMessage(message) {
|
|
198
467
|
const newUsers = {};
|
|
@@ -206,7 +475,7 @@ export function makeStateMachine(state, context, mockedEffects) {
|
|
|
206
475
|
};
|
|
207
476
|
}
|
|
208
477
|
state.users = newUsers;
|
|
209
|
-
|
|
478
|
+
return { type: "reset" };
|
|
210
479
|
}
|
|
211
480
|
function onNavigatorOnline() {
|
|
212
481
|
if (state.connection.state === "unavailable") {
|
|
@@ -225,17 +494,17 @@ export function makeStateMachine(state, context, mockedEffects) {
|
|
|
225
494
|
info: message.info,
|
|
226
495
|
id: message.id,
|
|
227
496
|
};
|
|
228
|
-
updateUsers({ type: "enter", user: state.users[message.actor] });
|
|
229
497
|
if (state.me) {
|
|
230
498
|
// Send current presence to new user
|
|
231
499
|
// TODO: Consider storing it on the backend
|
|
232
|
-
state.
|
|
500
|
+
state.buffer.messages.push({
|
|
233
501
|
type: ClientMessageType.UpdatePresence,
|
|
234
502
|
data: state.me,
|
|
235
503
|
targetActor: message.actor,
|
|
236
504
|
});
|
|
237
505
|
tryFlushing();
|
|
238
506
|
}
|
|
507
|
+
return { type: "enter", user: state.users[message.actor] };
|
|
239
508
|
}
|
|
240
509
|
function onMessage(event) {
|
|
241
510
|
if (event.data === "pong") {
|
|
@@ -243,29 +512,57 @@ export function makeStateMachine(state, context, mockedEffects) {
|
|
|
243
512
|
return;
|
|
244
513
|
}
|
|
245
514
|
const message = JSON.parse(event.data);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
515
|
+
let subMessages = [];
|
|
516
|
+
if (Array.isArray(message)) {
|
|
517
|
+
subMessages = message;
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
subMessages.push(message);
|
|
521
|
+
}
|
|
522
|
+
const updates = {
|
|
523
|
+
nodes: new Set(),
|
|
524
|
+
others: [],
|
|
525
|
+
};
|
|
526
|
+
for (const subMessage of subMessages) {
|
|
527
|
+
switch (subMessage.type) {
|
|
528
|
+
case ServerMessageType.UserJoined: {
|
|
529
|
+
updates.others.push(onUserJoinedMessage(message));
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
case ServerMessageType.UpdatePresence: {
|
|
533
|
+
updates.others.push(onUpdatePresenceMessage(subMessage));
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
case ServerMessageType.Event: {
|
|
537
|
+
onEvent(subMessage);
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
case ServerMessageType.UserLeft: {
|
|
541
|
+
const event = onUserLeftMessage(subMessage);
|
|
542
|
+
if (event) {
|
|
543
|
+
updates.others.push(event);
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case ServerMessageType.RoomState: {
|
|
548
|
+
updates.others.push(onRoomStateMessage(subMessage));
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
case ServerMessageType.InitialStorageState: {
|
|
552
|
+
createRootFromMessage(subMessage);
|
|
553
|
+
_getInitialStateResolver === null || _getInitialStateResolver === void 0 ? void 0 : _getInitialStateResolver();
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
case ServerMessageType.UpdateStorage: {
|
|
557
|
+
const applyResult = apply(subMessage.ops);
|
|
558
|
+
for (const node of applyResult.updates.nodes) {
|
|
559
|
+
updates.nodes.add(node);
|
|
560
|
+
}
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
266
563
|
}
|
|
267
564
|
}
|
|
268
|
-
|
|
565
|
+
notify(updates);
|
|
269
566
|
}
|
|
270
567
|
// function onWakeUp() {
|
|
271
568
|
// // Sometimes, the browser can put the webpage on pause (computer is on sleep mode for example)
|
|
@@ -285,11 +582,12 @@ export function makeStateMachine(state, context, mockedEffects) {
|
|
|
285
582
|
}
|
|
286
583
|
clearTimeout(state.timeoutHandles.reconnect);
|
|
287
584
|
state.users = {};
|
|
288
|
-
|
|
585
|
+
notify({ others: [{ type: "reset" }] });
|
|
289
586
|
if (event.code >= 4000 && event.code <= 4100) {
|
|
290
587
|
updateConnection({ state: "failed" });
|
|
291
588
|
const error = new LiveblocksError(event.reason, event.code);
|
|
292
589
|
for (const listener of state.listeners.error) {
|
|
590
|
+
console.error(`Liveblocks WebSocket connection closed. Reason: ${error.message} (code: ${error.code})`);
|
|
293
591
|
listener(error);
|
|
294
592
|
}
|
|
295
593
|
}
|
|
@@ -374,7 +672,7 @@ export function makeStateMachine(state, context, mockedEffects) {
|
|
|
374
672
|
return;
|
|
375
673
|
}
|
|
376
674
|
effects.send(messages);
|
|
377
|
-
state.
|
|
675
|
+
state.buffer = {
|
|
378
676
|
messages: [],
|
|
379
677
|
storageOperations: [],
|
|
380
678
|
presence: null,
|
|
@@ -390,19 +688,19 @@ export function makeStateMachine(state, context, mockedEffects) {
|
|
|
390
688
|
}
|
|
391
689
|
function flushDataToMessages(state) {
|
|
392
690
|
const messages = [];
|
|
393
|
-
if (state.
|
|
691
|
+
if (state.buffer.presence) {
|
|
394
692
|
messages.push({
|
|
395
693
|
type: ClientMessageType.UpdatePresence,
|
|
396
|
-
data: state.
|
|
694
|
+
data: state.buffer.presence,
|
|
397
695
|
});
|
|
398
696
|
}
|
|
399
|
-
for (const event of state.
|
|
697
|
+
for (const event of state.buffer.messages) {
|
|
400
698
|
messages.push(event);
|
|
401
699
|
}
|
|
402
|
-
if (state.
|
|
700
|
+
if (state.buffer.storageOperations.length > 0) {
|
|
403
701
|
messages.push({
|
|
404
702
|
type: ClientMessageType.UpdateStorage,
|
|
405
|
-
ops: state.
|
|
703
|
+
ops: state.buffer.storageOperations,
|
|
406
704
|
});
|
|
407
705
|
}
|
|
408
706
|
return messages;
|
|
@@ -424,7 +722,7 @@ export function makeStateMachine(state, context, mockedEffects) {
|
|
|
424
722
|
clearTimeout(state.timeoutHandles.pongTimeout);
|
|
425
723
|
clearInterval(state.intervalHandles.heartbeat);
|
|
426
724
|
state.users = {};
|
|
427
|
-
|
|
725
|
+
notify({ others: [{ type: "reset" }] });
|
|
428
726
|
clearListeners();
|
|
429
727
|
}
|
|
430
728
|
function clearListeners() {
|
|
@@ -442,44 +740,115 @@ export function makeStateMachine(state, context, mockedEffects) {
|
|
|
442
740
|
if (state.socket == null) {
|
|
443
741
|
return;
|
|
444
742
|
}
|
|
445
|
-
state.
|
|
743
|
+
state.buffer.messages.push({
|
|
446
744
|
type: ClientMessageType.ClientEvent,
|
|
447
745
|
event,
|
|
448
746
|
});
|
|
449
747
|
tryFlushing();
|
|
450
748
|
}
|
|
451
749
|
function dispatch(ops) {
|
|
452
|
-
state.
|
|
750
|
+
state.buffer.storageOperations.push(...ops);
|
|
453
751
|
tryFlushing();
|
|
454
752
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
state.flushData.messages.push({ type: ClientMessageType.FetchStorage });
|
|
458
|
-
tryFlushing();
|
|
459
|
-
},
|
|
460
|
-
dispatch,
|
|
461
|
-
getConnectionId: () => {
|
|
462
|
-
const me = getSelf();
|
|
463
|
-
if (me) {
|
|
464
|
-
return me.connectionId;
|
|
465
|
-
}
|
|
466
|
-
throw new Error("Unexpected");
|
|
467
|
-
},
|
|
468
|
-
defaultRoot: state.defaultStorageRoot,
|
|
469
|
-
});
|
|
753
|
+
let _getInitialStatePromise = null;
|
|
754
|
+
let _getInitialStateResolver = null;
|
|
470
755
|
function getStorage() {
|
|
471
756
|
return __awaiter(this, void 0, void 0, function* () {
|
|
472
|
-
|
|
757
|
+
if (state.root) {
|
|
758
|
+
return {
|
|
759
|
+
root: state.root,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
if (_getInitialStatePromise == null) {
|
|
763
|
+
state.buffer.messages.push({ type: ClientMessageType.FetchStorage });
|
|
764
|
+
tryFlushing();
|
|
765
|
+
_getInitialStatePromise = new Promise((resolve) => (_getInitialStateResolver = resolve));
|
|
766
|
+
}
|
|
767
|
+
yield _getInitialStatePromise;
|
|
473
768
|
return {
|
|
474
|
-
root:
|
|
769
|
+
root: state.root,
|
|
475
770
|
};
|
|
476
771
|
});
|
|
477
772
|
}
|
|
478
773
|
function undo() {
|
|
479
|
-
|
|
774
|
+
if (state.isBatching) {
|
|
775
|
+
throw new Error("undo is not allowed during a batch");
|
|
776
|
+
}
|
|
777
|
+
const historyItem = state.undoStack.pop();
|
|
778
|
+
if (historyItem == null) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
state.isHistoryPaused = false;
|
|
782
|
+
const result = apply(historyItem);
|
|
783
|
+
notify(result.updates);
|
|
784
|
+
state.redoStack.push(result.reverse);
|
|
785
|
+
for (const op of historyItem) {
|
|
786
|
+
if (op.type !== "presence") {
|
|
787
|
+
state.buffer.storageOperations.push(op);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
tryFlushing();
|
|
480
791
|
}
|
|
481
792
|
function redo() {
|
|
482
|
-
|
|
793
|
+
if (state.isBatching) {
|
|
794
|
+
throw new Error("redo is not allowed during a batch");
|
|
795
|
+
}
|
|
796
|
+
const historyItem = state.redoStack.pop();
|
|
797
|
+
if (historyItem == null) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
state.isHistoryPaused = false;
|
|
801
|
+
const result = apply(historyItem);
|
|
802
|
+
notify(result.updates);
|
|
803
|
+
state.undoStack.push(result.reverse);
|
|
804
|
+
for (const op of historyItem) {
|
|
805
|
+
if (op.type !== "presence") {
|
|
806
|
+
state.buffer.storageOperations.push(op);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
tryFlushing();
|
|
810
|
+
}
|
|
811
|
+
function batch(callback) {
|
|
812
|
+
if (state.isBatching) {
|
|
813
|
+
throw new Error("batch should not be called during a batch");
|
|
814
|
+
}
|
|
815
|
+
state.isBatching = true;
|
|
816
|
+
try {
|
|
817
|
+
callback();
|
|
818
|
+
}
|
|
819
|
+
finally {
|
|
820
|
+
state.isBatching = false;
|
|
821
|
+
if (state.batch.reverseOps.length > 0) {
|
|
822
|
+
addToUndoStack(state.batch.reverseOps);
|
|
823
|
+
}
|
|
824
|
+
// Clear the redo stack because batch is always called from a local operation
|
|
825
|
+
state.redoStack = [];
|
|
826
|
+
if (state.batch.ops.length > 0) {
|
|
827
|
+
dispatch(state.batch.ops);
|
|
828
|
+
}
|
|
829
|
+
notify(state.batch.updates);
|
|
830
|
+
state.batch = {
|
|
831
|
+
ops: [],
|
|
832
|
+
reverseOps: [],
|
|
833
|
+
updates: {
|
|
834
|
+
others: [],
|
|
835
|
+
nodes: new Set(),
|
|
836
|
+
presence: false,
|
|
837
|
+
},
|
|
838
|
+
};
|
|
839
|
+
tryFlushing();
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function pauseHistory() {
|
|
843
|
+
state.pausedHistory = [];
|
|
844
|
+
state.isHistoryPaused = true;
|
|
845
|
+
}
|
|
846
|
+
function resumeHistory() {
|
|
847
|
+
state.isHistoryPaused = false;
|
|
848
|
+
if (state.pausedHistory.length > 0) {
|
|
849
|
+
addToUndoStack(state.pausedHistory);
|
|
850
|
+
}
|
|
851
|
+
state.pausedHistory = [];
|
|
483
852
|
}
|
|
484
853
|
return {
|
|
485
854
|
// Internal
|
|
@@ -491,6 +860,8 @@ export function makeStateMachine(state, context, mockedEffects) {
|
|
|
491
860
|
onNavigatorOnline,
|
|
492
861
|
// onWakeUp,
|
|
493
862
|
onVisibilityChange,
|
|
863
|
+
getUndoStack: () => state.undoStack,
|
|
864
|
+
getItemsCount: () => state.items.size,
|
|
494
865
|
// Core
|
|
495
866
|
connect,
|
|
496
867
|
disconnect,
|
|
@@ -499,8 +870,11 @@ export function makeStateMachine(state, context, mockedEffects) {
|
|
|
499
870
|
// Presence
|
|
500
871
|
updatePresence,
|
|
501
872
|
broadcastEvent,
|
|
873
|
+
batch,
|
|
502
874
|
undo,
|
|
503
875
|
redo,
|
|
876
|
+
pauseHistory,
|
|
877
|
+
resumeHistory,
|
|
504
878
|
getStorage,
|
|
505
879
|
selectors: {
|
|
506
880
|
// Core
|
|
@@ -522,6 +896,7 @@ export function defaultState(me, defaultStorageRoot) {
|
|
|
522
896
|
"my-presence": [],
|
|
523
897
|
error: [],
|
|
524
898
|
connection: [],
|
|
899
|
+
storage: [],
|
|
525
900
|
},
|
|
526
901
|
numberOfRetry: 0,
|
|
527
902
|
lastFlushTime: 0,
|
|
@@ -530,7 +905,7 @@ export function defaultState(me, defaultStorageRoot) {
|
|
|
530
905
|
reconnect: 0,
|
|
531
906
|
pongTimeout: 0,
|
|
532
907
|
},
|
|
533
|
-
|
|
908
|
+
buffer: {
|
|
534
909
|
presence: me == null ? {} : me,
|
|
535
910
|
messages: [],
|
|
536
911
|
storageOperations: [],
|
|
@@ -543,11 +918,26 @@ export function defaultState(me, defaultStorageRoot) {
|
|
|
543
918
|
others: makeOthers({}),
|
|
544
919
|
defaultStorageRoot,
|
|
545
920
|
idFactory: null,
|
|
921
|
+
// Storage
|
|
922
|
+
clock: 0,
|
|
923
|
+
opClock: 0,
|
|
924
|
+
items: new Map(),
|
|
925
|
+
root: undefined,
|
|
926
|
+
undoStack: [],
|
|
927
|
+
redoStack: [],
|
|
928
|
+
isHistoryPaused: false,
|
|
929
|
+
pausedHistory: [],
|
|
930
|
+
isBatching: false,
|
|
931
|
+
batch: {
|
|
932
|
+
ops: [],
|
|
933
|
+
updates: { nodes: new Set(), presence: false, others: [] },
|
|
934
|
+
reverseOps: [],
|
|
935
|
+
},
|
|
546
936
|
};
|
|
547
937
|
}
|
|
548
938
|
export function createRoom(name, options) {
|
|
549
939
|
const throttleDelay = options.throttle || 100;
|
|
550
|
-
const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net/
|
|
940
|
+
const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net/v5";
|
|
551
941
|
let authEndpoint;
|
|
552
942
|
if (options.authEndpoint) {
|
|
553
943
|
authEndpoint = options.authEndpoint;
|
|
@@ -581,8 +971,13 @@ export function createRoom(name, options) {
|
|
|
581
971
|
getOthers: machine.selectors.getOthers,
|
|
582
972
|
broadcastEvent: machine.broadcastEvent,
|
|
583
973
|
getStorage: machine.getStorage,
|
|
584
|
-
|
|
585
|
-
|
|
974
|
+
batch: machine.batch,
|
|
975
|
+
history: {
|
|
976
|
+
undo: machine.undo,
|
|
977
|
+
redo: machine.redo,
|
|
978
|
+
pause: machine.pauseHistory,
|
|
979
|
+
resume: machine.resumeHistory,
|
|
980
|
+
},
|
|
586
981
|
};
|
|
587
982
|
return {
|
|
588
983
|
connect: machine.connect,
|