@liveblocks/client 0.14.0 → 0.15.0-alpha.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.
@@ -0,0 +1,263 @@
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
+ export 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
+ var _a, _b, _c, _d;
195
+ const pathItem = path.pop();
196
+ if (pathItem === undefined) {
197
+ switch (update.type) {
198
+ case "LiveObject": {
199
+ if (typeof state !== "object") {
200
+ throw new Error("Internal: received update on LiveObject but state was not an object");
201
+ }
202
+ let newState = Object.assign({}, state);
203
+ for (const key in update.updates) {
204
+ if (((_a = update.updates[key]) === null || _a === void 0 ? void 0 : _a.type) === "update") {
205
+ newState[key] = liveNodeToJson(update.node.get(key));
206
+ }
207
+ else if (((_b = update.updates[key]) === null || _b === void 0 ? void 0 : _b.type) === "delete") {
208
+ delete newState[key];
209
+ }
210
+ }
211
+ return newState;
212
+ }
213
+ case "LiveList": {
214
+ if (Array.isArray(state) === false) {
215
+ throw new Error("Internal: received update on LiveList but state was not an array");
216
+ }
217
+ let newState = state.map((x) => x);
218
+ const newArray = update.node.toArray();
219
+ for (const listUpdate of update.updates) {
220
+ if (listUpdate.type === "insert") {
221
+ if (listUpdate.index === newState.length) {
222
+ newState.push(liveNodeToJson(newArray[listUpdate.index]));
223
+ }
224
+ else {
225
+ newState = [
226
+ ...newState.slice(0, listUpdate.index),
227
+ liveNodeToJson(newArray[listUpdate.index]),
228
+ ...newState.slice(listUpdate.index),
229
+ ];
230
+ }
231
+ }
232
+ else if (listUpdate.type === "delete") {
233
+ newState.splice(listUpdate.index, 1);
234
+ }
235
+ }
236
+ return newState;
237
+ }
238
+ case "LiveMap": {
239
+ if (typeof state !== "object") {
240
+ throw new Error("Internal: received update on LiveMap but state was not an object");
241
+ }
242
+ let newState = Object.assign({}, state);
243
+ for (const key in update.updates) {
244
+ if (((_c = update.updates[key]) === null || _c === void 0 ? void 0 : _c.type) === "update") {
245
+ newState[key] = liveNodeToJson(update.node.get(key));
246
+ }
247
+ else if (((_d = update.updates[key]) === null || _d === void 0 ? void 0 : _d.type) === "delete") {
248
+ delete newState[key];
249
+ }
250
+ }
251
+ return newState;
252
+ }
253
+ }
254
+ }
255
+ if (Array.isArray(state)) {
256
+ const newArray = [...state];
257
+ newArray[pathItem] = patchImmutableNode(state[pathItem], path, update);
258
+ return newArray;
259
+ }
260
+ else {
261
+ return Object.assign(Object.assign({}, state), { [pathItem]: patchImmutableNode(state[pathItem], path, update) });
262
+ }
263
+ }
@@ -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 auth, { parseToken } from "./authentication";
12
12
  import { ClientMessageType, ServerMessageType, OpType, } from "./live";
13
13
  import { LiveMap } from "./LiveMap";
@@ -193,22 +193,22 @@ export function makeStateMachine(state, context, mockedEffects) {
193
193
  state.undoStack.push(historyItem);
194
194
  }
195
195
  }
196
- function storageDispatch(ops, reverse, modified) {
196
+ function storageDispatch(ops, reverse, storageUpdates) {
197
197
  if (state.isBatching) {
198
198
  state.batch.ops.push(...ops);
199
- for (const item of modified) {
200
- state.batch.updates.nodes.add(item);
201
- }
199
+ storageUpdates.forEach((value, key) => {
200
+ state.batch.updates.storageUpdates.set(key, mergeStorageUpdates(state.batch.updates.storageUpdates.get(key), value));
201
+ });
202
202
  state.batch.reverseOps.push(...reverse);
203
203
  }
204
204
  else {
205
205
  addToUndoStack(reverse);
206
206
  state.redoStack = [];
207
207
  dispatch(ops);
208
- notify({ nodes: new Set(modified) });
208
+ notify({ storageUpdates: storageUpdates });
209
209
  }
210
210
  }
211
- function notify({ nodes = new Set(), presence = false, others = [], }) {
211
+ function notify({ storageUpdates = new Map(), presence = false, others = [], }) {
212
212
  if (others.length > 0) {
213
213
  state.others = makeOthers(state.users);
214
214
  for (const event of others) {
@@ -222,28 +222,9 @@ export function makeStateMachine(state, context, mockedEffects) {
222
222
  listener(state.me);
223
223
  }
224
224
  }
225
- if (nodes.size > 0) {
225
+ if (storageUpdates.size > 0) {
226
226
  for (const subscriber of state.listeners.storage) {
227
- subscriber(Array.from(nodes).map((m) => {
228
- if (m instanceof LiveObject) {
229
- return {
230
- type: "LiveObject",
231
- node: m,
232
- };
233
- }
234
- else if (m instanceof LiveList) {
235
- return {
236
- type: "LiveList",
237
- node: m,
238
- };
239
- }
240
- else {
241
- return {
242
- type: "LiveMap",
243
- node: m,
244
- };
245
- }
246
- }));
227
+ subscriber(Array.from(storageUpdates.values()));
247
228
  }
248
229
  }
249
230
  }
@@ -266,7 +247,10 @@ export function makeStateMachine(state, context, mockedEffects) {
266
247
  function apply(item, isLocal) {
267
248
  const result = {
268
249
  reverse: [],
269
- updates: { nodes: new Set(), presence: false },
250
+ updates: {
251
+ storageUpdates: new Map(),
252
+ presence: false,
253
+ },
270
254
  };
271
255
  for (const op of item) {
272
256
  if (op.type === "presence") {
@@ -296,7 +280,7 @@ export function makeStateMachine(state, context, mockedEffects) {
296
280
  }
297
281
  const applyOpResult = applyOp(op, isLocal);
298
282
  if (applyOpResult.modified) {
299
- result.updates.nodes.add(applyOpResult.modified);
283
+ result.updates.storageUpdates.set(applyOpResult.modified.node._id, mergeStorageUpdates(result.updates.storageUpdates.get(applyOpResult.modified.node._id), applyOpResult.modified));
300
284
  result.reverse.unshift(...applyOpResult.reverse);
301
285
  }
302
286
  }
@@ -324,17 +308,12 @@ export function makeStateMachine(state, context, mockedEffects) {
324
308
  }
325
309
  if (item._parent instanceof LiveList) {
326
310
  const previousKey = item._parentKey;
327
- item._parent._setChildKey(op.parentKey, item);
328
- return {
329
- reverse: [
330
- {
331
- type: OpType.SetParentKey,
332
- id: item._id,
333
- parentKey: previousKey,
334
- },
335
- ],
336
- modified: item._parent,
337
- };
311
+ if (previousKey === op.parentKey) {
312
+ return { modified: false };
313
+ }
314
+ else {
315
+ return item._parent._setChildKey(op.parentKey, item, previousKey);
316
+ }
338
317
  }
339
318
  return { modified: false };
340
319
  }
@@ -557,7 +536,7 @@ See v0.13 release notes for more information.
557
536
  subMessages.push(message);
558
537
  }
559
538
  const updates = {
560
- nodes: new Set(),
539
+ storageUpdates: new Map(),
561
540
  others: [],
562
541
  };
563
542
  for (const subMessage of subMessages) {
@@ -593,9 +572,9 @@ See v0.13 release notes for more information.
593
572
  }
594
573
  case ServerMessageType.UpdateStorage: {
595
574
  const applyResult = apply(subMessage.ops, false);
596
- for (const node of applyResult.updates.nodes) {
597
- updates.nodes.add(node);
598
- }
575
+ applyResult.updates.storageUpdates.forEach((value, key) => {
576
+ updates.storageUpdates.set(key, mergeStorageUpdates(updates.storageUpdates.get(key), value));
577
+ });
599
578
  break;
600
579
  }
601
580
  }
@@ -889,8 +868,11 @@ See v0.13 release notes for more information.
889
868
  if (state.batch.reverseOps.length > 0) {
890
869
  addToUndoStack(state.batch.reverseOps);
891
870
  }
892
- // Clear the redo stack because batch is always called from a local operation
893
- state.redoStack = [];
871
+ if (state.batch.ops.length > 0) {
872
+ // Only clear the redo stack if something has changed during a batch
873
+ // Clear the redo stack because batch is always called from a local operation
874
+ state.redoStack = [];
875
+ }
894
876
  if (state.batch.ops.length > 0) {
895
877
  dispatch(state.batch.ops);
896
878
  }
@@ -900,7 +882,7 @@ See v0.13 release notes for more information.
900
882
  reverseOps: [],
901
883
  updates: {
902
884
  others: [],
903
- nodes: new Set(),
885
+ storageUpdates: new Map(),
904
886
  presence: false,
905
887
  },
906
888
  };
@@ -1012,7 +994,11 @@ export function defaultState(me, defaultStorageRoot) {
1012
994
  isBatching: false,
1013
995
  batch: {
1014
996
  ops: [],
1015
- updates: { nodes: new Set(), presence: false, others: [] },
997
+ updates: {
998
+ storageUpdates: new Map(),
999
+ presence: false,
1000
+ others: [],
1001
+ },
1016
1002
  reverseOps: [],
1017
1003
  },
1018
1004
  offlineOperations: new Map(),
@@ -16,17 +16,37 @@ export declare type RoomEventCallbackMap = {
16
16
  error: ErrorCallback;
17
17
  connection: ConnectionCallback;
18
18
  };
19
+ export declare type UpdateDelta = {
20
+ type: "update";
21
+ } | {
22
+ type: "delete";
23
+ };
19
24
  export declare type LiveMapUpdates<TKey extends string = string, TValue = any> = {
20
25
  type: "LiveMap";
21
26
  node: LiveMap<TKey, TValue>;
27
+ updates: Record<TKey, UpdateDelta>;
22
28
  };
29
+ export declare type LiveObjectUpdateDelta<T> = Partial<{
30
+ [Property in keyof T]: UpdateDelta;
31
+ }>;
23
32
  export declare type LiveObjectUpdates<TData = any> = {
24
33
  type: "LiveObject";
25
34
  node: LiveObject<TData>;
35
+ updates: LiveObjectUpdateDelta<TData>;
36
+ };
37
+ export declare type ListUpdateDelta = {
38
+ type: "insert";
39
+ } | {
40
+ type: "delete";
41
+ };
42
+ export declare type LiveListUpdateDelta = {
43
+ index: number;
44
+ type: "insert" | "delete";
26
45
  };
27
46
  export declare type LiveListUpdates<TItem = any> = {
28
47
  type: "LiveList";
29
48
  node: LiveList<TItem>;
49
+ updates: LiveListUpdateDelta[];
30
50
  };
31
51
  export declare type BroadcastOptions = {
32
52
  /**
@@ -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,4 @@ 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;
package/lib/esm/utils.js CHANGED
@@ -138,3 +138,27 @@ 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liveblocks/client",
3
- "version": "0.14.0",
3
+ "version": "0.15.0-alpha.1",
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
+ }