@liveblocks/client 0.13.0-beta.1 → 0.13.3

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.
@@ -0,0 +1,225 @@
1
+ import { LiveList } from "./LiveList";
2
+ import { LiveMap } from "./LiveMap";
3
+ import { LiveObject } from "./LiveObject";
4
+ import { LiveRegister } from "./LiveRegister";
5
+ export function liveObjectToJson(liveObject) {
6
+ const result = {};
7
+ const obj = liveObject.toObject();
8
+ for (const key in obj) {
9
+ result[key] = liveNodeToJson(obj[key]);
10
+ }
11
+ return result;
12
+ }
13
+ function liveMapToJson(map) {
14
+ const result = {};
15
+ const obj = Object.fromEntries(map);
16
+ for (const key in obj) {
17
+ result[key] = liveNodeToJson(obj[key]);
18
+ }
19
+ return result;
20
+ }
21
+ function liveListToJson(value) {
22
+ return value.toArray().map(liveNodeToJson);
23
+ }
24
+ function liveNodeToJson(value) {
25
+ if (value instanceof LiveObject) {
26
+ return liveObjectToJson(value);
27
+ }
28
+ else if (value instanceof LiveList) {
29
+ return liveListToJson(value);
30
+ }
31
+ else if (value instanceof LiveMap) {
32
+ return liveMapToJson(value);
33
+ }
34
+ else if (value instanceof LiveRegister) {
35
+ return value.data;
36
+ }
37
+ return value;
38
+ }
39
+ function isPlainObject(obj) {
40
+ return Object.prototype.toString.call(obj) === "[object Object]";
41
+ }
42
+ function anyToCrdt(obj) {
43
+ if (obj == null) {
44
+ return obj;
45
+ }
46
+ if (Array.isArray(obj)) {
47
+ return new LiveList(obj.map(anyToCrdt));
48
+ }
49
+ if (isPlainObject(obj)) {
50
+ const init = {};
51
+ for (const key in obj) {
52
+ init[key] = anyToCrdt(obj[key]);
53
+ }
54
+ return new LiveObject(init);
55
+ }
56
+ return obj;
57
+ }
58
+ export function patchLiveList(liveList, prev, next) {
59
+ let i = 0;
60
+ let prevEnd = prev.length - 1;
61
+ let nextEnd = next.length - 1;
62
+ let prevNode = prev[0];
63
+ let nextNode = next[0];
64
+ /**
65
+ * For A,B,C => A,B,C,D
66
+ * i = 3, prevEnd = 2, nextEnd = 3
67
+ *
68
+ * For A,B,C => B,C
69
+ * i = 2, prevEnd = 2, nextEnd = 1
70
+ *
71
+ * For B,C => A,B,C
72
+ * i = 0, pre
73
+ */
74
+ outer: {
75
+ while (prevNode === nextNode) {
76
+ ++i;
77
+ if (i > prevEnd || i > nextEnd) {
78
+ break outer;
79
+ }
80
+ prevNode = prev[i];
81
+ nextNode = next[i];
82
+ }
83
+ prevNode = prev[prevEnd];
84
+ nextNode = next[nextEnd];
85
+ while (prevNode === nextNode) {
86
+ prevEnd--;
87
+ nextEnd--;
88
+ if (i > prevEnd || i > nextEnd) {
89
+ break outer;
90
+ }
91
+ prevNode = prev[prevEnd];
92
+ nextNode = next[nextEnd];
93
+ }
94
+ }
95
+ if (i > prevEnd) {
96
+ if (i <= nextEnd) {
97
+ while (i <= nextEnd) {
98
+ liveList.insert(anyToCrdt(next[i]), i);
99
+ i++;
100
+ }
101
+ }
102
+ }
103
+ else if (i > nextEnd) {
104
+ while (i <= prevEnd) {
105
+ liveList.delete(i++);
106
+ }
107
+ }
108
+ else {
109
+ while (i <= prevEnd && i <= nextEnd) {
110
+ prevNode = prev[i];
111
+ nextNode = next[i];
112
+ const liveListNode = liveList.get(i);
113
+ if (liveListNode instanceof LiveObject &&
114
+ isPlainObject(prevNode) &&
115
+ isPlainObject(nextNode)) {
116
+ patchLiveObject(liveListNode, prevNode, nextNode);
117
+ }
118
+ else {
119
+ liveList.delete(i);
120
+ liveList.insert(anyToCrdt(nextNode), i);
121
+ }
122
+ i++;
123
+ }
124
+ while (i <= nextEnd) {
125
+ liveList.insert(anyToCrdt(next[i]), i);
126
+ i++;
127
+ }
128
+ while (i <= prevEnd) {
129
+ liveList.delete(i);
130
+ i++;
131
+ }
132
+ }
133
+ }
134
+ export function patchLiveObjectKey(liveObject, key, prev, next) {
135
+ const value = liveObject.get(key);
136
+ if (next === undefined) {
137
+ liveObject.delete(key);
138
+ }
139
+ else if (value === undefined) {
140
+ liveObject.set(key, anyToCrdt(next));
141
+ }
142
+ else if (prev === next) {
143
+ return;
144
+ }
145
+ else if (value instanceof LiveList &&
146
+ Array.isArray(prev) &&
147
+ Array.isArray(next)) {
148
+ patchLiveList(value, prev, next);
149
+ }
150
+ else if (value instanceof LiveObject &&
151
+ isPlainObject(prev) &&
152
+ isPlainObject(next)) {
153
+ patchLiveObject(value, prev, next);
154
+ }
155
+ else {
156
+ liveObject.set(key, anyToCrdt(next));
157
+ }
158
+ }
159
+ export function patchLiveObject(root, prev, next) {
160
+ const updates = {};
161
+ for (const key in next) {
162
+ patchLiveObjectKey(root, key, prev[key], next[key]);
163
+ }
164
+ for (const key in prev) {
165
+ if (next[key] === undefined) {
166
+ root.delete(key);
167
+ }
168
+ }
169
+ if (Object.keys(updates).length > 0) {
170
+ root.update(updates);
171
+ }
172
+ }
173
+ function getParentsPath(node) {
174
+ const path = [];
175
+ while (node._parentKey != null && node._parent != null) {
176
+ if (node._parent instanceof LiveList) {
177
+ path.push(node._parent._indexOfPosition(node._parentKey));
178
+ }
179
+ else {
180
+ path.push(node._parentKey);
181
+ }
182
+ node = node._parent;
183
+ }
184
+ return path;
185
+ }
186
+ export function patchImmutableObject(state, updates) {
187
+ return updates.reduce((state, update) => patchImmutableObjectWithUpdate(state, update), state);
188
+ }
189
+ function patchImmutableObjectWithUpdate(state, update) {
190
+ const path = getParentsPath(update.node);
191
+ return patchImmutableNode(state, path, update);
192
+ }
193
+ function patchImmutableNode(state, path, update) {
194
+ const pathItem = path.pop();
195
+ if (pathItem === undefined) {
196
+ switch (update.type) {
197
+ case "LiveObject": {
198
+ if (typeof state !== "object") {
199
+ throw new Error("Internal: received update on LiveObject but state was not an object");
200
+ }
201
+ return liveObjectToJson(update.node);
202
+ }
203
+ case "LiveList": {
204
+ if (Array.isArray(state) === false) {
205
+ throw new Error("Internal: received update on LiveList but state was not an array");
206
+ }
207
+ return liveListToJson(update.node);
208
+ }
209
+ case "LiveMap": {
210
+ if (typeof state !== "object") {
211
+ throw new Error("Internal: received update on LiveMap but state was not an object");
212
+ }
213
+ return liveMapToJson(update.node);
214
+ }
215
+ }
216
+ }
217
+ if (Array.isArray(state)) {
218
+ const newArray = [...state];
219
+ newArray[pathItem] = patchImmutableNode(state[pathItem], path, update);
220
+ return newArray;
221
+ }
222
+ else {
223
+ return Object.assign(Object.assign({}, state), { [pathItem]: patchImmutableNode(state[pathItem], path, update) });
224
+ }
225
+ }
@@ -1,5 +1,5 @@
1
1
  export { LiveObject } from "./LiveObject";
2
2
  export { LiveMap } from "./LiveMap";
3
3
  export { LiveList } from "./LiveList";
4
- export type { Others, Presence, Room, Client, User } from "./types";
4
+ export type { Others, Presence, Room, Client, User, BroadcastOptions, } from "./types";
5
5
  export { createClient } from "./client";
package/lib/esm/room.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Others, Presence, ClientOptions, Room, MyPresenceCallback, OthersEventCallback, AuthEndpoint, EventCallback, User, Connection, ErrorCallback, AuthenticationToken, ConnectionCallback, StorageCallback, StorageUpdate } from "./types";
1
+ import { Others, Presence, ClientOptions, Room, MyPresenceCallback, OthersEventCallback, AuthEndpoint, EventCallback, User, Connection, ErrorCallback, AuthenticationToken, ConnectionCallback, StorageCallback, StorageUpdate, BroadcastOptions } from "./types";
2
2
  import { ClientMessage, Op } from "./live";
3
3
  import { LiveMap } from "./LiveMap";
4
4
  import { LiveObject } from "./LiveObject";
@@ -118,7 +118,7 @@ export declare function makeStateMachine(state: State, context: Context, mockedE
118
118
  updatePresence: <T_4 extends Presence>(overrides: Partial<T_4>, options?: {
119
119
  addToHistory: boolean;
120
120
  } | undefined) => void;
121
- broadcastEvent: (event: any) => void;
121
+ broadcastEvent: (event: any, options?: BroadcastOptions) => void;
122
122
  batch: (callback: () => void) => void;
123
123
  undo: () => void;
124
124
  redo: () => void;
package/lib/esm/room.js CHANGED
@@ -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
  },
@@ -421,7 +424,9 @@ See v0.13 release notes for more information.
421
424
  state.socket = socket;
422
425
  }
423
426
  function authenticationFailure(error) {
424
- console.error(error);
427
+ if (process.env.NODE_ENV !== "production") {
428
+ console.error("Call to authentication endpoint failed", error);
429
+ }
425
430
  updateConnection({ state: "unavailable" });
426
431
  state.numberOfRetry++;
427
432
  state.timeoutHandles.reconnect = effects.scheduleReconnect(getRetryDelay());
@@ -587,13 +592,20 @@ See v0.13 release notes for more information.
587
592
  updateConnection({ state: "failed" });
588
593
  const error = new LiveblocksError(event.reason, event.code);
589
594
  for (const listener of state.listeners.error) {
595
+ if (process.env.NODE_ENV !== "production") {
596
+ console.error(`Connection to Liveblocks websocket server closed. Reason: ${error.message} (code: ${error.code})`);
597
+ }
590
598
  listener(error);
591
599
  }
592
600
  }
593
601
  else if (event.wasClean === false) {
594
- updateConnection({ state: "unavailable" });
595
602
  state.numberOfRetry++;
596
- state.timeoutHandles.reconnect = effects.scheduleReconnect(getRetryDelay());
603
+ const delay = getRetryDelay();
604
+ if (process.env.NODE_ENV !== "production") {
605
+ console.warn(`Connection to Liveblocks websocket server closed (code: ${event.code}). Retrying in ${delay}ms.`);
606
+ }
607
+ updateConnection({ state: "unavailable" });
608
+ state.timeoutHandles.reconnect = effects.scheduleReconnect(delay);
597
609
  }
598
610
  else {
599
611
  updateConnection({ state: "closed" });
@@ -735,8 +747,10 @@ See v0.13 release notes for more information.
735
747
  function getOthers() {
736
748
  return state.others;
737
749
  }
738
- function broadcastEvent(event) {
739
- if (state.socket == null) {
750
+ function broadcastEvent(event, options = {
751
+ shouldQueueEventIfNotReady: false,
752
+ }) {
753
+ if (state.socket == null && options.shouldQueueEventIfNotReady == false) {
740
754
  return;
741
755
  }
742
756
  state.buffer.messages.push({
@@ -817,10 +831,14 @@ See v0.13 release notes for more information.
817
831
  }
818
832
  finally {
819
833
  state.isBatching = false;
820
- addToUndoStack(state.batch.reverseOps);
834
+ if (state.batch.reverseOps.length > 0) {
835
+ addToUndoStack(state.batch.reverseOps);
836
+ }
821
837
  // Clear the redo stack because batch is always called from a local operation
822
838
  state.redoStack = [];
823
- dispatch(state.batch.ops);
839
+ if (state.batch.ops.length > 0) {
840
+ dispatch(state.batch.ops);
841
+ }
824
842
  notify(state.batch.updates);
825
843
  state.batch = {
826
844
  ops: [],
@@ -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
  */
@@ -277,25 +289,57 @@ export declare type Room = {
277
289
  }): () => void;
278
290
  };
279
291
  /**
280
- * Room's history contains function that let you undo and redo operation made on by the current client on the presence and storage.
292
+ * Room's history contains functions that let you undo and redo operation made on by the current client on the presence and storage.
281
293
  */
282
294
  history: {
283
295
  /**
284
296
  * Undoes the last operation executed by the current client.
285
297
  * It does not impact operations made by other clients.
298
+ *
299
+ * @example
300
+ * room.updatePresence({ selectedId: "xxx" }, { addToHistory: true });
301
+ * room.updatePresence({ selectedId: "yyy" }, { addToHistory: true });
302
+ * room.history.undo();
303
+ * // room.getPresence() equals { selectedId: "xxx" }
286
304
  */
287
305
  undo: () => void;
288
306
  /**
289
307
  * Redoes the last operation executed by the current client.
290
308
  * It does not impact operations made by other clients.
309
+ *
310
+ * @example
311
+ * room.updatePresence({ selectedId: "xxx" }, { addToHistory: true });
312
+ * room.updatePresence({ selectedId: "yyy" }, { addToHistory: true });
313
+ * room.history.undo();
314
+ * // room.getPresence() equals { selectedId: "xxx" }
315
+ * room.history.redo();
316
+ * // room.getPresence() equals { selectedId: "yyy" }
291
317
  */
292
318
  redo: () => void;
293
319
  /**
294
320
  * All future modifications made on the Room will be merged together to create a single history item until resume is called.
321
+ *
322
+ * @example
323
+ * room.updatePresence({ cursor: { x: 0, y: 0 } }, { addToHistory: true });
324
+ * room.history.pause();
325
+ * room.updatePresence({ cursor: { x: 1, y: 1 } }, { addToHistory: true });
326
+ * room.updatePresence({ cursor: { x: 2, y: 2 } }, { addToHistory: true });
327
+ * room.history.resume();
328
+ * room.history.undo();
329
+ * // room.getPresence() equals { cursor: { x: 0, y: 0 } }
295
330
  */
296
331
  pause: () => void;
297
332
  /**
298
333
  * Resumes history. Modifications made on the Room are not merged into a single history item anymore.
334
+ *
335
+ * @example
336
+ * room.updatePresence({ cursor: { x: 0, y: 0 } }, { addToHistory: true });
337
+ * room.history.pause();
338
+ * room.updatePresence({ cursor: { x: 1, y: 1 } }, { addToHistory: true });
339
+ * room.updatePresence({ cursor: { x: 2, y: 2 } }, { addToHistory: true });
340
+ * room.history.resume();
341
+ * room.history.undo();
342
+ * // room.getPresence() equals { cursor: { x: 0, y: 0 } }
299
343
  */
300
344
  resume: () => void;
301
345
  };
@@ -391,7 +435,14 @@ export declare type Room = {
391
435
  * }
392
436
  * });
393
437
  */
394
- broadcastEvent: (event: any) => void;
438
+ broadcastEvent: (event: any, options?: BroadcastOptions) => void;
439
+ /**
440
+ * Get the room's storage asynchronously.
441
+ * The storage's root is a {@link LiveObject}.
442
+ *
443
+ * @example
444
+ * const { root } = await room.getStorage();
445
+ */
395
446
  getStorage: <TRoot>() => Promise<{
396
447
  root: LiveObject<TRoot>;
397
448
  }>;
@@ -400,6 +451,13 @@ export declare type Room = {
400
451
  * All the modifications are sent to other clients in a single message.
401
452
  * All the subscribers are called only after the batch is over.
402
453
  * All the modifications are merged in a single history item (undo/redo).
454
+ *
455
+ * @example
456
+ * const { root } = await room.getStorage();
457
+ * room.batch(() => {
458
+ * root.set("x", 0);
459
+ * room.updatePresence({ cursor: { x: 100, y: 100 }});
460
+ * });
403
461
  */
404
462
  batch: (fn: () => void) => void;
405
463
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liveblocks/client",
3
- "version": "0.13.0-beta.1",
3
+ "version": "0.13.3",
4
4
  "description": "",
5
5
  "main": "./lib/cjs/index.js",
6
6
  "module": "./lib/esm/index.js",