@liveblocks/server 1.4.0 → 1.4.2-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
@@ -129,7 +129,9 @@ var feedMetadataRecordForCreate = record(
129
129
  (value) => Object.keys(value).length <= MAX_METADATA_COUNT,
130
130
  `Must be at most ${MAX_METADATA_COUNT} items`
131
131
  );
132
- var optionalFeedMetadataDecoder = optional(feedMetadataRecordForCreate);
132
+ var optionalFeedMetadataDecoder = optional(
133
+ feedMetadataRecordForCreate
134
+ );
133
135
  var feedMetadataUpdateDecoder = record(
134
136
  feedMetadataIdDecoder,
135
137
  feedMetadataNullableValueDecoder
@@ -476,6 +478,7 @@ function* iterJson(key, data, parent, state) {
476
478
  case "LiveMap":
477
479
  yield* iterMap(key, data.data, parent, state);
478
480
  return;
481
+ // istanbul ignore next
479
482
  default:
480
483
  assertNever(data, "Unknown `liveblocksType` field");
481
484
  }
@@ -560,7 +563,7 @@ function buildNode2(snapshot2, id) {
560
563
  } else if (node.type === CrdtType2.MAP) {
561
564
  return buildMap2(snapshot2, id);
562
565
  } else {
563
- return node.data;
566
+ return node.data ?? null;
564
567
  }
565
568
  }
566
569
  function buildObject2(snapshot2, id, staticData) {
@@ -604,7 +607,7 @@ function* emit2(snapshot2, id) {
604
607
  } else if (node.type === CrdtType2.MAP) {
605
608
  yield* emitMap2(snapshot2, id);
606
609
  } else if (node.type === CrdtType2.REGISTER) {
607
- yield JSON.stringify(node.data);
610
+ yield JSON.stringify(node.data ?? null);
608
611
  }
609
612
  }
610
613
  function* emitObject2(snapshot2, id, staticJson) {
@@ -819,7 +822,6 @@ function makeMetadataDB(driver) {
819
822
  import {
820
823
  assertNever as assertNever3,
821
824
  ClientMsgCode as ClientMsgCode2,
822
- nodeStreamToCompactNodes,
823
825
  OpCode as OpCode3,
824
826
  raise as raise3,
825
827
  ServerMsgCode as CoreServerMsgCode,
@@ -828,7 +830,7 @@ import {
828
830
  } from "@liveblocks/core";
829
831
  import { Mutex, tryAcquire } from "async-mutex";
830
832
  import { array as array3, formatInline } from "decoders";
831
- import { chunked } from "itertools";
833
+ import { chunkedByCost } from "itertools";
832
834
  import { nanoid as nanoid2 } from "nanoid";
833
835
 
834
836
  // src/lib/Logger.ts
@@ -940,12 +942,10 @@ var Logger = class _Logger {
940
942
  }
941
943
  });
942
944
  this.o = {
943
- /* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
944
945
  debug: minLevel <= 0 /* DEBUG */ ? makeLogFn(0 /* DEBUG */) : void 0,
945
946
  info: minLevel <= 1 /* INFO */ ? makeLogFn(1 /* INFO */) : void 0,
946
947
  warn: minLevel <= 2 /* WARNING */ ? makeLogFn(2 /* WARNING */) : void 0,
947
948
  error: minLevel <= 3 /* ERROR */ ? makeLogFn(3 /* ERROR */) : void 0
948
- /* eslint-enable @typescript-eslint/no-unsafe-enum-comparison */
949
949
  };
950
950
  this.debug = this.o.debug ?? noop;
951
951
  this.info = this.o.info ?? noop;
@@ -965,7 +965,13 @@ var Logger = class _Logger {
965
965
  };
966
966
 
967
967
  // src/plugins/InMemoryDriver.ts
968
- import { asPos, CrdtType as CrdtType4, isRootStorageNode as isRootStorageNode2, nn as nn2 } from "@liveblocks/core";
968
+ import {
969
+ asPos,
970
+ CrdtType as CrdtType4,
971
+ isRootStorageNode as isRootStorageNode2,
972
+ nn as nn2,
973
+ nodeStreamToCompactNodes
974
+ } from "@liveblocks/core";
969
975
  import { ifilter, imap } from "itertools";
970
976
 
971
977
  // src/lib/text.ts
@@ -1375,6 +1381,17 @@ var InMemoryDriver = class {
1375
1381
  * Yield all nodes as [id, node] pairs. Must always include the root node.
1376
1382
  */
1377
1383
  iter_nodes: () => nodes,
1384
+ /**
1385
+ * Yield each node as a pre-built CompactNode JSON tuple string.
1386
+ *
1387
+ * This implementation IS the canonical reference for the invariant:
1388
+ * iter_nodes_optimized` ≡ `nodeStreamToCompactNodes(iter_nodes())
1389
+ */
1390
+ *iter_nodes_optimized() {
1391
+ for (const compact of nodeStreamToCompactNodes(nodes)) {
1392
+ yield JSON.stringify(compact);
1393
+ }
1394
+ },
1378
1395
  /**
1379
1396
  * Return true iff a node with the given id exists. Must return true for "root".
1380
1397
  */
@@ -1494,6 +1511,7 @@ function nodeFromCreateChildOp(op2) {
1494
1511
  parentKey: op2.parentKey,
1495
1512
  data: op2.data
1496
1513
  };
1514
+ // istanbul ignore next
1497
1515
  default:
1498
1516
  return assertNever2(op2, "Unknown op code");
1499
1517
  }
@@ -1562,6 +1580,7 @@ var Storage = class {
1562
1580
  return await this.applyDeleteObjectKeyOp(op2);
1563
1581
  case OpCode2.DELETE_CRDT:
1564
1582
  return await this.applyDeleteCrdtOp(op2);
1583
+ // istanbul ignore next
1565
1584
  default:
1566
1585
  if (process.env.NODE_ENV === "production") {
1567
1586
  return ignore(op2);
@@ -1584,6 +1603,7 @@ var Storage = class {
1584
1603
  if (op2.type === OpCode2.CREATE_REGISTER) {
1585
1604
  return ignore(op2);
1586
1605
  }
1606
+ // fall through
1587
1607
  case CrdtType5.MAP:
1588
1608
  await this.loadedDriver.set_child(op2.id, node, true);
1589
1609
  return accept(op2);
@@ -1591,6 +1611,7 @@ var Storage = class {
1591
1611
  return this.createChildAsListItem(op2, node);
1592
1612
  case CrdtType5.REGISTER:
1593
1613
  return ignore(op2);
1614
+ // istanbul ignore next
1594
1615
  default:
1595
1616
  return assertNever2(parent, "Unhandled CRDT type");
1596
1617
  }
@@ -1731,6 +1752,7 @@ import { Base64 } from "js-base64";
1731
1752
  import { nanoid } from "nanoid";
1732
1753
  import * as Y from "yjs";
1733
1754
  var UPDATE_COUNT_THRESHOLD = 1e3;
1755
+ var MERGE_SHRINK_THRESHOLD = 0.8;
1734
1756
  var YjsStorage = class {
1735
1757
  constructor(driver, updateCountThreshold = UPDATE_COUNT_THRESHOLD) {
1736
1758
  __publicField(this, "driver");
@@ -1748,15 +1770,16 @@ var YjsStorage = class {
1748
1770
  await this.driver.delete_y_updates(docId, storedKeys);
1749
1771
  this.storedKeysById.set(docId, [newKey]);
1750
1772
  });
1751
- __publicField(this, "_loadYDocFromDurableStorage", async (doc, docId) => {
1773
+ __publicField(this, "_loadYDocFromDurableStorage", async (_logger, doc, docId) => {
1752
1774
  const docUpdates = Object.fromEntries(
1753
1775
  await this.driver.iter_y_updates(docId)
1754
1776
  );
1755
1777
  const updates = Object.values(docUpdates);
1778
+ const beforeSize = updates.reduce((acc, update) => acc + update.length, 0);
1756
1779
  const newupdate = Y.mergeUpdates(updates);
1757
1780
  const storedKeys = Object.keys(docUpdates);
1758
1781
  Y.applyUpdate(doc, newupdate);
1759
- if (this.shouldCompact(storedKeys)) {
1782
+ if (this.shouldCompactByKeyCount(storedKeys) || this.shouldCompactBySize(beforeSize, newupdate.length)) {
1760
1783
  await this._compactYJSUpdates(doc, docId, storedKeys);
1761
1784
  } else {
1762
1785
  this.storedKeysById.set(docId, storedKeys);
@@ -1775,8 +1798,8 @@ var YjsStorage = class {
1775
1798
  // ------------------------------------------------------------------------------------
1776
1799
  // Public API
1777
1800
  // ------------------------------------------------------------------------------------
1778
- async getYDoc(docId) {
1779
- const doc = await this.loadDocByIdIfNotAlreadyLoaded(docId);
1801
+ async getYDoc(logger, docId) {
1802
+ const doc = await this.loadDocByIdIfNotAlreadyLoaded(logger, docId);
1780
1803
  return doc;
1781
1804
  }
1782
1805
  /**
@@ -1796,7 +1819,7 @@ var YjsStorage = class {
1796
1819
  return Base64.fromUint8Array(update);
1797
1820
  }
1798
1821
  async getYDocUpdateBinary(logger, stateVector = "", guid, isV2 = false) {
1799
- const doc = guid !== void 0 ? await this.getYSubdoc(guid) : this.doc;
1822
+ const doc = guid !== void 0 ? await this.getYSubdoc(logger, guid) : this.doc;
1800
1823
  if (!doc) {
1801
1824
  return null;
1802
1825
  }
@@ -1813,15 +1836,15 @@ var YjsStorage = class {
1813
1836
  }
1814
1837
  return Y.encodeStateAsUpdate(doc, encodedTargetVector);
1815
1838
  }
1816
- async getYStateVector(guid) {
1817
- const doc = guid !== void 0 ? await this.getYSubdoc(guid) : this.doc;
1839
+ async getYStateVector(logger, guid) {
1840
+ const doc = guid !== void 0 ? await this.getYSubdoc(logger, guid) : this.doc;
1818
1841
  if (!doc) {
1819
1842
  return null;
1820
1843
  }
1821
1844
  return Base64.fromUint8Array(Y.encodeStateVector(doc));
1822
1845
  }
1823
- async getSnapshotHash(options) {
1824
- const doc = options.guid !== void 0 ? await this.getYSubdoc(options.guid) : this.doc;
1846
+ async getSnapshotHash(logger, options) {
1847
+ const doc = options.guid !== void 0 ? await this.getYSubdoc(logger, options.guid) : this.doc;
1825
1848
  if (!doc) {
1826
1849
  return null;
1827
1850
  }
@@ -1835,7 +1858,7 @@ var YjsStorage = class {
1835
1858
  * snapshotHash: the hash of the new snapshot
1836
1859
  */
1837
1860
  async addYDocUpdate(logger, update, guid, isV2) {
1838
- const doc = guid !== void 0 ? await this.getYSubdoc(guid) : this.doc;
1861
+ const doc = guid !== void 0 ? await this.getYSubdoc(logger, guid) : this.doc;
1839
1862
  if (!doc) {
1840
1863
  throw new Error(`YDoc with guid ${guid} not found`);
1841
1864
  }
@@ -1860,20 +1883,20 @@ var YjsStorage = class {
1860
1883
  );
1861
1884
  }
1862
1885
  }
1863
- loadDocByIdIfNotAlreadyLoaded(docId) {
1886
+ loadDocByIdIfNotAlreadyLoaded(logger, docId) {
1864
1887
  let loaded$ = this.initPromisesById.get(docId);
1865
1888
  let doc = docId === ROOT_YDOC_ID ? this.doc : this.findYSubdocByGuid(docId);
1866
1889
  if (!doc) {
1867
1890
  doc = new Y.Doc();
1868
1891
  }
1869
1892
  if (loaded$ === void 0) {
1870
- loaded$ = this._loadYDocFromDurableStorage(doc, docId);
1893
+ loaded$ = this._loadYDocFromDurableStorage(logger, doc, docId);
1871
1894
  this.initPromisesById.set(docId, loaded$);
1872
1895
  }
1873
1896
  return loaded$;
1874
1897
  }
1875
- async load(_logger) {
1876
- await this.loadDocByIdIfNotAlreadyLoaded(ROOT_YDOC_ID);
1898
+ async load(logger) {
1899
+ await this.loadDocByIdIfNotAlreadyLoaded(logger, ROOT_YDOC_ID);
1877
1900
  }
1878
1901
  /**
1879
1902
  * Unloads the Yjs documents from memory.
@@ -1916,12 +1939,12 @@ var YjsStorage = class {
1916
1939
  );
1917
1940
  }
1918
1941
  // gets a subdoc, it will be loaded if not already loaded
1919
- async getYSubdoc(guid) {
1942
+ async getYSubdoc(logger, guid) {
1920
1943
  const subdoc = this.findYSubdocByGuid(guid);
1921
1944
  if (!subdoc) {
1922
1945
  return null;
1923
1946
  }
1924
- await this.loadDocByIdIfNotAlreadyLoaded(guid);
1947
+ await this.loadDocByIdIfNotAlreadyLoaded(logger, guid);
1925
1948
  return subdoc;
1926
1949
  }
1927
1950
  // When the YJS doc changes, update it in durable storage
@@ -1929,7 +1952,7 @@ var YjsStorage = class {
1929
1952
  const v1update = isV2 ? Y.convertUpdateFormatV2ToV1(update) : update;
1930
1953
  const docId = doc.guid === this.doc.guid ? ROOT_YDOC_ID : doc.guid;
1931
1954
  const storedKeys = this.storedKeysById.get(docId);
1932
- if (this.shouldCompact(storedKeys)) {
1955
+ if (this.shouldCompactByKeyCount(storedKeys)) {
1933
1956
  await this._compactYJSUpdates(doc, docId, storedKeys || []);
1934
1957
  return;
1935
1958
  }
@@ -1941,7 +1964,10 @@ var YjsStorage = class {
1941
1964
  storedKeys.push(newKey);
1942
1965
  }
1943
1966
  }
1944
- shouldCompact(storedKeys) {
1967
+ shouldCompactBySize(beforeSize, afterSize) {
1968
+ return beforeSize * MERGE_SHRINK_THRESHOLD > afterSize;
1969
+ }
1970
+ shouldCompactByKeyCount(storedKeys) {
1945
1971
  if (!storedKeys) {
1946
1972
  return false;
1947
1973
  }
@@ -2027,6 +2053,7 @@ function isLeasedSessionExpired(leasedSession) {
2027
2053
  }
2028
2054
 
2029
2055
  // src/Room.ts
2056
+ var MB = 1024 * 1024;
2030
2057
  var messagesDecoder = array3(clientMsgDecoder);
2031
2058
  var ServerMsgCode2 = { ...CoreServerMsgCode, ...FeedMsgCode };
2032
2059
  var HIGHEST_PROTOCOL_VERSION = Math.max(
@@ -2040,6 +2067,11 @@ var SERVER_MSG_CODE_NAMES = Object.fromEntries(
2040
2067
  var BLACK_HOLE = new Logger([
2041
2068
  /* No targets, i.e. black hole logger */
2042
2069
  ]);
2070
+ function groupNodesForWebSocketMessages(input) {
2071
+ const MAX_SIZE = 16 * MB;
2072
+ const idealSize = (chunkIndex) => chunkIndex < 3 ? 1 * MB : Math.min(8, chunkIndex - 1) * MB;
2073
+ return chunkedByCost(input, (tup) => tup.length, MAX_SIZE, idealSize);
2074
+ }
2043
2075
  function collectSideEffects() {
2044
2076
  const deferred = [];
2045
2077
  return {
@@ -2173,7 +2205,7 @@ var BackendSession = class extends BrowserSession {
2173
2205
  super(ticket, socket, debug);
2174
2206
  }
2175
2207
  };
2176
- var __debug2, __allowStreaming;
2208
+ var __debug2;
2177
2209
  var Room = class {
2178
2210
  constructor(meta, options) {
2179
2211
  // ^^^^^^^^^^ User-defined Room Metadata, Session Metadata, and Client Metadata
@@ -2195,12 +2227,10 @@ var Room = class {
2195
2227
  __publicField(this, "sessions", new UniqueMap((s) => s.actor));
2196
2228
  __publicField(this, "hooks");
2197
2229
  __privateAdd(this, __debug2);
2198
- __privateAdd(this, __allowStreaming);
2199
2230
  const driver = options?.storage ?? makeNewInMemoryDriver();
2200
2231
  this.meta = meta;
2201
2232
  this.driver = driver;
2202
2233
  this.logger = options?.logger ?? BLACK_HOLE;
2203
- __privateSet(this, __allowStreaming, options?.allowStreaming ?? true);
2204
2234
  this.hooks = {
2205
2235
  isClientMsgAllowed: options?.hooks?.isClientMsgAllowed ?? (() => {
2206
2236
  return {
@@ -2905,7 +2935,6 @@ var Room = class {
2905
2935
  * @internal
2906
2936
  * Handles an incoming ping, by sending a pong back.
2907
2937
  */
2908
- // eslint-disable-next-line @typescript-eslint/require-await
2909
2938
  async handlePing(sessionKey, ctx) {
2910
2939
  const session = this.sessions.get(sessionKey);
2911
2940
  if (session === void 0) {
@@ -2966,7 +2995,13 @@ var Room = class {
2966
2995
  const toReplyImmediately = [];
2967
2996
  const toReplyAfter = [];
2968
2997
  const replyImmediately = (msg) => {
2969
- if (Array.isArray(msg)) {
2998
+ if (typeof msg === "string") {
2999
+ if (toReplyImmediately.length > 0) {
3000
+ session.send(toReplyImmediately);
3001
+ toReplyImmediately.length = 0;
3002
+ }
3003
+ session.send(msg);
3004
+ } else if (Array.isArray(msg)) {
2970
3005
  for (const m of msg) {
2971
3006
  toReplyImmediately.push(m);
2972
3007
  }
@@ -3024,24 +3059,10 @@ var Room = class {
3024
3059
  }
3025
3060
  case ClientMsgCode2.FETCH_STORAGE: {
3026
3061
  if (session.version >= 8 /* V8 */) {
3027
- if (__privateGet(this, __allowStreaming)) {
3028
- const NODES_PER_CHUNK = 250;
3029
- for (const chunk of chunked(
3030
- nodeStreamToCompactNodes(this.storage.loadedDriver.iter_nodes()),
3031
- NODES_PER_CHUNK
3032
- )) {
3033
- replyImmediately({
3034
- type: ServerMsgCode2.STORAGE_CHUNK,
3035
- nodes: chunk
3036
- });
3037
- }
3038
- } else {
3039
- replyImmediately({
3040
- type: ServerMsgCode2.STORAGE_CHUNK,
3041
- nodes: Array.from(
3042
- nodeStreamToCompactNodes(this.storage.loadedDriver.iter_nodes())
3043
- )
3044
- });
3062
+ const rawStream = this.storage.loadedDriver.iter_nodes_optimized();
3063
+ for (const chunk of groupNodesForWebSocketMessages(rawStream)) {
3064
+ const frame = `{"type":${ServerMsgCode2.STORAGE_CHUNK},"nodes":[${chunk.join(",")}]}`;
3065
+ replyImmediately(frame);
3045
3066
  }
3046
3067
  replyImmediately({ type: ServerMsgCode2.STORAGE_STREAM_END });
3047
3068
  } else {
@@ -3064,6 +3085,7 @@ var Room = class {
3064
3085
  return r.ignoredOpId !== void 0 ? [ackIgnoredOp(r.ignoredOpId)] : [];
3065
3086
  case "accepted":
3066
3087
  return r.fix !== void 0 ? [r.fix] : [];
3088
+ // istanbul ignore next
3067
3089
  default:
3068
3090
  return assertNever3(r, "Unhandled case");
3069
3091
  }
@@ -3096,8 +3118,8 @@ var Room = class {
3096
3118
  const isV2 = msg.v2;
3097
3119
  const [update, stateVector, snapshotHash] = await Promise.all([
3098
3120
  this.yjsStorage.getYDocUpdate(this.logger, vector, guid, isV2),
3099
- this.yjsStorage.getYStateVector(guid),
3100
- this.yjsStorage.getSnapshotHash({ guid, isV2 })
3121
+ this.yjsStorage.getYStateVector(this.logger, guid),
3122
+ this.yjsStorage.getSnapshotHash(this.logger, { guid, isV2 })
3101
3123
  ]);
3102
3124
  if (update !== null && snapshotHash !== null) {
3103
3125
  replyImmediately({
@@ -3141,6 +3163,7 @@ var Room = class {
3141
3163
  }
3142
3164
  break;
3143
3165
  }
3166
+ // Feed messages
3144
3167
  case FeedMsgCode.FETCH_FEEDS: {
3145
3168
  const fetchMsg = msg;
3146
3169
  const [result, err] = await tryCatch(
@@ -3190,8 +3213,7 @@ var Room = class {
3190
3213
  const [feed, err] = await tryCatch(
3191
3214
  this.createFeed({
3192
3215
  feedId: addMsg.feedId,
3193
- metadata: addMsg.metadata ?? {},
3194
- timestamp: addMsg.timestamp
3216
+ metadata: addMsg.metadata ?? {}
3195
3217
  })
3196
3218
  );
3197
3219
  if (err) {
@@ -3253,8 +3275,7 @@ var Room = class {
3253
3275
  const [message, err] = await tryCatch(
3254
3276
  this.addFeedMessage(addMsg.feedId, {
3255
3277
  data: addMsg.data,
3256
- id: addMsg.id,
3257
- timestamp: addMsg.timestamp
3278
+ id: addMsg.id
3258
3279
  })
3259
3280
  );
3260
3281
  if (err) {
@@ -3276,8 +3297,7 @@ var Room = class {
3276
3297
  this.updateFeedMessage(
3277
3298
  updateMsg.feedId,
3278
3299
  updateMsg.messageId,
3279
- updateMsg.data,
3280
- updateMsg.timestamp
3300
+ updateMsg.data
3281
3301
  )
3282
3302
  );
3283
3303
  if (err) {
@@ -3321,7 +3341,6 @@ var Room = class {
3321
3341
  }
3322
3342
  };
3323
3343
  __debug2 = new WeakMap();
3324
- __allowStreaming = new WeakMap();
3325
3344
  export {
3326
3345
  BackendSession,
3327
3346
  BrowserSession,