@liveblocks/client 0.13.1 → 0.14.0

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/lib/esm/room.js CHANGED
@@ -7,7 +7,7 @@ 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 { isSameNodeOrChildOf, remove } from "./utils";
10
+ import { getTreesDiffOperations, isSameNodeOrChildOf, remove } from "./utils";
11
11
  import auth, { parseToken } from "./authentication";
12
12
  import { ClientMessageType, ServerMessageType, OpType, } from "./live";
13
13
  import { LiveMap } from "./LiveMap";
@@ -36,6 +36,9 @@ function makeOthers(presenceMap) {
36
36
  get count() {
37
37
  return array.length;
38
38
  },
39
+ [Symbol.iterator]() {
40
+ return array[Symbol.iterator]();
41
+ },
39
42
  map(callback) {
40
43
  return array.map(callback);
41
44
  },
@@ -107,18 +110,23 @@ export function makeStateMachine(state, context, mockedEffects) {
107
110
  };
108
111
  return genericSubscribe(cb);
109
112
  }
110
- function createRootFromMessage(message) {
111
- state.root = load(message.items);
113
+ function createOrUpdateRootFromMessage(message) {
114
+ if (message.items.length === 0) {
115
+ throw new Error("Internal error: cannot load storage without items");
116
+ }
117
+ if (state.root) {
118
+ updateRoot(message.items);
119
+ }
120
+ else {
121
+ state.root = load(message.items);
122
+ }
112
123
  for (const key in state.defaultStorageRoot) {
113
124
  if (state.root.get(key) == null) {
114
125
  state.root.set(key, state.defaultStorageRoot[key]);
115
126
  }
116
127
  }
117
128
  }
118
- function load(items) {
119
- if (items.length === 0) {
120
- throw new Error("Internal error: cannot load storage without items");
121
- }
129
+ function buildRootAndParentToChildren(items) {
122
130
  const parentToChildren = new Map();
123
131
  let root = null;
124
132
  for (const tuple of items) {
@@ -139,6 +147,23 @@ export function makeStateMachine(state, context, mockedEffects) {
139
147
  if (root == null) {
140
148
  throw new Error("Root can't be null");
141
149
  }
150
+ return [root, parentToChildren];
151
+ }
152
+ function updateRoot(items) {
153
+ if (!state.root) {
154
+ return;
155
+ }
156
+ const currentItems = new Map();
157
+ state.items.forEach((liveCrdt, id) => {
158
+ currentItems.set(id, liveCrdt._toSerializedCrdt());
159
+ });
160
+ // Get operations that represent the diff between 2 states.
161
+ const ops = getTreesDiffOperations(currentItems, new Map(items));
162
+ const result = apply(ops, false);
163
+ notify(result.updates);
164
+ }
165
+ function load(items) {
166
+ const [root, parentToChildren] = buildRootAndParentToChildren(items);
142
167
  return LiveObject._deserialize(root, parentToChildren, {
143
168
  addItem,
144
169
  deleteItem,
@@ -227,7 +252,10 @@ export function makeStateMachine(state, context, mockedEffects) {
227
252
  state.connection.state === "connecting") {
228
253
  return state.connection.id;
229
254
  }
230
- throw new Error("Internal. Tried to get connection id but connection is not open");
255
+ else if (state.lastConnectionId !== null) {
256
+ return state.lastConnectionId;
257
+ }
258
+ throw new Error("Internal. Tried to get connection id but connection was never open");
231
259
  }
232
260
  function generateId() {
233
261
  return `${getConnectionId()}:${state.clock++}`;
@@ -235,7 +263,7 @@ export function makeStateMachine(state, context, mockedEffects) {
235
263
  function generateOpId() {
236
264
  return `${getConnectionId()}:${state.opClock++}`;
237
265
  }
238
- function apply(item) {
266
+ function apply(item, isLocal) {
239
267
  const result = {
240
268
  reverse: [],
241
269
  updates: { nodes: new Set(), presence: false },
@@ -262,7 +290,11 @@ export function makeStateMachine(state, context, mockedEffects) {
262
290
  result.updates.presence = true;
263
291
  }
264
292
  else {
265
- const applyOpResult = applyOp(op);
293
+ // Ops applied after undo/redo don't have an opId.
294
+ if (isLocal && !op.opId) {
295
+ op.opId = generateOpId();
296
+ }
297
+ const applyOpResult = applyOp(op, isLocal);
266
298
  if (applyOpResult.modified) {
267
299
  result.updates.nodes.add(applyOpResult.modified);
268
300
  result.reverse.unshift(...applyOpResult.reverse);
@@ -271,7 +303,10 @@ export function makeStateMachine(state, context, mockedEffects) {
271
303
  }
272
304
  return result;
273
305
  }
274
- function applyOp(op) {
306
+ function applyOp(op, isLocal) {
307
+ if (op.opId) {
308
+ state.offlineOperations.delete(op.opId);
309
+ }
275
310
  switch (op.type) {
276
311
  case OpType.DeleteObjectKey:
277
312
  case OpType.UpdateObject:
@@ -280,7 +315,7 @@ export function makeStateMachine(state, context, mockedEffects) {
280
315
  if (item == null) {
281
316
  return { modified: false };
282
317
  }
283
- return item._apply(op);
318
+ return item._apply(op, isLocal);
284
319
  }
285
320
  case OpType.SetParentKey: {
286
321
  const item = state.items.get(op.id);
@@ -308,28 +343,28 @@ export function makeStateMachine(state, context, mockedEffects) {
308
343
  if (parent == null || getItem(op.id) != null) {
309
344
  return { modified: false };
310
345
  }
311
- return parent._attachChild(op.id, op.parentKey, new LiveObject(op.data));
346
+ return parent._attachChild(op.id, op.parentKey, new LiveObject(op.data), isLocal);
312
347
  }
313
348
  case OpType.CreateList: {
314
349
  const parent = state.items.get(op.parentId);
315
350
  if (parent == null || getItem(op.id) != null) {
316
351
  return { modified: false };
317
352
  }
318
- return parent._attachChild(op.id, op.parentKey, new LiveList());
353
+ return parent._attachChild(op.id, op.parentKey, new LiveList(), isLocal);
319
354
  }
320
355
  case OpType.CreateRegister: {
321
356
  const parent = state.items.get(op.parentId);
322
357
  if (parent == null || getItem(op.id) != null) {
323
358
  return { modified: false };
324
359
  }
325
- return parent._attachChild(op.id, op.parentKey, new LiveRegister(op.data));
360
+ return parent._attachChild(op.id, op.parentKey, new LiveRegister(op.data), isLocal);
326
361
  }
327
362
  case OpType.CreateMap: {
328
363
  const parent = state.items.get(op.parentId);
329
364
  if (parent == null || getItem(op.id) != null) {
330
365
  return { modified: false };
331
366
  }
332
- return parent._attachChild(op.id, op.parentKey, new LiveMap());
367
+ return parent._attachChild(op.id, op.parentKey, new LiveMap(), isLocal);
333
368
  }
334
369
  }
335
370
  return { modified: false };
@@ -421,7 +456,9 @@ See v0.13 release notes for more information.
421
456
  state.socket = socket;
422
457
  }
423
458
  function authenticationFailure(error) {
424
- console.error(error);
459
+ if (process.env.NODE_ENV !== "production") {
460
+ console.error("Call to authentication endpoint failed", error);
461
+ }
425
462
  updateConnection({ state: "unavailable" });
426
463
  state.numberOfRetry++;
427
464
  state.timeoutHandles.reconnect = effects.scheduleReconnect(getRetryDelay());
@@ -549,12 +586,13 @@ See v0.13 release notes for more information.
549
586
  break;
550
587
  }
551
588
  case ServerMessageType.InitialStorageState: {
552
- createRootFromMessage(subMessage);
589
+ createOrUpdateRootFromMessage(subMessage);
590
+ applyAndSendOfflineOps();
553
591
  _getInitialStateResolver === null || _getInitialStateResolver === void 0 ? void 0 : _getInitialStateResolver();
554
592
  break;
555
593
  }
556
594
  case ServerMessageType.UpdateStorage: {
557
- const applyResult = apply(subMessage.ops);
595
+ const applyResult = apply(subMessage.ops, false);
558
596
  for (const node of applyResult.updates.nodes) {
559
597
  updates.nodes.add(node);
560
598
  }
@@ -587,13 +625,20 @@ See v0.13 release notes for more information.
587
625
  updateConnection({ state: "failed" });
588
626
  const error = new LiveblocksError(event.reason, event.code);
589
627
  for (const listener of state.listeners.error) {
628
+ if (process.env.NODE_ENV !== "production") {
629
+ console.error(`Connection to Liveblocks websocket server closed. Reason: ${error.message} (code: ${error.code})`);
630
+ }
590
631
  listener(error);
591
632
  }
592
633
  }
593
634
  else if (event.wasClean === false) {
594
- updateConnection({ state: "unavailable" });
595
635
  state.numberOfRetry++;
596
- state.timeoutHandles.reconnect = effects.scheduleReconnect(getRetryDelay());
636
+ const delay = getRetryDelay();
637
+ if (process.env.NODE_ENV !== "production") {
638
+ console.warn(`Connection to Liveblocks websocket server closed (code: ${event.code}). Retrying in ${delay}ms.`);
639
+ }
640
+ updateConnection({ state: "unavailable" });
641
+ state.timeoutHandles.reconnect = effects.scheduleReconnect(delay);
597
642
  }
598
643
  else {
599
644
  updateConnection({ state: "closed" });
@@ -617,6 +662,10 @@ See v0.13 release notes for more information.
617
662
  if (state.connection.state === "connecting") {
618
663
  updateConnection(Object.assign(Object.assign({}, state.connection), { state: "open" }));
619
664
  state.numberOfRetry = 0;
665
+ state.lastConnectionId = state.connection.id;
666
+ if (state.root) {
667
+ state.buffer.messages.push({ type: ClientMessageType.FetchStorage });
668
+ }
620
669
  tryFlushing();
621
670
  }
622
671
  else {
@@ -656,11 +705,29 @@ See v0.13 release notes for more information.
656
705
  clearInterval(state.intervalHandles.heartbeat);
657
706
  connect();
658
707
  }
659
- function tryFlushing() {
660
- if (state.socket == null) {
708
+ function applyAndSendOfflineOps() {
709
+ if (state.offlineOperations.size === 0) {
661
710
  return;
662
711
  }
663
- if (state.socket.readyState !== WebSocket.OPEN) {
712
+ const messages = [];
713
+ const ops = Array.from(state.offlineOperations.values());
714
+ const result = apply(ops, true);
715
+ messages.push({
716
+ type: ClientMessageType.UpdateStorage,
717
+ ops: ops,
718
+ });
719
+ notify(result.updates);
720
+ effects.send(messages);
721
+ }
722
+ function tryFlushing() {
723
+ const storageOps = state.buffer.storageOperations;
724
+ if (storageOps.length > 0) {
725
+ storageOps.forEach((op) => {
726
+ state.offlineOperations.set(op.opId, op);
727
+ });
728
+ }
729
+ if (state.socket == null || state.socket.readyState !== WebSocket.OPEN) {
730
+ state.buffer.storageOperations = [];
664
731
  return;
665
732
  }
666
733
  const now = Date.now();
@@ -735,8 +802,10 @@ See v0.13 release notes for more information.
735
802
  function getOthers() {
736
803
  return state.others;
737
804
  }
738
- function broadcastEvent(event) {
739
- if (state.socket == null) {
805
+ function broadcastEvent(event, options = {
806
+ shouldQueueEventIfNotReady: false,
807
+ }) {
808
+ if (state.socket == null && options.shouldQueueEventIfNotReady == false) {
740
809
  return;
741
810
  }
742
811
  state.buffer.messages.push({
@@ -778,7 +847,7 @@ See v0.13 release notes for more information.
778
847
  return;
779
848
  }
780
849
  state.isHistoryPaused = false;
781
- const result = apply(historyItem);
850
+ const result = apply(historyItem, true);
782
851
  notify(result.updates);
783
852
  state.redoStack.push(result.reverse);
784
853
  for (const op of historyItem) {
@@ -797,7 +866,7 @@ See v0.13 release notes for more information.
797
866
  return;
798
867
  }
799
868
  state.isHistoryPaused = false;
800
- const result = apply(historyItem);
869
+ const result = apply(historyItem, true);
801
870
  notify(result.updates);
802
871
  state.undoStack.push(result.reverse);
803
872
  for (const op of historyItem) {
@@ -849,6 +918,16 @@ See v0.13 release notes for more information.
849
918
  }
850
919
  state.pausedHistory = [];
851
920
  }
921
+ function simulateSocketClose() {
922
+ if (state.socket) {
923
+ state.socket.close();
924
+ }
925
+ }
926
+ function simulateSendCloseEvent(event) {
927
+ if (state.socket) {
928
+ onClose(event);
929
+ }
930
+ }
852
931
  return {
853
932
  // Internal
854
933
  onOpen,
@@ -857,6 +936,9 @@ See v0.13 release notes for more information.
857
936
  authenticationSuccess,
858
937
  heartbeat,
859
938
  onNavigatorOnline,
939
+ // Internal dev tools
940
+ simulateSocketClose,
941
+ simulateSendCloseEvent,
860
942
  // onWakeUp,
861
943
  onVisibilityChange,
862
944
  getUndoStack: () => state.undoStack,
@@ -888,6 +970,7 @@ See v0.13 release notes for more information.
888
970
  export function defaultState(me, defaultStorageRoot) {
889
971
  return {
890
972
  connection: { state: "closed" },
973
+ lastConnectionId: null,
891
974
  socket: null,
892
975
  listeners: {
893
976
  event: [],
@@ -932,6 +1015,7 @@ export function defaultState(me, defaultStorageRoot) {
932
1015
  updates: { nodes: new Set(), presence: false, others: [] },
933
1016
  reverseOps: [],
934
1017
  },
1018
+ offlineOperations: new Map(),
935
1019
  };
936
1020
  }
937
1021
  export function createRoom(name, options) {
@@ -977,6 +1061,11 @@ export function createRoom(name, options) {
977
1061
  pause: machine.pauseHistory,
978
1062
  resume: machine.resumeHistory,
979
1063
  },
1064
+ // @ts-ignore
1065
+ internalDevTools: {
1066
+ closeWebsocket: machine.simulateSocketClose,
1067
+ sendCloseEvent: machine.simulateSendCloseEvent,
1068
+ },
980
1069
  };
981
1070
  return {
982
1071
  connect: machine.connect,
@@ -28,6 +28,14 @@ export declare type LiveListUpdates<TItem = any> = {
28
28
  type: "LiveList";
29
29
  node: LiveList<TItem>;
30
30
  };
31
+ export declare type BroadcastOptions = {
32
+ /**
33
+ * Whether or not event is queued if the connection is currently closed.
34
+ *
35
+ * ❗ We are not sure if we want to support this option in the future so it might be deprecated to be replaced by something else
36
+ */
37
+ shouldQueueEventIfNotReady: boolean;
38
+ };
31
39
  export declare type StorageUpdate = LiveMapUpdates | LiveObjectUpdates | LiveListUpdates;
32
40
  export declare type StorageCallback = (updates: StorageUpdate[]) => void;
33
41
  export declare type Client = {
@@ -65,6 +73,10 @@ export interface Others<TPresence extends Presence = Presence> {
65
73
  * Number of other users in the room.
66
74
  */
67
75
  readonly count: number;
76
+ /**
77
+ * Returns a new Iterator object that contains the users.
78
+ */
79
+ [Symbol.iterator](): IterableIterator<User<TPresence>>;
68
80
  /**
69
81
  * Returns the array of connected users in room.
70
82
  */
@@ -423,7 +435,7 @@ export declare type Room = {
423
435
  * }
424
436
  * });
425
437
  */
426
- broadcastEvent: (event: any) => void;
438
+ broadcastEvent: (event: any, options?: BroadcastOptions) => void;
427
439
  /**
428
440
  * Get the room's storage asynchronously.
429
441
  * The storage's root is a {@link LiveObject}.
@@ -1,8 +1,9 @@
1
1
  import { AbstractCrdt, Doc } from "./AbstractCrdt";
2
- import { SerializedCrdtWithId } from "./live";
2
+ import { SerializedCrdtWithId, Op, SerializedCrdt } from "./live";
3
3
  export declare function remove<T>(array: T[], item: T): void;
4
4
  export declare function isSameNodeOrChildOf(node: AbstractCrdt, parent: AbstractCrdt): boolean;
5
5
  export declare function deserialize(entry: SerializedCrdtWithId, parentToChildren: Map<string, SerializedCrdtWithId[]>, doc: Doc): AbstractCrdt;
6
6
  export declare function isCrdt(obj: any): obj is AbstractCrdt;
7
7
  export declare function selfOrRegisterValue(obj: AbstractCrdt): any;
8
8
  export declare function selfOrRegister(obj: any): AbstractCrdt;
9
+ export declare function getTreesDiffOperations(currentItems: Map<string, SerializedCrdt>, newItems: Map<string, SerializedCrdt>): Op[];
package/lib/esm/utils.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CrdtType, } from "./live";
1
+ import { CrdtType, OpType, } from "./live";
2
2
  import { LiveList } from "./LiveList";
3
3
  import { LiveMap } from "./LiveMap";
4
4
  import { LiveObject } from "./LiveObject";
@@ -64,3 +64,77 @@ export function selfOrRegister(obj) {
64
64
  return new LiveRegister(obj);
65
65
  }
66
66
  }
67
+ export function getTreesDiffOperations(currentItems, newItems) {
68
+ const ops = [];
69
+ currentItems.forEach((_, id) => {
70
+ if (!newItems.get(id)) {
71
+ // Delete crdt
72
+ ops.push({
73
+ type: OpType.DeleteCrdt,
74
+ id: id,
75
+ });
76
+ }
77
+ });
78
+ newItems.forEach((crdt, id) => {
79
+ const currentCrdt = currentItems.get(id);
80
+ if (currentCrdt) {
81
+ if (crdt.type === CrdtType.Object) {
82
+ if (JSON.stringify(crdt.data) !==
83
+ JSON.stringify(currentCrdt.data)) {
84
+ ops.push({
85
+ type: OpType.UpdateObject,
86
+ id: id,
87
+ data: crdt.data,
88
+ });
89
+ }
90
+ }
91
+ if (crdt.parentKey !== currentCrdt.parentKey) {
92
+ ops.push({
93
+ type: OpType.SetParentKey,
94
+ id: id,
95
+ parentKey: crdt.parentKey,
96
+ });
97
+ }
98
+ }
99
+ else {
100
+ // new Crdt
101
+ switch (crdt.type) {
102
+ case CrdtType.Register:
103
+ ops.push({
104
+ type: OpType.CreateRegister,
105
+ id: id,
106
+ parentId: crdt.parentId,
107
+ parentKey: crdt.parentKey,
108
+ data: crdt.data,
109
+ });
110
+ break;
111
+ case CrdtType.List:
112
+ ops.push({
113
+ type: OpType.CreateList,
114
+ id: id,
115
+ parentId: crdt.parentId,
116
+ parentKey: crdt.parentKey,
117
+ });
118
+ break;
119
+ case CrdtType.Object:
120
+ ops.push({
121
+ type: OpType.CreateObject,
122
+ id: id,
123
+ parentId: crdt.parentId,
124
+ parentKey: crdt.parentKey,
125
+ data: crdt.data,
126
+ });
127
+ break;
128
+ case CrdtType.Map:
129
+ ops.push({
130
+ type: OpType.CreateMap,
131
+ id: id,
132
+ parentId: crdt.parentId,
133
+ parentKey: crdt.parentKey,
134
+ });
135
+ break;
136
+ }
137
+ }
138
+ });
139
+ return ops;
140
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liveblocks/client",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "",
5
5
  "main": "./lib/cjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -38,4 +38,4 @@
38
38
  "url": "https://github.com/liveblocks/liveblocks.git",
39
39
  "directory": "packages/liveblocks"
40
40
  }
41
- }
41
+ }