@liveblocks/client 0.14.4 → 0.15.0-alpha.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,290 @@
1
+ import { LiveList } from "./LiveList";
2
+ import { LiveMap } from "./LiveMap";
3
+ import { LiveObject } from "./LiveObject";
4
+ import { LiveRegister } from "./LiveRegister";
5
+ import { findNonSerializableValue } from "./utils";
6
+ export function liveObjectToJson(liveObject) {
7
+ const result = {};
8
+ const obj = liveObject.toObject();
9
+ for (const key in obj) {
10
+ result[key] = liveNodeToJson(obj[key]);
11
+ }
12
+ return result;
13
+ }
14
+ function liveMapToJson(map) {
15
+ const result = {};
16
+ const obj = Object.fromEntries(map);
17
+ for (const key in obj) {
18
+ result[key] = liveNodeToJson(obj[key]);
19
+ }
20
+ return result;
21
+ }
22
+ function liveListToJson(value) {
23
+ return value.toArray().map(liveNodeToJson);
24
+ }
25
+ export function liveNodeToJson(value) {
26
+ if (value instanceof LiveObject) {
27
+ return liveObjectToJson(value);
28
+ }
29
+ else if (value instanceof LiveList) {
30
+ return liveListToJson(value);
31
+ }
32
+ else if (value instanceof LiveMap) {
33
+ return liveMapToJson(value);
34
+ }
35
+ else if (value instanceof LiveRegister) {
36
+ return value.data;
37
+ }
38
+ return value;
39
+ }
40
+ function isPlainObject(obj) {
41
+ return Object.prototype.toString.call(obj) === "[object Object]";
42
+ }
43
+ function anyToCrdt(obj) {
44
+ if (obj == null) {
45
+ return obj;
46
+ }
47
+ if (Array.isArray(obj)) {
48
+ return new LiveList(obj.map(anyToCrdt));
49
+ }
50
+ if (isPlainObject(obj)) {
51
+ const init = {};
52
+ for (const key in obj) {
53
+ init[key] = anyToCrdt(obj[key]);
54
+ }
55
+ return new LiveObject(init);
56
+ }
57
+ return obj;
58
+ }
59
+ export function patchLiveList(liveList, prev, next) {
60
+ let i = 0;
61
+ let prevEnd = prev.length - 1;
62
+ let nextEnd = next.length - 1;
63
+ let prevNode = prev[0];
64
+ let nextNode = next[0];
65
+ /**
66
+ * For A,B,C => A,B,C,D
67
+ * i = 3, prevEnd = 2, nextEnd = 3
68
+ *
69
+ * For A,B,C => B,C
70
+ * i = 2, prevEnd = 2, nextEnd = 1
71
+ *
72
+ * For B,C => A,B,C
73
+ * i = 0, pre
74
+ */
75
+ outer: {
76
+ while (prevNode === nextNode) {
77
+ ++i;
78
+ if (i > prevEnd || i > nextEnd) {
79
+ break outer;
80
+ }
81
+ prevNode = prev[i];
82
+ nextNode = next[i];
83
+ }
84
+ prevNode = prev[prevEnd];
85
+ nextNode = next[nextEnd];
86
+ while (prevNode === nextNode) {
87
+ prevEnd--;
88
+ nextEnd--;
89
+ if (i > prevEnd || i > nextEnd) {
90
+ break outer;
91
+ }
92
+ prevNode = prev[prevEnd];
93
+ nextNode = next[nextEnd];
94
+ }
95
+ }
96
+ if (i > prevEnd) {
97
+ if (i <= nextEnd) {
98
+ while (i <= nextEnd) {
99
+ liveList.insert(anyToCrdt(next[i]), i);
100
+ i++;
101
+ }
102
+ }
103
+ }
104
+ else if (i > nextEnd) {
105
+ let localI = i;
106
+ while (localI <= prevEnd) {
107
+ liveList.delete(i);
108
+ localI++;
109
+ }
110
+ }
111
+ else {
112
+ while (i <= prevEnd && i <= nextEnd) {
113
+ prevNode = prev[i];
114
+ nextNode = next[i];
115
+ const liveListNode = liveList.get(i);
116
+ if (liveListNode instanceof LiveObject &&
117
+ isPlainObject(prevNode) &&
118
+ isPlainObject(nextNode)) {
119
+ patchLiveObject(liveListNode, prevNode, nextNode);
120
+ }
121
+ else {
122
+ liveList.delete(i);
123
+ liveList.insert(anyToCrdt(nextNode), i);
124
+ }
125
+ i++;
126
+ }
127
+ while (i <= nextEnd) {
128
+ liveList.insert(anyToCrdt(next[i]), i);
129
+ i++;
130
+ }
131
+ while (i <= prevEnd) {
132
+ liveList.delete(i);
133
+ i++;
134
+ }
135
+ }
136
+ }
137
+ export function patchLiveObjectKey(liveObject, key, prev, next) {
138
+ if (process.env.NODE_ENV !== "production") {
139
+ const nonSerializableValue = findNonSerializableValue(next);
140
+ if (nonSerializableValue) {
141
+ console.error(`New state path: '${nonSerializableValue.path}' value: '${nonSerializableValue.value}' is not serializable.\nOnly serializable value can be synced with Liveblocks.`);
142
+ return;
143
+ }
144
+ }
145
+ const value = liveObject.get(key);
146
+ if (next === undefined) {
147
+ liveObject.delete(key);
148
+ }
149
+ else if (value === undefined) {
150
+ liveObject.set(key, anyToCrdt(next));
151
+ }
152
+ else if (prev === next) {
153
+ return;
154
+ }
155
+ else if (value instanceof LiveList &&
156
+ Array.isArray(prev) &&
157
+ Array.isArray(next)) {
158
+ patchLiveList(value, prev, next);
159
+ }
160
+ else if (value instanceof LiveObject &&
161
+ isPlainObject(prev) &&
162
+ isPlainObject(next)) {
163
+ patchLiveObject(value, prev, next);
164
+ }
165
+ else {
166
+ liveObject.set(key, anyToCrdt(next));
167
+ }
168
+ }
169
+ export function patchLiveObject(root, prev, next) {
170
+ const updates = {};
171
+ for (const key in next) {
172
+ patchLiveObjectKey(root, key, prev[key], next[key]);
173
+ }
174
+ for (const key in prev) {
175
+ if (next[key] === undefined) {
176
+ root.delete(key);
177
+ }
178
+ }
179
+ if (Object.keys(updates).length > 0) {
180
+ root.update(updates);
181
+ }
182
+ }
183
+ function getParentsPath(node) {
184
+ const path = [];
185
+ while (node._parentKey != null && node._parent != null) {
186
+ if (node._parent instanceof LiveList) {
187
+ path.push(node._parent._indexOfPosition(node._parentKey));
188
+ }
189
+ else {
190
+ path.push(node._parentKey);
191
+ }
192
+ node = node._parent;
193
+ }
194
+ return path;
195
+ }
196
+ export function patchImmutableObject(state, updates) {
197
+ return updates.reduce((state, update) => patchImmutableObjectWithUpdate(state, update), state);
198
+ }
199
+ function patchImmutableObjectWithUpdate(state, update) {
200
+ const path = getParentsPath(update.node);
201
+ return patchImmutableNode(state, path, update);
202
+ }
203
+ function patchImmutableNode(state, path, update) {
204
+ var _a, _b, _c, _d;
205
+ const pathItem = path.pop();
206
+ if (pathItem === undefined) {
207
+ switch (update.type) {
208
+ case "LiveObject": {
209
+ if (typeof state !== "object") {
210
+ throw new Error("Internal: received update on LiveObject but state was not an object");
211
+ }
212
+ let newState = Object.assign({}, state);
213
+ for (const key in update.updates) {
214
+ if (((_a = update.updates[key]) === null || _a === void 0 ? void 0 : _a.type) === "update") {
215
+ newState[key] = liveNodeToJson(update.node.get(key));
216
+ }
217
+ else if (((_b = update.updates[key]) === null || _b === void 0 ? void 0 : _b.type) === "delete") {
218
+ delete newState[key];
219
+ }
220
+ }
221
+ return newState;
222
+ }
223
+ case "LiveList": {
224
+ if (Array.isArray(state) === false) {
225
+ throw new Error("Internal: received update on LiveList but state was not an array");
226
+ }
227
+ let newState = state.map((x) => x);
228
+ for (const listUpdate of update.updates) {
229
+ if (listUpdate.type === "insert") {
230
+ if (listUpdate.index === newState.length) {
231
+ newState.push(liveNodeToJson(listUpdate.item));
232
+ }
233
+ else {
234
+ newState = [
235
+ ...newState.slice(0, listUpdate.index),
236
+ liveNodeToJson(listUpdate.item),
237
+ ...newState.slice(listUpdate.index),
238
+ ];
239
+ }
240
+ }
241
+ else if (listUpdate.type === "delete") {
242
+ newState.splice(listUpdate.index, 1);
243
+ }
244
+ else if (listUpdate.type === "move") {
245
+ if (listUpdate.previousIndex > listUpdate.index) {
246
+ newState = [
247
+ ...newState.slice(0, listUpdate.index),
248
+ liveNodeToJson(listUpdate.item),
249
+ ...newState.slice(listUpdate.index, listUpdate.previousIndex),
250
+ ...newState.slice(listUpdate.previousIndex + 1),
251
+ ];
252
+ }
253
+ else {
254
+ newState = [
255
+ ...newState.slice(0, listUpdate.previousIndex),
256
+ ...newState.slice(listUpdate.previousIndex + 1, listUpdate.index + 1),
257
+ liveNodeToJson(listUpdate.item),
258
+ ...newState.slice(listUpdate.index + 1),
259
+ ];
260
+ }
261
+ }
262
+ }
263
+ return newState;
264
+ }
265
+ case "LiveMap": {
266
+ if (typeof state !== "object") {
267
+ throw new Error("Internal: received update on LiveMap but state was not an object");
268
+ }
269
+ let newState = Object.assign({}, state);
270
+ for (const key in update.updates) {
271
+ if (((_c = update.updates[key]) === null || _c === void 0 ? void 0 : _c.type) === "update") {
272
+ newState[key] = liveNodeToJson(update.node.get(key));
273
+ }
274
+ else if (((_d = update.updates[key]) === null || _d === void 0 ? void 0 : _d.type) === "delete") {
275
+ delete newState[key];
276
+ }
277
+ }
278
+ return newState;
279
+ }
280
+ }
281
+ }
282
+ if (Array.isArray(state)) {
283
+ const newArray = [...state];
284
+ newArray[pathItem] = patchImmutableNode(state[pathItem], path, update);
285
+ return newArray;
286
+ }
287
+ else {
288
+ return Object.assign(Object.assign({}, state), { [pathItem]: patchImmutableNode(state[pathItem], path, update) });
289
+ }
290
+ }
@@ -3,3 +3,4 @@ export { LiveMap } from "./LiveMap";
3
3
  export { LiveList } from "./LiveList";
4
4
  export type { Others, Presence, Room, Client, User, BroadcastOptions, } from "./types";
5
5
  export { createClient } from "./client";
6
+ export { liveObjectToJson, liveNodeToJson, patchLiveList, patchImmutableObject, patchLiveObject, patchLiveObjectKey, } from "./immutable";
package/lib/esm/index.js CHANGED
@@ -2,3 +2,4 @@ export { LiveObject } from "./LiveObject";
2
2
  export { LiveMap } from "./LiveMap";
3
3
  export { LiveList } from "./LiveList";
4
4
  export { createClient } from "./client";
5
+ export { liveObjectToJson, liveNodeToJson, patchLiveList, patchImmutableObject, patchLiveObject, patchLiveObjectKey, } from "./immutable";
package/lib/esm/room.d.ts CHANGED
@@ -60,7 +60,7 @@ export declare type State = {
60
60
  updates: {
61
61
  others: [];
62
62
  presence: boolean;
63
- nodes: Set<AbstractCrdt>;
63
+ storageUpdates: Map<string, StorageUpdate>;
64
64
  };
65
65
  };
66
66
  offlineOperations: Map<string, Op>;
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 { getTreesDiffOperations, isSameNodeOrChildOf, remove } from "./utils";
10
+ import { getTreesDiffOperations, isSameNodeOrChildOf, remove, mergeStorageUpdates, } from "./utils";
11
11
  import { ClientMessageType, ServerMessageType, OpType, } from "./live";
12
12
  import { LiveMap } from "./LiveMap";
13
13
  import { LiveObject } from "./LiveObject";
@@ -189,22 +189,22 @@ export function makeStateMachine(state, context, mockedEffects) {
189
189
  state.undoStack.push(historyItem);
190
190
  }
191
191
  }
192
- function storageDispatch(ops, reverse, modified) {
192
+ function storageDispatch(ops, reverse, storageUpdates) {
193
193
  if (state.isBatching) {
194
194
  state.batch.ops.push(...ops);
195
- for (const item of modified) {
196
- state.batch.updates.nodes.add(item);
197
- }
195
+ storageUpdates.forEach((value, key) => {
196
+ state.batch.updates.storageUpdates.set(key, mergeStorageUpdates(state.batch.updates.storageUpdates.get(key), value));
197
+ });
198
198
  state.batch.reverseOps.push(...reverse);
199
199
  }
200
200
  else {
201
201
  addToUndoStack(reverse);
202
202
  state.redoStack = [];
203
203
  dispatch(ops);
204
- notify({ nodes: new Set(modified) });
204
+ notify({ storageUpdates: storageUpdates });
205
205
  }
206
206
  }
207
- function notify({ nodes = new Set(), presence = false, others = [], }) {
207
+ function notify({ storageUpdates = new Map(), presence = false, others = [], }) {
208
208
  if (others.length > 0) {
209
209
  state.others = makeOthers(state.users);
210
210
  for (const event of others) {
@@ -218,28 +218,9 @@ export function makeStateMachine(state, context, mockedEffects) {
218
218
  listener(state.me);
219
219
  }
220
220
  }
221
- if (nodes.size > 0) {
221
+ if (storageUpdates.size > 0) {
222
222
  for (const subscriber of state.listeners.storage) {
223
- subscriber(Array.from(nodes).map((m) => {
224
- if (m instanceof LiveObject) {
225
- return {
226
- type: "LiveObject",
227
- node: m,
228
- };
229
- }
230
- else if (m instanceof LiveList) {
231
- return {
232
- type: "LiveList",
233
- node: m,
234
- };
235
- }
236
- else {
237
- return {
238
- type: "LiveMap",
239
- node: m,
240
- };
241
- }
242
- }));
223
+ subscriber(Array.from(storageUpdates.values()));
243
224
  }
244
225
  }
245
226
  }
@@ -262,7 +243,10 @@ export function makeStateMachine(state, context, mockedEffects) {
262
243
  function apply(item, isLocal) {
263
244
  const result = {
264
245
  reverse: [],
265
- updates: { nodes: new Set(), presence: false },
246
+ updates: {
247
+ storageUpdates: new Map(),
248
+ presence: false,
249
+ },
266
250
  };
267
251
  for (const op of item) {
268
252
  if (op.type === "presence") {
@@ -292,7 +276,7 @@ export function makeStateMachine(state, context, mockedEffects) {
292
276
  }
293
277
  const applyOpResult = applyOp(op, isLocal);
294
278
  if (applyOpResult.modified) {
295
- result.updates.nodes.add(applyOpResult.modified);
279
+ result.updates.storageUpdates.set(applyOpResult.modified.node._id, mergeStorageUpdates(result.updates.storageUpdates.get(applyOpResult.modified.node._id), applyOpResult.modified));
296
280
  result.reverse.unshift(...applyOpResult.reverse);
297
281
  }
298
282
  }
@@ -320,17 +304,12 @@ export function makeStateMachine(state, context, mockedEffects) {
320
304
  }
321
305
  if (item._parent instanceof LiveList) {
322
306
  const previousKey = item._parentKey;
323
- item._parent._setChildKey(op.parentKey, item);
324
- return {
325
- reverse: [
326
- {
327
- type: OpType.SetParentKey,
328
- id: item._id,
329
- parentKey: previousKey,
330
- },
331
- ],
332
- modified: item._parent,
333
- };
307
+ if (previousKey === op.parentKey) {
308
+ return { modified: false };
309
+ }
310
+ else {
311
+ return item._parent._setChildKey(op.parentKey, item, previousKey);
312
+ }
334
313
  }
335
314
  return { modified: false };
336
315
  }
@@ -556,7 +535,7 @@ See v0.13 release notes for more information.
556
535
  subMessages.push(message);
557
536
  }
558
537
  const updates = {
559
- nodes: new Set(),
538
+ storageUpdates: new Map(),
560
539
  others: [],
561
540
  };
562
541
  for (const subMessage of subMessages) {
@@ -592,9 +571,9 @@ See v0.13 release notes for more information.
592
571
  }
593
572
  case ServerMessageType.UpdateStorage: {
594
573
  const applyResult = apply(subMessage.ops, false);
595
- for (const node of applyResult.updates.nodes) {
596
- updates.nodes.add(node);
597
- }
574
+ applyResult.updates.storageUpdates.forEach((value, key) => {
575
+ updates.storageUpdates.set(key, mergeStorageUpdates(updates.storageUpdates.get(key), value));
576
+ });
598
577
  break;
599
578
  }
600
579
  }
@@ -661,11 +640,6 @@ See v0.13 release notes for more information.
661
640
  if (state.connection.state === "connecting") {
662
641
  updateConnection(Object.assign(Object.assign({}, state.connection), { state: "open" }));
663
642
  state.numberOfRetry = 0;
664
- // Re-broadcast the user presence during a reconnect.
665
- if (state.lastConnectionId !== undefined) {
666
- state.buffer.presence = state.me;
667
- tryFlushing();
668
- }
669
643
  state.lastConnectionId = state.connection.id;
670
644
  if (state.root) {
671
645
  state.buffer.messages.push({ type: ClientMessageType.FetchStorage });
@@ -893,8 +867,11 @@ See v0.13 release notes for more information.
893
867
  if (state.batch.reverseOps.length > 0) {
894
868
  addToUndoStack(state.batch.reverseOps);
895
869
  }
896
- // Clear the redo stack because batch is always called from a local operation
897
- state.redoStack = [];
870
+ if (state.batch.ops.length > 0) {
871
+ // Only clear the redo stack if something has changed during a batch
872
+ // Clear the redo stack because batch is always called from a local operation
873
+ state.redoStack = [];
874
+ }
898
875
  if (state.batch.ops.length > 0) {
899
876
  dispatch(state.batch.ops);
900
877
  }
@@ -904,7 +881,7 @@ See v0.13 release notes for more information.
904
881
  reverseOps: [],
905
882
  updates: {
906
883
  others: [],
907
- nodes: new Set(),
884
+ storageUpdates: new Map(),
908
885
  presence: false,
909
886
  },
910
887
  };
@@ -1015,7 +992,11 @@ export function defaultState(me, defaultStorageRoot) {
1015
992
  isBatching: false,
1016
993
  batch: {
1017
994
  ops: [],
1018
- updates: { nodes: new Set(), presence: false, others: [] },
995
+ updates: {
996
+ storageUpdates: new Map(),
997
+ presence: false,
998
+ others: [],
999
+ },
1019
1000
  reverseOps: [],
1020
1001
  },
1021
1002
  offlineOperations: new Map(),
@@ -1025,7 +1006,6 @@ export function createRoom(options, context) {
1025
1006
  const state = defaultState(options.defaultPresence, options.defaultStorageRoot);
1026
1007
  const machine = makeStateMachine(state, context);
1027
1008
  const room = {
1028
- id: context.room,
1029
1009
  /////////////
1030
1010
  // Core //
1031
1011
  /////////////
@@ -1,3 +1,4 @@
1
+ import { AbstractCrdt } from "./AbstractCrdt";
1
2
  import type { LiveList } from "./LiveList";
2
3
  import type { LiveMap } from "./LiveMap";
3
4
  import type { LiveObject } from "./LiveObject";
@@ -16,17 +17,41 @@ export declare type RoomEventCallbackMap = {
16
17
  error: ErrorCallback;
17
18
  connection: ConnectionCallback;
18
19
  };
20
+ export declare type UpdateDelta = {
21
+ type: "update";
22
+ } | {
23
+ type: "delete";
24
+ };
19
25
  export declare type LiveMapUpdates<TKey extends string = string, TValue = any> = {
20
26
  type: "LiveMap";
21
27
  node: LiveMap<TKey, TValue>;
28
+ updates: Record<TKey, UpdateDelta>;
22
29
  };
30
+ export declare type LiveObjectUpdateDelta<T> = Partial<{
31
+ [Property in keyof T]: UpdateDelta;
32
+ }>;
23
33
  export declare type LiveObjectUpdates<TData = any> = {
24
34
  type: "LiveObject";
25
35
  node: LiveObject<TData>;
36
+ updates: LiveObjectUpdateDelta<TData>;
37
+ };
38
+ export declare type LiveListUpdateDelta = {
39
+ index: number;
40
+ item: AbstractCrdt;
41
+ type: "insert";
42
+ } | {
43
+ index: number;
44
+ type: "delete";
45
+ } | {
46
+ index: number;
47
+ previousIndex: number;
48
+ item: AbstractCrdt;
49
+ type: "move";
26
50
  };
27
51
  export declare type LiveListUpdates<TItem = any> = {
28
52
  type: "LiveList";
29
53
  node: LiveList<TItem>;
54
+ updates: LiveListUpdateDelta[];
30
55
  };
31
56
  export declare type BroadcastOptions = {
32
57
  /**
@@ -167,10 +192,6 @@ export declare type OthersEvent<T extends Presence = Presence> = {
167
192
  type: "reset";
168
193
  };
169
194
  export declare type Room = {
170
- /**
171
- * The id of the room.
172
- */
173
- readonly id: string;
174
195
  getConnectionState(): ConnectionState;
175
196
  subscribe: {
176
197
  /**
@@ -1,5 +1,6 @@
1
1
  import { AbstractCrdt, Doc } from "./AbstractCrdt";
2
2
  import { SerializedCrdtWithId, Op, SerializedCrdt } from "./live";
3
+ import { StorageUpdate } from "./types";
3
4
  export declare function remove<T>(array: T[], item: T): void;
4
5
  export declare function isSameNodeOrChildOf(node: AbstractCrdt, parent: AbstractCrdt): boolean;
5
6
  export declare function deserialize(entry: SerializedCrdtWithId, parentToChildren: Map<string, SerializedCrdtWithId[]>, doc: Doc): AbstractCrdt;
@@ -7,3 +8,8 @@ export declare function isCrdt(obj: any): obj is AbstractCrdt;
7
8
  export declare function selfOrRegisterValue(obj: AbstractCrdt): any;
8
9
  export declare function selfOrRegister(obj: any): AbstractCrdt;
9
10
  export declare function getTreesDiffOperations(currentItems: Map<string, SerializedCrdt>, newItems: Map<string, SerializedCrdt>): Op[];
11
+ export declare function mergeStorageUpdates(first: StorageUpdate | undefined, second: StorageUpdate): StorageUpdate;
12
+ export declare function findNonSerializableValue(value: any, path?: string): {
13
+ path: string;
14
+ value: any;
15
+ } | false;
package/lib/esm/utils.js CHANGED
@@ -138,3 +138,76 @@ export function getTreesDiffOperations(currentItems, newItems) {
138
138
  });
139
139
  return ops;
140
140
  }
141
+ export function mergeStorageUpdates(first, second) {
142
+ if (!first) {
143
+ return second;
144
+ }
145
+ if (second.type === "LiveObject") {
146
+ const updates = first.updates;
147
+ for (const [key, value] of Object.entries(second.updates)) {
148
+ updates[key] = value;
149
+ }
150
+ return Object.assign(Object.assign({}, second), { updates: updates });
151
+ }
152
+ else if (second.type === "LiveMap") {
153
+ const updates = first.updates;
154
+ for (const [key, value] of Object.entries(second.updates)) {
155
+ updates[key] = value;
156
+ }
157
+ return Object.assign(Object.assign({}, second), { updates: updates });
158
+ }
159
+ else if (second.type === "LiveList") {
160
+ const updates = first.updates;
161
+ return Object.assign(Object.assign({}, second), { updates: updates.concat(second.updates) });
162
+ }
163
+ return second;
164
+ }
165
+ function isPlain(value) {
166
+ const type = typeof value;
167
+ return (type === "undefined" ||
168
+ value === null ||
169
+ type === "string" ||
170
+ type === "boolean" ||
171
+ type === "number" ||
172
+ Array.isArray(value) ||
173
+ isPlainObject(value));
174
+ }
175
+ function isPlainObject(value) {
176
+ if (typeof value !== "object" || value === null)
177
+ return false;
178
+ let proto = Object.getPrototypeOf(value);
179
+ if (proto === null)
180
+ return true;
181
+ let baseProto = proto;
182
+ while (Object.getPrototypeOf(baseProto) !== null) {
183
+ baseProto = Object.getPrototypeOf(baseProto);
184
+ }
185
+ return proto === baseProto;
186
+ }
187
+ export function findNonSerializableValue(value, path = "") {
188
+ if (!isPlain) {
189
+ return {
190
+ path: path || "root",
191
+ value: value,
192
+ };
193
+ }
194
+ if (typeof value !== "object" || value === null) {
195
+ return false;
196
+ }
197
+ for (const [key, nestedValue] of Object.entries(value)) {
198
+ const nestedPath = path ? path + "." + key : key;
199
+ if (!isPlain(nestedValue)) {
200
+ return {
201
+ path: nestedPath,
202
+ value: nestedValue,
203
+ };
204
+ }
205
+ if (typeof nestedValue === "object") {
206
+ const nonSerializableNestedValue = findNonSerializableValue(nestedValue, nestedPath);
207
+ if (nonSerializableNestedValue) {
208
+ return nonSerializableNestedValue;
209
+ }
210
+ }
211
+ }
212
+ return false;
213
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liveblocks/client",
3
- "version": "0.14.4",
3
+ "version": "0.15.0-alpha.3",
4
4
  "description": "",
5
5
  "main": "./lib/cjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -33,6 +33,7 @@
33
33
  "@types/ws": "^8.2.2",
34
34
  "babel-jest": "^26.6.3",
35
35
  "jest": "^26.6.3",
36
+ "jest-each": "^27.5.1",
36
37
  "node-fetch": "2.6.7",
37
38
  "typescript": "^4.4.0",
38
39
  "ws": "^8.5.0"
@@ -42,4 +43,4 @@
42
43
  "url": "https://github.com/liveblocks/liveblocks.git",
43
44
  "directory": "packages/liveblocks"
44
45
  }
45
- }
46
+ }