@liveblocks/server 1.7.0 → 1.8.0-pre1

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/dist/index.js CHANGED
@@ -12,7 +12,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
12
12
  // src/decoders/ClientMsg.ts
13
13
  import { ClientMsgCode } from "@liveblocks/core";
14
14
  import {
15
- array as array2,
15
+ array as array3,
16
16
  boolean,
17
17
  constant as constant2,
18
18
  jsonObject,
@@ -158,7 +158,9 @@ var jsonObjectYolo = jsonYolo.refine(
158
158
  // src/decoders/Op.ts
159
159
  import { OpCode } from "@liveblocks/core";
160
160
  import {
161
+ array as array2,
161
162
  constant,
163
+ either as either2,
162
164
  number,
163
165
  object,
164
166
  oneOf,
@@ -166,7 +168,8 @@ import {
166
168
  sized as sized2,
167
169
  startsWith,
168
170
  string as string2,
169
- taggedUnion
171
+ taggedUnion,
172
+ tuple
170
173
  } from "decoders";
171
174
  var intent = oneOf(["set", "push"]);
172
175
  var storageFileId = sized2(startsWith("fl_"), { size: 24 });
@@ -174,6 +177,9 @@ var fileSize = number.refine(
174
177
  (value) => Number.isSafeInteger(value) && value >= 0,
175
178
  "Must be a valid file size"
176
179
  );
180
+ var liveTextVersion = number.reject(
181
+ (value) => Number.isSafeInteger(value) && value >= 0 ? null : "Must be a non-negative safe integer"
182
+ );
177
183
  var updateObjectOp = object({
178
184
  type: constant(OpCode.UPDATE_OBJECT),
179
185
  opId: string2,
@@ -218,6 +224,41 @@ var createRegisterOp = object({
218
224
  intent: optional2(intent),
219
225
  deletedId: optional2(string2)
220
226
  });
227
+ var liveTextSegment = either2(
228
+ tuple(string2),
229
+ tuple(string2, jsonObjectYolo)
230
+ );
231
+ var liveTextData = array2(liveTextSegment);
232
+ var textOperation = taggedUnion("type", {
233
+ insert: object({
234
+ type: constant("insert"),
235
+ index: number,
236
+ text: string2,
237
+ attributes: optional2(jsonObjectYolo)
238
+ }),
239
+ delete: object({
240
+ type: constant("delete"),
241
+ index: number,
242
+ length: number
243
+ }),
244
+ format: object({
245
+ type: constant("format"),
246
+ index: number,
247
+ length: number,
248
+ attributes: jsonObjectYolo
249
+ })
250
+ });
251
+ var createTextOp = object({
252
+ type: constant(OpCode.CREATE_TEXT),
253
+ opId: string2,
254
+ id: string2,
255
+ parentId: string2,
256
+ parentKey: string2,
257
+ data: liveTextData,
258
+ version: liveTextVersion,
259
+ intent: optional2(intent),
260
+ deletedId: optional2(string2)
261
+ });
221
262
  var createFileOp = object({
222
263
  type: constant(OpCode.CREATE_FILE),
223
264
  opId: string2,
@@ -233,6 +274,14 @@ var createFileOp = object({
233
274
  intent: optional2(intent),
234
275
  deletedId: optional2(string2)
235
276
  });
277
+ var updateTextOp = object({
278
+ type: constant(OpCode.UPDATE_TEXT),
279
+ opId: string2,
280
+ id: string2,
281
+ baseVersion: liveTextVersion,
282
+ version: optional2(liveTextVersion),
283
+ ops: array2(textOperation)
284
+ });
236
285
  var deleteCrdtOp = object({
237
286
  type: constant(OpCode.DELETE_CRDT),
238
287
  opId: string2,
@@ -256,6 +305,8 @@ var op = taggedUnion("type", {
256
305
  [OpCode.CREATE_LIST]: createListOp,
257
306
  [OpCode.CREATE_MAP]: createMapOp,
258
307
  [OpCode.CREATE_REGISTER]: createRegisterOp,
308
+ [OpCode.CREATE_TEXT]: createTextOp,
309
+ [OpCode.UPDATE_TEXT]: updateTextOp,
259
310
  [OpCode.CREATE_FILE]: createFileOp,
260
311
  [OpCode.DELETE_CRDT]: deleteCrdtOp,
261
312
  [OpCode.SET_PARENT_KEY]: setParentKeyOp,
@@ -282,7 +333,7 @@ var fetchStorageClientMsg = object2({
282
333
  });
283
334
  var updateStorageClientMsg = object2({
284
335
  type: constant2(ClientMsgCode.UPDATE_STORAGE),
285
- ops: array2(op)
336
+ ops: array3(op)
286
337
  });
287
338
  var fetchYDocClientMsg = object2({
288
339
  type: constant2(ClientMsgCode.FETCH_YDOC),
@@ -396,6 +447,8 @@ function buildNode(snapshot2, id) {
396
447
  return buildList(snapshot2, id);
397
448
  } else if (node.type === CrdtType.MAP) {
398
449
  return buildMap(snapshot2, id);
450
+ } else if (node.type === CrdtType.TEXT) {
451
+ return node.data;
399
452
  } else if (node.type === CrdtType.FILE) {
400
453
  return node.data;
401
454
  } else {
@@ -441,6 +494,8 @@ function* emit(snapshot2, id) {
441
494
  yield* emitMap(snapshot2, id);
442
495
  } else if (node.type === CrdtType.REGISTER) {
443
496
  yield JSON.stringify(node.data);
497
+ } else if (node.type === CrdtType.TEXT) {
498
+ yield JSON.stringify(node.data);
444
499
  } else if (node.type === CrdtType.FILE) {
445
500
  yield JSON.stringify(node.data);
446
501
  }
@@ -514,6 +569,18 @@ function* iterJson(key, data, parent, state) {
514
569
  case "LiveMap":
515
570
  yield* iterMap(key, data.data, parent, state);
516
571
  return;
572
+ case "LiveText":
573
+ yield [
574
+ generateId(state),
575
+ {
576
+ type: CrdtType2.TEXT,
577
+ data: data.data,
578
+ version: data.version ?? 0,
579
+ parentId: parent[0],
580
+ parentKey: key
581
+ }
582
+ ];
583
+ return;
517
584
  case "LiveFile":
518
585
  yield [
519
586
  generateId(state),
@@ -609,6 +676,11 @@ function buildNode2(snapshot2, id) {
609
676
  return buildList2(snapshot2, id);
610
677
  } else if (node.type === CrdtType2.MAP) {
611
678
  return buildMap2(snapshot2, id);
679
+ } else if (node.type === CrdtType2.TEXT) {
680
+ return {
681
+ liveblocksType: "LiveText",
682
+ data: node.data
683
+ };
612
684
  } else if (node.type === CrdtType2.FILE) {
613
685
  return { liveblocksType: "LiveFile", data: node.data };
614
686
  } else {
@@ -657,6 +729,11 @@ function* emit2(snapshot2, id) {
657
729
  yield* emitMap2(snapshot2, id);
658
730
  } else if (node.type === CrdtType2.REGISTER) {
659
731
  yield JSON.stringify(node.data ?? null);
732
+ } else if (node.type === CrdtType2.TEXT) {
733
+ yield JSON.stringify({
734
+ liveblocksType: "LiveText",
735
+ data: node.data
736
+ });
660
737
  } else if (node.type === CrdtType2.FILE) {
661
738
  yield '{"liveblocksType":"LiveFile","data":';
662
739
  yield JSON.stringify(node.data);
@@ -882,7 +959,7 @@ import {
882
959
  WebsocketCloseCodes as CloseCode
883
960
  } from "@liveblocks/core";
884
961
  import { Mutex, tryAcquire } from "async-mutex";
885
- import { array as array3, formatInline } from "decoders";
962
+ import { array as array4, formatInline } from "decoders";
886
963
  import { chunkedByCost } from "itertools";
887
964
  import { nanoid as nanoid2 } from "nanoid";
888
965
 
@@ -1057,7 +1134,7 @@ function buildReverseLookup(nodes) {
1057
1134
  delete node.data[key];
1058
1135
  }
1059
1136
  }
1060
- const isLeafNode = node.type === CrdtType4.REGISTER || node.type === CrdtType4.FILE;
1137
+ const isLeafNode = node.type === CrdtType4.REGISTER || node.type === CrdtType4.FILE || node.type === CrdtType4.TEXT;
1061
1138
  if (!isLeafNode) {
1062
1139
  queue.push(...revNodes.valuesAt(nodeId));
1063
1140
  } else if (node.type === CrdtType4.REGISTER) {
@@ -1081,7 +1158,6 @@ function hasStaticDataAt(node, key) {
1081
1158
  return node.type === CrdtType4.OBJECT && Object.prototype.hasOwnProperty.call(node.data, key) && node.data[key] !== void 0;
1082
1159
  }
1083
1160
  var InMemoryDriver = class {
1084
- // Key: `${feedId}:${messageId}`
1085
1161
  constructor(options) {
1086
1162
  __publicField(this, "_nextActor");
1087
1163
  __publicField(this, "_nodes");
@@ -1091,12 +1167,15 @@ var InMemoryDriver = class {
1091
1167
  __publicField(this, "_leasedSessions");
1092
1168
  __publicField(this, "_feeds");
1093
1169
  __publicField(this, "_feedMessages");
1170
+ // Key: `${feedId}:${messageId}`
1171
+ __publicField(this, "_liveTextHistory");
1094
1172
  this._nodes = /* @__PURE__ */ new Map();
1095
1173
  this._metadb = /* @__PURE__ */ new Map();
1096
1174
  this._ydb = /* @__PURE__ */ new Map();
1097
1175
  this._leasedSessions = /* @__PURE__ */ new Map();
1098
1176
  this._feeds = /* @__PURE__ */ new Map();
1099
1177
  this._feedMessages = /* @__PURE__ */ new Map();
1178
+ this._liveTextHistory = /* @__PURE__ */ new Map();
1100
1179
  this._nextActor = options?.initialActor ?? -1;
1101
1180
  for (const [key, value] of options?.initialNodes ?? []) {
1102
1181
  this._nodes.set(key, value);
@@ -1112,10 +1191,40 @@ var InMemoryDriver = class {
1112
1191
  DANGEROUSLY_reset_nodes(doc) {
1113
1192
  this.reinitialize();
1114
1193
  this._nodes.clear();
1194
+ this._liveTextHistory.clear();
1115
1195
  for (const [id, node] of plainLsonToNodeStream(doc)) {
1116
1196
  this._nodes.set(id, node);
1117
1197
  }
1118
1198
  }
1199
+ get_live_text_history_since(nodeId, version) {
1200
+ return (this._liveTextHistory.get(nodeId) ?? []).filter((entry) => entry.version > version).sort((left, right) => left.version - right.version).map((entry) => ({ ...entry, ops: [...entry.ops] }));
1201
+ }
1202
+ get_live_text_history_by_op_id(nodeId, opId) {
1203
+ const entry = (this._liveTextHistory.get(nodeId) ?? []).find(
1204
+ (item) => item.opId === opId
1205
+ );
1206
+ return entry === void 0 ? void 0 : { ...entry, ops: [...entry.ops] };
1207
+ }
1208
+ append_live_text_history(entry) {
1209
+ const history = this._liveTextHistory.get(entry.nodeId) ?? [];
1210
+ history.push({ ...entry, ops: [...entry.ops] });
1211
+ history.sort((left, right) => left.version - right.version);
1212
+ this._liveTextHistory.set(entry.nodeId, history);
1213
+ }
1214
+ purge_live_text_history_before(nodeId, minVersionToKeep) {
1215
+ const history = this._liveTextHistory.get(nodeId);
1216
+ if (history === void 0) {
1217
+ return;
1218
+ }
1219
+ const retained = history.filter(
1220
+ (entry) => entry.version >= minVersionToKeep
1221
+ );
1222
+ if (retained.length === 0) {
1223
+ this._liveTextHistory.delete(nodeId);
1224
+ } else {
1225
+ this._liveTextHistory.set(nodeId, retained);
1226
+ }
1227
+ }
1119
1228
  get_meta(key) {
1120
1229
  return this._metadb.get(key);
1121
1230
  }
@@ -1391,6 +1500,7 @@ var InMemoryDriver = class {
1391
1500
  }
1392
1501
  _loadNodesApi() {
1393
1502
  const nodes = this._nodes;
1503
+ const liveTextHistory = this._liveTextHistory;
1394
1504
  if (!nodes.has("root")) {
1395
1505
  nodes.set("root", { type: CrdtType4.OBJECT, data: {} });
1396
1506
  }
@@ -1481,6 +1591,7 @@ var InMemoryDriver = class {
1481
1591
  const currid = queue.pop();
1482
1592
  queue.push(...revNodes.valuesAt(currid));
1483
1593
  nodes.delete(currid);
1594
+ liveTextHistory.delete(currid);
1484
1595
  revNodes.deleteAll(currid);
1485
1596
  }
1486
1597
  }
@@ -1600,24 +1711,27 @@ function makeNewInMemoryDriver(options) {
1600
1711
 
1601
1712
  // src/Storage.ts
1602
1713
  import {
1714
+ applyLiveTextOperations,
1603
1715
  asPos as asPos2,
1604
1716
  assertNever as assertNever2,
1605
1717
  CrdtType as CrdtType5,
1606
1718
  makePosition as makePosition2,
1607
- OpCode as OpCode2
1719
+ OpCode as OpCode2,
1720
+ transformTextOperations
1608
1721
  } from "@liveblocks/core";
1722
+ var LIVE_TEXT_HISTORY_LIMIT = 1e3;
1723
+ var LIVE_TEXT_HISTORY_TOO_OLD_REASON = "LiveText operation is older than retained history";
1609
1724
  function accept(op2, fix) {
1610
1725
  return { action: "accepted", op: op2, fix };
1611
1726
  }
1612
1727
  function ignore(ignoredOp) {
1613
1728
  return { action: "ignored", ignoredOpId: ignoredOp.opId };
1614
1729
  }
1615
- function rectify(op2, parentKey) {
1616
- return {
1617
- action: "rectified",
1618
- ackOp: { ...op2, parentKey },
1619
- fix: { type: OpCode2.SET_PARENT_KEY, id: op2.id, parentKey }
1620
- };
1730
+ function reject(op2, reason) {
1731
+ return { action: "rejected", opIds: [op2.opId], reason };
1732
+ }
1733
+ function rectify(ackOp, fix) {
1734
+ return { action: "rectified", ackOp, fix };
1621
1735
  }
1622
1736
  function nodeFromCreateChildOp(op2) {
1623
1737
  switch (op2.type) {
@@ -1647,6 +1761,14 @@ function nodeFromCreateChildOp(op2) {
1647
1761
  parentKey: op2.parentKey,
1648
1762
  data: op2.data
1649
1763
  };
1764
+ case OpCode2.CREATE_TEXT:
1765
+ return {
1766
+ type: CrdtType5.TEXT,
1767
+ parentId: op2.parentId,
1768
+ parentKey: op2.parentKey,
1769
+ data: op2.data,
1770
+ version: op2.version
1771
+ };
1650
1772
  case OpCode2.CREATE_FILE:
1651
1773
  return {
1652
1774
  type: CrdtType5.FILE,
@@ -1695,10 +1817,13 @@ var Storage = class {
1695
1817
  case OpCode2.CREATE_MAP:
1696
1818
  case OpCode2.CREATE_REGISTER:
1697
1819
  case OpCode2.CREATE_OBJECT:
1820
+ case OpCode2.CREATE_TEXT:
1698
1821
  case OpCode2.CREATE_FILE:
1699
1822
  return this.applyCreateOp(op2);
1700
1823
  case OpCode2.UPDATE_OBJECT:
1701
1824
  return this.applyUpdateObjectOp(op2);
1825
+ case OpCode2.UPDATE_TEXT:
1826
+ return this.applyUpdateTextOp(op2);
1702
1827
  case OpCode2.SET_PARENT_KEY:
1703
1828
  return this.applySetParentKeyOp(op2);
1704
1829
  case OpCode2.DELETE_OBJECT_KEY:
@@ -1719,7 +1844,14 @@ var Storage = class {
1719
1844
  if (op2.intent === "push") {
1720
1845
  const stored = this.driver.get_node(op2.id);
1721
1846
  if (stored?.parentId !== void 0 && this.driver.get_node(stored.parentId)?.type === CrdtType5.LIST) {
1722
- return rectify(op2, stored.parentKey);
1847
+ return rectify(
1848
+ { ...op2, parentKey: stored.parentKey },
1849
+ {
1850
+ type: OpCode2.SET_PARENT_KEY,
1851
+ id: op2.id,
1852
+ parentKey: stored.parentKey
1853
+ }
1854
+ );
1723
1855
  }
1724
1856
  }
1725
1857
  return ignore(op2);
@@ -1741,6 +1873,7 @@ var Storage = class {
1741
1873
  case CrdtType5.LIST:
1742
1874
  return this.createChildAsListItem(op2, node);
1743
1875
  case CrdtType5.REGISTER:
1876
+ case CrdtType5.TEXT:
1744
1877
  case CrdtType5.FILE:
1745
1878
  return ignore(op2);
1746
1879
  // istanbul ignore next
@@ -1798,6 +1931,59 @@ var Storage = class {
1798
1931
  this.driver.set_object_data(op2.id, op2.data, true);
1799
1932
  return accept(op2);
1800
1933
  }
1934
+ applyUpdateTextOp(op2) {
1935
+ const node = this.driver.get_node(op2.id);
1936
+ if (node?.type !== CrdtType5.TEXT) {
1937
+ return ignore(op2);
1938
+ }
1939
+ const duplicate = this.driver.get_live_text_history_by_op_id(
1940
+ op2.id,
1941
+ op2.opId
1942
+ );
1943
+ if (duplicate !== void 0) {
1944
+ return rectify({
1945
+ ...op2,
1946
+ baseVersion: duplicate.baseVersion,
1947
+ version: duplicate.version,
1948
+ ops: [...duplicate.ops]
1949
+ });
1950
+ }
1951
+ if (op2.ops.length === 0) {
1952
+ return ignore(op2);
1953
+ }
1954
+ if (op2.baseVersion > node.version) {
1955
+ return reject(op2, "LiveText operation base version is ahead of storage");
1956
+ }
1957
+ const history = op2.baseVersion < node.version ? this.driver.get_live_text_history_since(op2.id, op2.baseVersion) : [];
1958
+ if (op2.baseVersion < node.version && history.length !== node.version - op2.baseVersion) {
1959
+ return reject(op2, LIVE_TEXT_HISTORY_TOO_OLD_REASON);
1960
+ }
1961
+ const acceptedOps = history.flatMap((entry) => entry.ops);
1962
+ const ops = acceptedOps.length > 0 ? transformTextOperations(op2.ops, acceptedOps, "after") : op2.ops;
1963
+ const version = node.version + 1;
1964
+ const data = applyLiveTextOperations(node.data, ops);
1965
+ this.driver.set_child(
1966
+ op2.id,
1967
+ {
1968
+ ...node,
1969
+ data,
1970
+ version
1971
+ },
1972
+ true
1973
+ );
1974
+ this.driver.append_live_text_history({
1975
+ nodeId: op2.id,
1976
+ baseVersion: node.version,
1977
+ version,
1978
+ opId: op2.opId,
1979
+ ops: [...ops]
1980
+ });
1981
+ this.driver.purge_live_text_history_before(
1982
+ op2.id,
1983
+ Math.max(0, version - LIVE_TEXT_HISTORY_LIMIT + 1)
1984
+ );
1985
+ return accept({ ...op2, baseVersion: node.version, version, ops: [...ops] });
1986
+ }
1801
1987
  applyDeleteCrdtOp(op2) {
1802
1988
  this.driver.delete_node(op2.id);
1803
1989
  return accept(op2);
@@ -2215,7 +2401,7 @@ function isLeasedSessionExpired(leasedSession) {
2215
2401
 
2216
2402
  // src/Room.ts
2217
2403
  var MB = 1024 * 1024;
2218
- var messagesDecoder = array3(clientMsgDecoder);
2404
+ var messagesDecoder = array4(clientMsgDecoder);
2219
2405
  var ServerMsgCode2 = { ...CoreServerMsgCode, ...FeedMsgCode };
2220
2406
  var HIGHEST_PROTOCOL_VERSION = Math.max(
2221
2407
  ...Object.values(ProtocolVersion).filter(
@@ -3186,9 +3372,11 @@ var Room = class {
3186
3372
  case "ignored":
3187
3373
  return r.ignoredOpId !== void 0 ? [ackIgnoredOp(r.ignoredOpId)] : [];
3188
3374
  case "rectified":
3189
- return [r.ackOp, r.fix];
3375
+ return r.fix !== void 0 ? [r.ackOp, r.fix] : [r.ackOp];
3190
3376
  case "accepted":
3191
3377
  return r.fix !== void 0 ? [r.fix] : [];
3378
+ case "rejected":
3379
+ return [];
3192
3380
  // istanbul ignore next
3193
3381
  default:
3194
3382
  return assertNever3(r, "Unhandled case");
@@ -3211,6 +3399,13 @@ var Room = class {
3211
3399
  ops: opsToSendBack
3212
3400
  });
3213
3401
  }
3402
+ for (const rejected of result.filter((r) => r.action === "rejected")) {
3403
+ replyImmediately({
3404
+ type: ServerMsgCode2.REJECT_STORAGE_OP,
3405
+ opIds: rejected.opIds,
3406
+ reason: rejected.reason
3407
+ });
3408
+ }
3214
3409
  if (opsToForward.length > 0) {
3215
3410
  const p$ = this.hooks.postClientMsgStorageDidUpdate?.(ctx, session);
3216
3411
  if (p$) defer(p$);