@liveblocks/client 0.12.2 → 0.13.1

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 (44) 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/index.d.ts +3 -1
  13. package/lib/cjs/index.js +7 -5
  14. package/lib/cjs/room.d.ts +50 -9
  15. package/lib/cjs/room.js +476 -85
  16. package/lib/cjs/types.d.ts +220 -40
  17. package/lib/cjs/utils.d.ts +7 -0
  18. package/lib/cjs/utils.js +64 -1
  19. package/lib/esm/AbstractCrdt.d.ts +61 -0
  20. package/lib/esm/AbstractCrdt.js +94 -0
  21. package/lib/esm/LiveList.d.ts +133 -0
  22. package/lib/esm/LiveList.js +370 -0
  23. package/lib/esm/LiveMap.d.ts +83 -0
  24. package/lib/esm/LiveMap.js +268 -0
  25. package/lib/esm/LiveObject.d.ts +66 -0
  26. package/lib/esm/LiveObject.js +364 -0
  27. package/lib/esm/LiveRegister.d.ts +21 -0
  28. package/lib/esm/LiveRegister.js +65 -0
  29. package/lib/esm/index.d.ts +3 -1
  30. package/lib/esm/index.js +3 -1
  31. package/lib/esm/room.d.ts +50 -9
  32. package/lib/esm/room.js +478 -84
  33. package/lib/esm/types.d.ts +220 -40
  34. package/lib/esm/utils.d.ts +7 -0
  35. package/lib/esm/utils.js +58 -0
  36. package/package.json +3 -3
  37. package/lib/cjs/doc.d.ts +0 -347
  38. package/lib/cjs/doc.js +0 -1349
  39. package/lib/cjs/storage.d.ts +0 -21
  40. package/lib/cjs/storage.js +0 -68
  41. package/lib/esm/doc.d.ts +0 -347
  42. package/lib/esm/doc.js +0 -1342
  43. package/lib/esm/storage.d.ts +0 -21
  44. 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,7 +582,7 @@ 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);
@@ -374,7 +671,7 @@ export function makeStateMachine(state, context, mockedEffects) {
374
671
  return;
375
672
  }
376
673
  effects.send(messages);
377
- state.flushData = {
674
+ state.buffer = {
378
675
  messages: [],
379
676
  storageOperations: [],
380
677
  presence: null,
@@ -390,19 +687,19 @@ export function makeStateMachine(state, context, mockedEffects) {
390
687
  }
391
688
  function flushDataToMessages(state) {
392
689
  const messages = [];
393
- if (state.flushData.presence) {
690
+ if (state.buffer.presence) {
394
691
  messages.push({
395
692
  type: ClientMessageType.UpdatePresence,
396
- data: state.flushData.presence,
693
+ data: state.buffer.presence,
397
694
  });
398
695
  }
399
- for (const event of state.flushData.messages) {
696
+ for (const event of state.buffer.messages) {
400
697
  messages.push(event);
401
698
  }
402
- if (state.flushData.storageOperations.length > 0) {
699
+ if (state.buffer.storageOperations.length > 0) {
403
700
  messages.push({
404
701
  type: ClientMessageType.UpdateStorage,
405
- ops: state.flushData.storageOperations,
702
+ ops: state.buffer.storageOperations,
406
703
  });
407
704
  }
408
705
  return messages;
@@ -424,7 +721,7 @@ export function makeStateMachine(state, context, mockedEffects) {
424
721
  clearTimeout(state.timeoutHandles.pongTimeout);
425
722
  clearInterval(state.intervalHandles.heartbeat);
426
723
  state.users = {};
427
- updateUsers({ type: "reset" });
724
+ notify({ others: [{ type: "reset" }] });
428
725
  clearListeners();
429
726
  }
430
727
  function clearListeners() {
@@ -442,44 +739,115 @@ export function makeStateMachine(state, context, mockedEffects) {
442
739
  if (state.socket == null) {
443
740
  return;
444
741
  }
445
- state.flushData.messages.push({
742
+ state.buffer.messages.push({
446
743
  type: ClientMessageType.ClientEvent,
447
744
  event,
448
745
  });
449
746
  tryFlushing();
450
747
  }
451
748
  function dispatch(ops) {
452
- state.flushData.storageOperations.push(...ops);
749
+ state.buffer.storageOperations.push(...ops);
453
750
  tryFlushing();
454
751
  }
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
- });
752
+ let _getInitialStatePromise = null;
753
+ let _getInitialStateResolver = null;
470
754
  function getStorage() {
471
755
  return __awaiter(this, void 0, void 0, function* () {
472
- const doc = yield storage.getDocument();
756
+ if (state.root) {
757
+ return {
758
+ root: state.root,
759
+ };
760
+ }
761
+ if (_getInitialStatePromise == null) {
762
+ state.buffer.messages.push({ type: ClientMessageType.FetchStorage });
763
+ tryFlushing();
764
+ _getInitialStatePromise = new Promise((resolve) => (_getInitialStateResolver = resolve));
765
+ }
766
+ yield _getInitialStatePromise;
473
767
  return {
474
- root: doc.root,
768
+ root: state.root,
475
769
  };
476
770
  });
477
771
  }
478
772
  function undo() {
479
- storage.undo();
773
+ if (state.isBatching) {
774
+ throw new Error("undo is not allowed during a batch");
775
+ }
776
+ const historyItem = state.undoStack.pop();
777
+ if (historyItem == null) {
778
+ return;
779
+ }
780
+ state.isHistoryPaused = false;
781
+ const result = apply(historyItem);
782
+ notify(result.updates);
783
+ state.redoStack.push(result.reverse);
784
+ for (const op of historyItem) {
785
+ if (op.type !== "presence") {
786
+ state.buffer.storageOperations.push(op);
787
+ }
788
+ }
789
+ tryFlushing();
480
790
  }
481
791
  function redo() {
482
- storage.redo();
792
+ if (state.isBatching) {
793
+ throw new Error("redo is not allowed during a batch");
794
+ }
795
+ const historyItem = state.redoStack.pop();
796
+ if (historyItem == null) {
797
+ return;
798
+ }
799
+ state.isHistoryPaused = false;
800
+ const result = apply(historyItem);
801
+ notify(result.updates);
802
+ state.undoStack.push(result.reverse);
803
+ for (const op of historyItem) {
804
+ if (op.type !== "presence") {
805
+ state.buffer.storageOperations.push(op);
806
+ }
807
+ }
808
+ tryFlushing();
809
+ }
810
+ function batch(callback) {
811
+ if (state.isBatching) {
812
+ throw new Error("batch should not be called during a batch");
813
+ }
814
+ state.isBatching = true;
815
+ try {
816
+ callback();
817
+ }
818
+ finally {
819
+ state.isBatching = false;
820
+ if (state.batch.reverseOps.length > 0) {
821
+ addToUndoStack(state.batch.reverseOps);
822
+ }
823
+ // Clear the redo stack because batch is always called from a local operation
824
+ state.redoStack = [];
825
+ if (state.batch.ops.length > 0) {
826
+ dispatch(state.batch.ops);
827
+ }
828
+ notify(state.batch.updates);
829
+ state.batch = {
830
+ ops: [],
831
+ reverseOps: [],
832
+ updates: {
833
+ others: [],
834
+ nodes: new Set(),
835
+ presence: false,
836
+ },
837
+ };
838
+ tryFlushing();
839
+ }
840
+ }
841
+ function pauseHistory() {
842
+ state.pausedHistory = [];
843
+ state.isHistoryPaused = true;
844
+ }
845
+ function resumeHistory() {
846
+ state.isHistoryPaused = false;
847
+ if (state.pausedHistory.length > 0) {
848
+ addToUndoStack(state.pausedHistory);
849
+ }
850
+ state.pausedHistory = [];
483
851
  }
484
852
  return {
485
853
  // Internal
@@ -491,6 +859,8 @@ export function makeStateMachine(state, context, mockedEffects) {
491
859
  onNavigatorOnline,
492
860
  // onWakeUp,
493
861
  onVisibilityChange,
862
+ getUndoStack: () => state.undoStack,
863
+ getItemsCount: () => state.items.size,
494
864
  // Core
495
865
  connect,
496
866
  disconnect,
@@ -499,8 +869,11 @@ export function makeStateMachine(state, context, mockedEffects) {
499
869
  // Presence
500
870
  updatePresence,
501
871
  broadcastEvent,
872
+ batch,
502
873
  undo,
503
874
  redo,
875
+ pauseHistory,
876
+ resumeHistory,
504
877
  getStorage,
505
878
  selectors: {
506
879
  // Core
@@ -522,6 +895,7 @@ export function defaultState(me, defaultStorageRoot) {
522
895
  "my-presence": [],
523
896
  error: [],
524
897
  connection: [],
898
+ storage: [],
525
899
  },
526
900
  numberOfRetry: 0,
527
901
  lastFlushTime: 0,
@@ -530,7 +904,7 @@ export function defaultState(me, defaultStorageRoot) {
530
904
  reconnect: 0,
531
905
  pongTimeout: 0,
532
906
  },
533
- flushData: {
907
+ buffer: {
534
908
  presence: me == null ? {} : me,
535
909
  messages: [],
536
910
  storageOperations: [],
@@ -543,11 +917,26 @@ export function defaultState(me, defaultStorageRoot) {
543
917
  others: makeOthers({}),
544
918
  defaultStorageRoot,
545
919
  idFactory: null,
920
+ // Storage
921
+ clock: 0,
922
+ opClock: 0,
923
+ items: new Map(),
924
+ root: undefined,
925
+ undoStack: [],
926
+ redoStack: [],
927
+ isHistoryPaused: false,
928
+ pausedHistory: [],
929
+ isBatching: false,
930
+ batch: {
931
+ ops: [],
932
+ updates: { nodes: new Set(), presence: false, others: [] },
933
+ reverseOps: [],
934
+ },
546
935
  };
547
936
  }
548
937
  export function createRoom(name, options) {
549
938
  const throttleDelay = options.throttle || 100;
550
- const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net/v4";
939
+ const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net/v5";
551
940
  let authEndpoint;
552
941
  if (options.authEndpoint) {
553
942
  authEndpoint = options.authEndpoint;
@@ -581,8 +970,13 @@ export function createRoom(name, options) {
581
970
  getOthers: machine.selectors.getOthers,
582
971
  broadcastEvent: machine.broadcastEvent,
583
972
  getStorage: machine.getStorage,
584
- undo: machine.undo,
585
- redo: machine.redo,
973
+ batch: machine.batch,
974
+ history: {
975
+ undo: machine.undo,
976
+ redo: machine.redo,
977
+ pause: machine.pauseHistory,
978
+ resume: machine.resumeHistory,
979
+ },
586
980
  };
587
981
  return {
588
982
  connect: machine.connect,