@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/cjs/room.js CHANGED
@@ -27,15 +27,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
27
27
  step((generator = generator.apply(thisArg, _arguments || [])).next());
28
28
  });
29
29
  };
30
- var __importDefault = (this && this.__importDefault) || function (mod) {
31
- return (mod && mod.__esModule) ? mod : { "default": mod };
32
- };
33
30
  Object.defineProperty(exports, "__esModule", { value: true });
34
31
  exports.createRoom = exports.defaultState = exports.makeStateMachine = void 0;
35
32
  const utils_1 = require("./utils");
36
33
  const authentication_1 = __importStar(require("./authentication"));
37
34
  const live_1 = require("./live");
38
- const storage_1 = __importDefault(require("./storage"));
35
+ const LiveMap_1 = require("./LiveMap");
36
+ const LiveObject_1 = require("./LiveObject");
37
+ const LiveList_1 = require("./LiveList");
38
+ const AbstractCrdt_1 = require("./AbstractCrdt");
39
+ const LiveRegister_1 = require("./LiveRegister");
39
40
  const BACKOFF_RETRY_DELAYS = [250, 500, 1000, 2000, 4000, 8000, 10000];
40
41
  const HEARTBEAT_INTERVAL = 30000;
41
42
  // const WAKE_UP_CHECK_INTERVAL = 2000;
@@ -107,13 +108,275 @@ function makeStateMachine(state, context, mockedEffects) {
107
108
  return setTimeout(connect, delay);
108
109
  },
109
110
  };
110
- function subscribe(type, listener) {
111
- if (!isValidRoomEventType(type)) {
112
- throw new Error(`"${type}" is not a valid event name`);
111
+ function genericSubscribe(callback) {
112
+ state.listeners.storage.push(callback);
113
+ return () => (0, utils_1.remove)(state.listeners.storage, callback);
114
+ }
115
+ function crdtSubscribe(crdt, innerCallback, options) {
116
+ const cb = (updates) => {
117
+ const relatedUpdates = [];
118
+ for (const update of updates) {
119
+ if ((options === null || options === void 0 ? void 0 : options.isDeep) && (0, utils_1.isSameNodeOrChildOf)(update.node, crdt)) {
120
+ relatedUpdates.push(update);
121
+ }
122
+ else if (update.node._id === crdt._id) {
123
+ innerCallback(update.node);
124
+ }
125
+ }
126
+ if ((options === null || options === void 0 ? void 0 : options.isDeep) && relatedUpdates.length > 0) {
127
+ innerCallback(relatedUpdates);
128
+ }
129
+ };
130
+ return genericSubscribe(cb);
131
+ }
132
+ function createRootFromMessage(message) {
133
+ state.root = load(message.items);
134
+ for (const key in state.defaultStorageRoot) {
135
+ if (state.root.get(key) == null) {
136
+ state.root.set(key, state.defaultStorageRoot[key]);
137
+ }
138
+ }
139
+ }
140
+ function load(items) {
141
+ if (items.length === 0) {
142
+ throw new Error("Internal error: cannot load storage without items");
143
+ }
144
+ const parentToChildren = new Map();
145
+ let root = null;
146
+ for (const tuple of items) {
147
+ const parentId = tuple[1].parentId;
148
+ if (parentId == null) {
149
+ root = tuple;
150
+ }
151
+ else {
152
+ const children = parentToChildren.get(parentId);
153
+ if (children != null) {
154
+ children.push(tuple);
155
+ }
156
+ else {
157
+ parentToChildren.set(parentId, [tuple]);
158
+ }
159
+ }
160
+ }
161
+ if (root == null) {
162
+ throw new Error("Root can't be null");
163
+ }
164
+ return LiveObject_1.LiveObject._deserialize(root, parentToChildren, {
165
+ addItem,
166
+ deleteItem,
167
+ generateId,
168
+ generateOpId,
169
+ dispatch: storageDispatch,
170
+ });
171
+ }
172
+ function addItem(id, item) {
173
+ state.items.set(id, item);
174
+ }
175
+ function deleteItem(id) {
176
+ state.items.delete(id);
177
+ }
178
+ function getItem(id) {
179
+ return state.items.get(id);
180
+ }
181
+ function addToUndoStack(historyItem) {
182
+ // If undo stack is too large, we remove the older item
183
+ if (state.undoStack.length >= 50) {
184
+ state.undoStack.shift();
185
+ }
186
+ if (state.isHistoryPaused) {
187
+ state.pausedHistory.unshift(...historyItem);
188
+ }
189
+ else {
190
+ state.undoStack.push(historyItem);
191
+ }
192
+ }
193
+ function storageDispatch(ops, reverse, modified) {
194
+ if (state.isBatching) {
195
+ state.batch.ops.push(...ops);
196
+ for (const item of modified) {
197
+ state.batch.updates.nodes.add(item);
198
+ }
199
+ state.batch.reverseOps.push(...reverse);
200
+ }
201
+ else {
202
+ addToUndoStack(reverse);
203
+ state.redoStack = [];
204
+ dispatch(ops);
205
+ notify({ nodes: new Set(modified) });
206
+ }
207
+ }
208
+ function notify({ nodes = new Set(), presence = false, others = [], }) {
209
+ if (others.length > 0) {
210
+ state.others = makeOthers(state.users);
211
+ for (const event of others) {
212
+ for (const listener of state.listeners["others"]) {
213
+ listener(state.others, event);
214
+ }
215
+ }
216
+ }
217
+ if (presence) {
218
+ for (const listener of state.listeners["my-presence"]) {
219
+ listener(state.me);
220
+ }
221
+ }
222
+ if (nodes.size > 0) {
223
+ for (const subscriber of state.listeners.storage) {
224
+ subscriber(Array.from(nodes).map((m) => {
225
+ if (m instanceof LiveObject_1.LiveObject) {
226
+ return {
227
+ type: "LiveObject",
228
+ node: m,
229
+ };
230
+ }
231
+ else if (m instanceof LiveList_1.LiveList) {
232
+ return {
233
+ type: "LiveList",
234
+ node: m,
235
+ };
236
+ }
237
+ else {
238
+ return {
239
+ type: "LiveMap",
240
+ node: m,
241
+ };
242
+ }
243
+ }));
244
+ }
245
+ }
246
+ }
247
+ function getConnectionId() {
248
+ if (state.connection.state === "open" ||
249
+ state.connection.state === "connecting") {
250
+ return state.connection.id;
251
+ }
252
+ throw new Error("Internal. Tried to get connection id but connection is not open");
253
+ }
254
+ function generateId() {
255
+ return `${getConnectionId()}:${state.clock++}`;
256
+ }
257
+ function generateOpId() {
258
+ return `${getConnectionId()}:${state.opClock++}`;
259
+ }
260
+ function apply(item) {
261
+ const result = {
262
+ reverse: [],
263
+ updates: { nodes: new Set(), presence: false },
264
+ };
265
+ for (const op of item) {
266
+ if (op.type === "presence") {
267
+ const reverse = {
268
+ type: "presence",
269
+ data: {},
270
+ };
271
+ for (const key in op.data) {
272
+ reverse.data[key] = state.me[key];
273
+ }
274
+ state.me = Object.assign(Object.assign({}, state.me), op.data);
275
+ if (state.buffer.presence == null) {
276
+ state.buffer.presence = op.data;
277
+ }
278
+ else {
279
+ for (const key in op.data) {
280
+ state.buffer.presence[key] = op.data;
281
+ }
282
+ }
283
+ result.reverse.unshift(reverse);
284
+ result.updates.presence = true;
285
+ }
286
+ else {
287
+ const applyOpResult = applyOp(op);
288
+ if (applyOpResult.modified) {
289
+ result.updates.nodes.add(applyOpResult.modified);
290
+ result.reverse.unshift(...applyOpResult.reverse);
291
+ }
292
+ }
293
+ }
294
+ return result;
295
+ }
296
+ function applyOp(op) {
297
+ switch (op.type) {
298
+ case live_1.OpType.DeleteObjectKey:
299
+ case live_1.OpType.UpdateObject:
300
+ case live_1.OpType.DeleteCrdt: {
301
+ const item = state.items.get(op.id);
302
+ if (item == null) {
303
+ return { modified: false };
304
+ }
305
+ return item._apply(op);
306
+ }
307
+ case live_1.OpType.SetParentKey: {
308
+ const item = state.items.get(op.id);
309
+ if (item == null) {
310
+ return { modified: false };
311
+ }
312
+ if (item._parent instanceof LiveList_1.LiveList) {
313
+ const previousKey = item._parentKey;
314
+ item._parent._setChildKey(op.parentKey, item);
315
+ return {
316
+ reverse: [
317
+ {
318
+ type: live_1.OpType.SetParentKey,
319
+ id: item._id,
320
+ parentKey: previousKey,
321
+ },
322
+ ],
323
+ modified: item._parent,
324
+ };
325
+ }
326
+ return { modified: false };
327
+ }
328
+ case live_1.OpType.CreateObject: {
329
+ const parent = state.items.get(op.parentId);
330
+ if (parent == null || getItem(op.id) != null) {
331
+ return { modified: false };
332
+ }
333
+ return parent._attachChild(op.id, op.parentKey, new LiveObject_1.LiveObject(op.data));
334
+ }
335
+ case live_1.OpType.CreateList: {
336
+ const parent = state.items.get(op.parentId);
337
+ if (parent == null || getItem(op.id) != null) {
338
+ return { modified: false };
339
+ }
340
+ return parent._attachChild(op.id, op.parentKey, new LiveList_1.LiveList());
341
+ }
342
+ case live_1.OpType.CreateRegister: {
343
+ const parent = state.items.get(op.parentId);
344
+ if (parent == null || getItem(op.id) != null) {
345
+ return { modified: false };
346
+ }
347
+ return parent._attachChild(op.id, op.parentKey, new LiveRegister_1.LiveRegister(op.data));
348
+ }
349
+ case live_1.OpType.CreateMap: {
350
+ const parent = state.items.get(op.parentId);
351
+ if (parent == null || getItem(op.id) != null) {
352
+ return { modified: false };
353
+ }
354
+ return parent._attachChild(op.id, op.parentKey, new LiveMap_1.LiveMap());
355
+ }
113
356
  }
114
- state.listeners[type].push(listener);
357
+ return { modified: false };
358
+ }
359
+ function subscribe(firstParam, listener, options) {
360
+ if (firstParam instanceof AbstractCrdt_1.AbstractCrdt) {
361
+ return crdtSubscribe(firstParam, listener, options);
362
+ }
363
+ else if (typeof firstParam === "function") {
364
+ return genericSubscribe(firstParam);
365
+ }
366
+ else if (!isValidRoomEventType(firstParam)) {
367
+ throw new Error(`"${firstParam}" is not a valid event name`);
368
+ }
369
+ state.listeners[firstParam].push(listener);
370
+ return () => {
371
+ const callbacks = state.listeners[firstParam];
372
+ (0, utils_1.remove)(callbacks, listener);
373
+ };
115
374
  }
116
375
  function unsubscribe(event, callback) {
376
+ console.warn(`unsubscribe is depreacted and will be removed in a future version.
377
+ use the callback returned by subscribe instead.
378
+ See v0.13 release notes for more information.
379
+ `);
117
380
  if (!isValidRoomEventType(event)) {
118
381
  throw new Error(`"${event}" is not a valid event name`);
119
382
  }
@@ -145,20 +408,28 @@ function makeStateMachine(state, context, mockedEffects) {
145
408
  updateConnection({ state: "authenticating" });
146
409
  effects.authenticate();
147
410
  }
148
- function updatePresence(overrides) {
149
- const newPresence = Object.assign(Object.assign({}, state.me), overrides);
150
- if (state.flushData.presence == null) {
151
- state.flushData.presence = overrides;
411
+ function updatePresence(overrides, options) {
412
+ const oldValues = {};
413
+ if (state.buffer.presence == null) {
414
+ state.buffer.presence = {};
152
415
  }
153
- else {
154
- for (const key in overrides) {
155
- state.flushData.presence[key] = overrides[key];
416
+ for (const key in overrides) {
417
+ state.buffer.presence[key] = overrides[key];
418
+ oldValues[key] = state.me[key];
419
+ }
420
+ state.me = Object.assign(Object.assign({}, state.me), overrides);
421
+ if (state.isBatching) {
422
+ if (options === null || options === void 0 ? void 0 : options.addToHistory) {
423
+ state.batch.reverseOps.push({ type: "presence", data: oldValues });
156
424
  }
425
+ state.batch.updates.presence = true;
157
426
  }
158
- state.me = newPresence;
159
- tryFlushing();
160
- for (const listener of state.listeners["my-presence"]) {
161
- listener(state.me);
427
+ else {
428
+ tryFlushing();
429
+ if (options === null || options === void 0 ? void 0 : options.addToHistory) {
430
+ addToUndoStack([{ type: "presence", data: oldValues }]);
431
+ }
432
+ notify({ presence: true });
162
433
  }
163
434
  }
164
435
  function authenticationSuccess(token, socket) {
@@ -199,25 +470,20 @@ function makeStateMachine(state, context, mockedEffects) {
199
470
  presence: Object.assign(Object.assign({}, user.presence), message.data),
200
471
  };
201
472
  }
202
- updateUsers({
473
+ return {
203
474
  type: "update",
204
475
  updates: message.data,
205
476
  user: state.users[message.actor],
206
- });
207
- }
208
- function updateUsers(event) {
209
- state.others = makeOthers(state.users);
210
- for (const listener of state.listeners["others"]) {
211
- listener(state.others, event);
212
- }
477
+ };
213
478
  }
214
479
  function onUserLeftMessage(message) {
215
480
  const userLeftMessage = message;
216
481
  const user = state.users[userLeftMessage.actor];
217
482
  if (user) {
218
483
  delete state.users[userLeftMessage.actor];
219
- updateUsers({ type: "leave", user });
484
+ return { type: "leave", user };
220
485
  }
486
+ return null;
221
487
  }
222
488
  function onRoomStateMessage(message) {
223
489
  const newUsers = {};
@@ -231,7 +497,7 @@ function makeStateMachine(state, context, mockedEffects) {
231
497
  };
232
498
  }
233
499
  state.users = newUsers;
234
- updateUsers({ type: "reset" });
500
+ return { type: "reset" };
235
501
  }
236
502
  function onNavigatorOnline() {
237
503
  if (state.connection.state === "unavailable") {
@@ -250,17 +516,17 @@ function makeStateMachine(state, context, mockedEffects) {
250
516
  info: message.info,
251
517
  id: message.id,
252
518
  };
253
- updateUsers({ type: "enter", user: state.users[message.actor] });
254
519
  if (state.me) {
255
520
  // Send current presence to new user
256
521
  // TODO: Consider storing it on the backend
257
- state.flushData.messages.push({
522
+ state.buffer.messages.push({
258
523
  type: live_1.ClientMessageType.UpdatePresence,
259
524
  data: state.me,
260
525
  targetActor: message.actor,
261
526
  });
262
527
  tryFlushing();
263
528
  }
529
+ return { type: "enter", user: state.users[message.actor] };
264
530
  }
265
531
  function onMessage(event) {
266
532
  if (event.data === "pong") {
@@ -268,29 +534,57 @@ function makeStateMachine(state, context, mockedEffects) {
268
534
  return;
269
535
  }
270
536
  const message = JSON.parse(event.data);
271
- switch (message.type) {
272
- case live_1.ServerMessageType.UserJoined: {
273
- onUserJoinedMessage(message);
274
- break;
275
- }
276
- case live_1.ServerMessageType.UpdatePresence: {
277
- onUpdatePresenceMessage(message);
278
- break;
279
- }
280
- case live_1.ServerMessageType.Event: {
281
- onEvent(message);
282
- break;
283
- }
284
- case live_1.ServerMessageType.UserLeft: {
285
- onUserLeftMessage(message);
286
- break;
287
- }
288
- case live_1.ServerMessageType.RoomState: {
289
- onRoomStateMessage(message);
290
- break;
537
+ let subMessages = [];
538
+ if (Array.isArray(message)) {
539
+ subMessages = message;
540
+ }
541
+ else {
542
+ subMessages.push(message);
543
+ }
544
+ const updates = {
545
+ nodes: new Set(),
546
+ others: [],
547
+ };
548
+ for (const subMessage of subMessages) {
549
+ switch (subMessage.type) {
550
+ case live_1.ServerMessageType.UserJoined: {
551
+ updates.others.push(onUserJoinedMessage(message));
552
+ break;
553
+ }
554
+ case live_1.ServerMessageType.UpdatePresence: {
555
+ updates.others.push(onUpdatePresenceMessage(subMessage));
556
+ break;
557
+ }
558
+ case live_1.ServerMessageType.Event: {
559
+ onEvent(subMessage);
560
+ break;
561
+ }
562
+ case live_1.ServerMessageType.UserLeft: {
563
+ const event = onUserLeftMessage(subMessage);
564
+ if (event) {
565
+ updates.others.push(event);
566
+ }
567
+ break;
568
+ }
569
+ case live_1.ServerMessageType.RoomState: {
570
+ updates.others.push(onRoomStateMessage(subMessage));
571
+ break;
572
+ }
573
+ case live_1.ServerMessageType.InitialStorageState: {
574
+ createRootFromMessage(subMessage);
575
+ _getInitialStateResolver === null || _getInitialStateResolver === void 0 ? void 0 : _getInitialStateResolver();
576
+ break;
577
+ }
578
+ case live_1.ServerMessageType.UpdateStorage: {
579
+ const applyResult = apply(subMessage.ops);
580
+ for (const node of applyResult.updates.nodes) {
581
+ updates.nodes.add(node);
582
+ }
583
+ break;
584
+ }
291
585
  }
292
586
  }
293
- storage.onMessage(message);
587
+ notify(updates);
294
588
  }
295
589
  // function onWakeUp() {
296
590
  // // Sometimes, the browser can put the webpage on pause (computer is on sleep mode for example)
@@ -310,7 +604,7 @@ function makeStateMachine(state, context, mockedEffects) {
310
604
  }
311
605
  clearTimeout(state.timeoutHandles.reconnect);
312
606
  state.users = {};
313
- updateUsers({ type: "reset" });
607
+ notify({ others: [{ type: "reset" }] });
314
608
  if (event.code >= 4000 && event.code <= 4100) {
315
609
  updateConnection({ state: "failed" });
316
610
  const error = new LiveblocksError(event.reason, event.code);
@@ -399,7 +693,7 @@ function makeStateMachine(state, context, mockedEffects) {
399
693
  return;
400
694
  }
401
695
  effects.send(messages);
402
- state.flushData = {
696
+ state.buffer = {
403
697
  messages: [],
404
698
  storageOperations: [],
405
699
  presence: null,
@@ -415,19 +709,19 @@ function makeStateMachine(state, context, mockedEffects) {
415
709
  }
416
710
  function flushDataToMessages(state) {
417
711
  const messages = [];
418
- if (state.flushData.presence) {
712
+ if (state.buffer.presence) {
419
713
  messages.push({
420
714
  type: live_1.ClientMessageType.UpdatePresence,
421
- data: state.flushData.presence,
715
+ data: state.buffer.presence,
422
716
  });
423
717
  }
424
- for (const event of state.flushData.messages) {
718
+ for (const event of state.buffer.messages) {
425
719
  messages.push(event);
426
720
  }
427
- if (state.flushData.storageOperations.length > 0) {
721
+ if (state.buffer.storageOperations.length > 0) {
428
722
  messages.push({
429
723
  type: live_1.ClientMessageType.UpdateStorage,
430
- ops: state.flushData.storageOperations,
724
+ ops: state.buffer.storageOperations,
431
725
  });
432
726
  }
433
727
  return messages;
@@ -449,7 +743,7 @@ function makeStateMachine(state, context, mockedEffects) {
449
743
  clearTimeout(state.timeoutHandles.pongTimeout);
450
744
  clearInterval(state.intervalHandles.heartbeat);
451
745
  state.users = {};
452
- updateUsers({ type: "reset" });
746
+ notify({ others: [{ type: "reset" }] });
453
747
  clearListeners();
454
748
  }
455
749
  function clearListeners() {
@@ -467,44 +761,115 @@ function makeStateMachine(state, context, mockedEffects) {
467
761
  if (state.socket == null) {
468
762
  return;
469
763
  }
470
- state.flushData.messages.push({
764
+ state.buffer.messages.push({
471
765
  type: live_1.ClientMessageType.ClientEvent,
472
766
  event,
473
767
  });
474
768
  tryFlushing();
475
769
  }
476
770
  function dispatch(ops) {
477
- state.flushData.storageOperations.push(...ops);
771
+ state.buffer.storageOperations.push(...ops);
478
772
  tryFlushing();
479
773
  }
480
- const storage = new storage_1.default({
481
- fetchStorage: () => {
482
- state.flushData.messages.push({ type: live_1.ClientMessageType.FetchStorage });
483
- tryFlushing();
484
- },
485
- dispatch,
486
- getConnectionId: () => {
487
- const me = getSelf();
488
- if (me) {
489
- return me.connectionId;
490
- }
491
- throw new Error("Unexpected");
492
- },
493
- defaultRoot: state.defaultStorageRoot,
494
- });
774
+ let _getInitialStatePromise = null;
775
+ let _getInitialStateResolver = null;
495
776
  function getStorage() {
496
777
  return __awaiter(this, void 0, void 0, function* () {
497
- const doc = yield storage.getDocument();
778
+ if (state.root) {
779
+ return {
780
+ root: state.root,
781
+ };
782
+ }
783
+ if (_getInitialStatePromise == null) {
784
+ state.buffer.messages.push({ type: live_1.ClientMessageType.FetchStorage });
785
+ tryFlushing();
786
+ _getInitialStatePromise = new Promise((resolve) => (_getInitialStateResolver = resolve));
787
+ }
788
+ yield _getInitialStatePromise;
498
789
  return {
499
- root: doc.root,
790
+ root: state.root,
500
791
  };
501
792
  });
502
793
  }
503
794
  function undo() {
504
- storage.undo();
795
+ if (state.isBatching) {
796
+ throw new Error("undo is not allowed during a batch");
797
+ }
798
+ const historyItem = state.undoStack.pop();
799
+ if (historyItem == null) {
800
+ return;
801
+ }
802
+ state.isHistoryPaused = false;
803
+ const result = apply(historyItem);
804
+ notify(result.updates);
805
+ state.redoStack.push(result.reverse);
806
+ for (const op of historyItem) {
807
+ if (op.type !== "presence") {
808
+ state.buffer.storageOperations.push(op);
809
+ }
810
+ }
811
+ tryFlushing();
505
812
  }
506
813
  function redo() {
507
- storage.redo();
814
+ if (state.isBatching) {
815
+ throw new Error("redo is not allowed during a batch");
816
+ }
817
+ const historyItem = state.redoStack.pop();
818
+ if (historyItem == null) {
819
+ return;
820
+ }
821
+ state.isHistoryPaused = false;
822
+ const result = apply(historyItem);
823
+ notify(result.updates);
824
+ state.undoStack.push(result.reverse);
825
+ for (const op of historyItem) {
826
+ if (op.type !== "presence") {
827
+ state.buffer.storageOperations.push(op);
828
+ }
829
+ }
830
+ tryFlushing();
831
+ }
832
+ function batch(callback) {
833
+ if (state.isBatching) {
834
+ throw new Error("batch should not be called during a batch");
835
+ }
836
+ state.isBatching = true;
837
+ try {
838
+ callback();
839
+ }
840
+ finally {
841
+ state.isBatching = false;
842
+ if (state.batch.reverseOps.length > 0) {
843
+ addToUndoStack(state.batch.reverseOps);
844
+ }
845
+ // Clear the redo stack because batch is always called from a local operation
846
+ state.redoStack = [];
847
+ if (state.batch.ops.length > 0) {
848
+ dispatch(state.batch.ops);
849
+ }
850
+ notify(state.batch.updates);
851
+ state.batch = {
852
+ ops: [],
853
+ reverseOps: [],
854
+ updates: {
855
+ others: [],
856
+ nodes: new Set(),
857
+ presence: false,
858
+ },
859
+ };
860
+ tryFlushing();
861
+ }
862
+ }
863
+ function pauseHistory() {
864
+ state.pausedHistory = [];
865
+ state.isHistoryPaused = true;
866
+ }
867
+ function resumeHistory() {
868
+ state.isHistoryPaused = false;
869
+ if (state.pausedHistory.length > 0) {
870
+ addToUndoStack(state.pausedHistory);
871
+ }
872
+ state.pausedHistory = [];
508
873
  }
509
874
  return {
510
875
  // Internal
@@ -516,6 +881,8 @@ function makeStateMachine(state, context, mockedEffects) {
516
881
  onNavigatorOnline,
517
882
  // onWakeUp,
518
883
  onVisibilityChange,
884
+ getUndoStack: () => state.undoStack,
885
+ getItemsCount: () => state.items.size,
519
886
  // Core
520
887
  connect,
521
888
  disconnect,
@@ -524,8 +891,11 @@ function makeStateMachine(state, context, mockedEffects) {
524
891
  // Presence
525
892
  updatePresence,
526
893
  broadcastEvent,
894
+ batch,
527
895
  undo,
528
896
  redo,
897
+ pauseHistory,
898
+ resumeHistory,
529
899
  getStorage,
530
900
  selectors: {
531
901
  // Core
@@ -548,6 +918,7 @@ function defaultState(me, defaultStorageRoot) {
548
918
  "my-presence": [],
549
919
  error: [],
550
920
  connection: [],
921
+ storage: [],
551
922
  },
552
923
  numberOfRetry: 0,
553
924
  lastFlushTime: 0,
@@ -556,7 +927,7 @@ function defaultState(me, defaultStorageRoot) {
556
927
  reconnect: 0,
557
928
  pongTimeout: 0,
558
929
  },
559
- flushData: {
930
+ buffer: {
560
931
  presence: me == null ? {} : me,
561
932
  messages: [],
562
933
  storageOperations: [],
@@ -569,12 +940,27 @@ function defaultState(me, defaultStorageRoot) {
569
940
  others: makeOthers({}),
570
941
  defaultStorageRoot,
571
942
  idFactory: null,
943
+ // Storage
944
+ clock: 0,
945
+ opClock: 0,
946
+ items: new Map(),
947
+ root: undefined,
948
+ undoStack: [],
949
+ redoStack: [],
950
+ isHistoryPaused: false,
951
+ pausedHistory: [],
952
+ isBatching: false,
953
+ batch: {
954
+ ops: [],
955
+ updates: { nodes: new Set(), presence: false, others: [] },
956
+ reverseOps: [],
957
+ },
572
958
  };
573
959
  }
574
960
  exports.defaultState = defaultState;
575
961
  function createRoom(name, options) {
576
962
  const throttleDelay = options.throttle || 100;
577
- const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net/v4";
963
+ const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net/v5";
578
964
  let authEndpoint;
579
965
  if (options.authEndpoint) {
580
966
  authEndpoint = options.authEndpoint;
@@ -608,8 +994,13 @@ function createRoom(name, options) {
608
994
  getOthers: machine.selectors.getOthers,
609
995
  broadcastEvent: machine.broadcastEvent,
610
996
  getStorage: machine.getStorage,
611
- undo: machine.undo,
612
- redo: machine.redo,
997
+ batch: machine.batch,
998
+ history: {
999
+ undo: machine.undo,
1000
+ redo: machine.redo,
1001
+ pause: machine.pauseHistory,
1002
+ resume: machine.resumeHistory,
1003
+ },
613
1004
  };
614
1005
  return {
615
1006
  connect: machine.connect,