@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.
Files changed (48) hide show
  1. package/README.md +34 -6
  2. package/lib/cjs/AbstractCrdt.d.ts +61 -0
  3. package/lib/cjs/AbstractCrdt.js +98 -0
  4. package/lib/cjs/LiveList.d.ts +133 -0
  5. package/lib/cjs/LiveList.js +374 -0
  6. package/lib/cjs/LiveMap.d.ts +83 -0
  7. package/lib/cjs/LiveMap.js +272 -0
  8. package/lib/cjs/LiveObject.d.ts +66 -0
  9. package/lib/cjs/LiveObject.js +368 -0
  10. package/lib/cjs/LiveRegister.d.ts +21 -0
  11. package/lib/cjs/LiveRegister.js +69 -0
  12. package/lib/cjs/immutable/index.d.ts +7 -0
  13. package/lib/cjs/immutable/index.js +214 -0
  14. package/lib/cjs/index.d.ts +3 -1
  15. package/lib/cjs/index.js +7 -5
  16. package/lib/cjs/room.d.ts +50 -9
  17. package/lib/cjs/room.js +477 -85
  18. package/lib/cjs/types.d.ts +220 -40
  19. package/lib/cjs/utils.d.ts +7 -0
  20. package/lib/cjs/utils.js +64 -1
  21. package/lib/esm/AbstractCrdt.d.ts +61 -0
  22. package/lib/esm/AbstractCrdt.js +94 -0
  23. package/lib/esm/LiveList.d.ts +133 -0
  24. package/lib/esm/LiveList.js +370 -0
  25. package/lib/esm/LiveMap.d.ts +83 -0
  26. package/lib/esm/LiveMap.js +268 -0
  27. package/lib/esm/LiveObject.d.ts +66 -0
  28. package/lib/esm/LiveObject.js +364 -0
  29. package/lib/esm/LiveRegister.d.ts +21 -0
  30. package/lib/esm/LiveRegister.js +65 -0
  31. package/lib/esm/immutable/index.d.ts +7 -0
  32. package/lib/esm/immutable/index.js +207 -0
  33. package/lib/esm/index.d.ts +3 -1
  34. package/lib/esm/index.js +3 -1
  35. package/lib/esm/room.d.ts +50 -9
  36. package/lib/esm/room.js +479 -84
  37. package/lib/esm/types.d.ts +220 -40
  38. package/lib/esm/utils.d.ts +7 -0
  39. package/lib/esm/utils.js +58 -0
  40. package/package.json +3 -3
  41. package/lib/cjs/doc.d.ts +0 -347
  42. package/lib/cjs/doc.js +0 -1349
  43. package/lib/cjs/storage.d.ts +0 -21
  44. package/lib/cjs/storage.js +0 -68
  45. package/lib/esm/doc.d.ts +0 -347
  46. package/lib/esm/doc.js +0 -1342
  47. package/lib/esm/storage.d.ts +0 -21
  48. 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 Storage from "./storage";
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 subscribe(type, listener) {
86
- if (!isValidRoomEventType(type)) {
87
- throw new Error(`"${type}" is not a valid event name`);
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
- state.listeners[type].push(listener);
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 newPresence = Object.assign(Object.assign({}, state.me), overrides);
125
- if (state.flushData.presence == null) {
126
- state.flushData.presence = overrides;
389
+ function updatePresence(overrides, options) {
390
+ const oldValues = {};
391
+ if (state.buffer.presence == null) {
392
+ state.buffer.presence = {};
127
393
  }
128
- else {
129
- for (const key in overrides) {
130
- state.flushData.presence[key] = overrides[key];
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
- state.me = newPresence;
134
- tryFlushing();
135
- for (const listener of state.listeners["my-presence"]) {
136
- listener(state.me);
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
- updateUsers({
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
- updateUsers({ type: "leave", user });
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
- updateUsers({ type: "reset" });
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.flushData.messages.push({
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
- switch (message.type) {
247
- case ServerMessageType.UserJoined: {
248
- onUserJoinedMessage(message);
249
- break;
250
- }
251
- case ServerMessageType.UpdatePresence: {
252
- onUpdatePresenceMessage(message);
253
- break;
254
- }
255
- case ServerMessageType.Event: {
256
- onEvent(message);
257
- break;
258
- }
259
- case ServerMessageType.UserLeft: {
260
- onUserLeftMessage(message);
261
- break;
262
- }
263
- case ServerMessageType.RoomState: {
264
- onRoomStateMessage(message);
265
- break;
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
- storage.onMessage(message);
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
- updateUsers({ type: "reset" });
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.flushData = {
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.flushData.presence) {
691
+ if (state.buffer.presence) {
394
692
  messages.push({
395
693
  type: ClientMessageType.UpdatePresence,
396
- data: state.flushData.presence,
694
+ data: state.buffer.presence,
397
695
  });
398
696
  }
399
- for (const event of state.flushData.messages) {
697
+ for (const event of state.buffer.messages) {
400
698
  messages.push(event);
401
699
  }
402
- if (state.flushData.storageOperations.length > 0) {
700
+ if (state.buffer.storageOperations.length > 0) {
403
701
  messages.push({
404
702
  type: ClientMessageType.UpdateStorage,
405
- ops: state.flushData.storageOperations,
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
- updateUsers({ type: "reset" });
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.flushData.messages.push({
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.flushData.storageOperations.push(...ops);
750
+ state.buffer.storageOperations.push(...ops);
453
751
  tryFlushing();
454
752
  }
455
- const storage = new Storage({
456
- fetchStorage: () => {
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
- const doc = yield storage.getDocument();
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: doc.root,
769
+ root: state.root,
475
770
  };
476
771
  });
477
772
  }
478
773
  function undo() {
479
- storage.undo();
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
- storage.redo();
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
- flushData: {
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/v4";
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
- undo: machine.undo,
585
- redo: machine.redo,
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,