@liveblocks/server 1.0.15 → 1.1.1-pnpmtest1

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.d.cts CHANGED
@@ -562,6 +562,11 @@ interface IStorageDriver {
562
562
  * Delete a leased session by session ID.
563
563
  */
564
564
  delete_leased_session(sessionId: string): Awaitable<void>;
565
+ /**
566
+ * Return the number of storage rows written since last call to this method,
567
+ * and reset the counter.
568
+ */
569
+ takeRowsWritten?(): number;
565
570
  }
566
571
 
567
572
  /**
@@ -824,12 +829,12 @@ declare class Storage {
824
829
 
825
830
  declare class YjsStorage {
826
831
  private readonly driver;
832
+ private readonly updateCountThreshold;
827
833
  private readonly doc;
828
- private readonly lastUpdatesById;
829
834
  private readonly lastSnapshotById;
830
- private readonly keysById;
831
835
  private readonly initPromisesById;
832
- constructor(driver: IStorageDriver);
836
+ private readonly storedKeysById;
837
+ constructor(driver: IStorageDriver, updateCountThreshold?: number);
833
838
  getYDoc(docId: YDocId): Promise<Y.Doc>;
834
839
  /**
835
840
  * If passed a state vector, an update with diff will be returned, if not the entire doc is returned.
@@ -846,7 +851,9 @@ declare class YjsStorage {
846
851
  }): Promise<string | null>;
847
852
  /**
848
853
  * @param update base64 encoded uint8array
849
- * @returns
854
+ * @returns { isUpdated: boolean; snapshotHash: string }
855
+ * isUpdated: true if the update had an effect on the YDoc
856
+ * snapshotHash: the hash of the new snapshot
850
857
  */
851
858
  addYDocUpdate(logger: Logger, update: string | Uint8Array, guid?: Guid, isV2?: boolean): Promise<{
852
859
  isUpdated: boolean;
@@ -860,15 +867,13 @@ declare class YjsStorage {
860
867
  unload(): void;
861
868
  private _getOrPutLastSnapshot;
862
869
  private _putLastSnapshot;
863
- /**
864
- * Given a record of updates, merge them and compress if savings are significant
865
- */
866
- private _loadAndCompressYJSUpdates;
870
+ private _compactYJSUpdates;
867
871
  private _loadYDocFromDurableStorage;
868
872
  private findYSubdocByGuid;
869
873
  private calculateSnapshotHash;
870
874
  private getYSubdoc;
871
875
  private handleYDocUpdate;
876
+ private shouldCompact;
872
877
  }
873
878
 
874
879
  /**
@@ -1102,6 +1107,8 @@ declare class Room<RM, SM, CM extends JsonObject, C = undefined> {
1102
1107
  ticket: Ticket<SM, CM>;
1103
1108
  socket: IServerWebSocket;
1104
1109
  lastActivity: Date;
1110
+ /** Original session creation time (e.g. from persisted attachment). Required for correct session duration after hibernation. */
1111
+ createdAt?: Date;
1105
1112
  }[]): void;
1106
1113
  private sendSessionStartMessages;
1107
1114
  /**
@@ -1478,6 +1485,7 @@ declare class InMemoryDriver implements IStorageDriver {
1478
1485
  get_leased_session(sessionId: string): Promise<LeasedSession | undefined>;
1479
1486
  put_leased_session(session: LeasedSession): Promise<void>;
1480
1487
  delete_leased_session(sessionId: string): Promise<void>;
1488
+ takeRowsWritten(): number;
1481
1489
  next_actor(): number;
1482
1490
  iter_y_updates(docId: YDocId): Promise<IterableIterator<[string, Uint8Array]>>;
1483
1491
  write_y_updates(docId: YDocId, key: string, data: Uint8Array): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -562,6 +562,11 @@ interface IStorageDriver {
562
562
  * Delete a leased session by session ID.
563
563
  */
564
564
  delete_leased_session(sessionId: string): Awaitable<void>;
565
+ /**
566
+ * Return the number of storage rows written since last call to this method,
567
+ * and reset the counter.
568
+ */
569
+ takeRowsWritten?(): number;
565
570
  }
566
571
 
567
572
  /**
@@ -824,12 +829,12 @@ declare class Storage {
824
829
 
825
830
  declare class YjsStorage {
826
831
  private readonly driver;
832
+ private readonly updateCountThreshold;
827
833
  private readonly doc;
828
- private readonly lastUpdatesById;
829
834
  private readonly lastSnapshotById;
830
- private readonly keysById;
831
835
  private readonly initPromisesById;
832
- constructor(driver: IStorageDriver);
836
+ private readonly storedKeysById;
837
+ constructor(driver: IStorageDriver, updateCountThreshold?: number);
833
838
  getYDoc(docId: YDocId): Promise<Y.Doc>;
834
839
  /**
835
840
  * If passed a state vector, an update with diff will be returned, if not the entire doc is returned.
@@ -846,7 +851,9 @@ declare class YjsStorage {
846
851
  }): Promise<string | null>;
847
852
  /**
848
853
  * @param update base64 encoded uint8array
849
- * @returns
854
+ * @returns { isUpdated: boolean; snapshotHash: string }
855
+ * isUpdated: true if the update had an effect on the YDoc
856
+ * snapshotHash: the hash of the new snapshot
850
857
  */
851
858
  addYDocUpdate(logger: Logger, update: string | Uint8Array, guid?: Guid, isV2?: boolean): Promise<{
852
859
  isUpdated: boolean;
@@ -860,15 +867,13 @@ declare class YjsStorage {
860
867
  unload(): void;
861
868
  private _getOrPutLastSnapshot;
862
869
  private _putLastSnapshot;
863
- /**
864
- * Given a record of updates, merge them and compress if savings are significant
865
- */
866
- private _loadAndCompressYJSUpdates;
870
+ private _compactYJSUpdates;
867
871
  private _loadYDocFromDurableStorage;
868
872
  private findYSubdocByGuid;
869
873
  private calculateSnapshotHash;
870
874
  private getYSubdoc;
871
875
  private handleYDocUpdate;
876
+ private shouldCompact;
872
877
  }
873
878
 
874
879
  /**
@@ -1102,6 +1107,8 @@ declare class Room<RM, SM, CM extends JsonObject, C = undefined> {
1102
1107
  ticket: Ticket<SM, CM>;
1103
1108
  socket: IServerWebSocket;
1104
1109
  lastActivity: Date;
1110
+ /** Original session creation time (e.g. from persisted attachment). Required for correct session duration after hibernation. */
1111
+ createdAt?: Date;
1105
1112
  }[]): void;
1106
1113
  private sendSessionStartMessages;
1107
1114
  /**
@@ -1478,6 +1485,7 @@ declare class InMemoryDriver implements IStorageDriver {
1478
1485
  get_leased_session(sessionId: string): Promise<LeasedSession | undefined>;
1479
1486
  put_leased_session(session: LeasedSession): Promise<void>;
1480
1487
  delete_leased_session(sessionId: string): Promise<void>;
1488
+ takeRowsWritten(): number;
1481
1489
  next_actor(): number;
1482
1490
  iter_y_updates(docId: YDocId): Promise<IterableIterator<[string, Uint8Array]>>;
1483
1491
  write_y_updates(docId: YDocId, key: string, data: Uint8Array): Promise<void>;
package/dist/index.js CHANGED
@@ -891,6 +891,9 @@ var InMemoryDriver = class {
891
891
  async delete_leased_session(sessionId) {
892
892
  this._leasedSessions.delete(sessionId);
893
893
  }
894
+ takeRowsWritten() {
895
+ return 0;
896
+ }
894
897
  next_actor() {
895
898
  return ++this._nextActor;
896
899
  }
@@ -1369,63 +1372,45 @@ var Storage = class {
1369
1372
  };
1370
1373
 
1371
1374
  // src/YjsStorage.ts
1372
- import { DefaultMap as DefaultMap2 } from "@liveblocks/core";
1373
1375
  import { Base64 } from "js-base64";
1374
1376
  import { nanoid } from "nanoid";
1375
1377
  import * as Y from "yjs";
1376
- var MAX_Y_UPDATE_SIZE = 1e5;
1378
+ var UPDATE_COUNT_THRESHOLD = 1e3;
1377
1379
  var YjsStorage = class {
1378
- constructor(driver) {
1380
+ constructor(driver, updateCountThreshold = UPDATE_COUNT_THRESHOLD) {
1379
1381
  __publicField(this, "driver");
1382
+ __publicField(this, "updateCountThreshold");
1380
1383
  __publicField(this, "doc", new Y.Doc());
1381
1384
  // the root document
1382
- __publicField(this, "lastUpdatesById", /* @__PURE__ */ new Map());
1383
1385
  __publicField(this, "lastSnapshotById", /* @__PURE__ */ new Map());
1384
- // Keeps track of which keys are loaded, so we can clean them up without calling `.list()`
1385
- __publicField(this, "keysById", new DefaultMap2(
1386
- () => /* @__PURE__ */ new Set()
1387
- ));
1388
1386
  __publicField(this, "initPromisesById", /* @__PURE__ */ new Map());
1389
- /**
1390
- * Given a record of updates, merge them and compress if savings are significant
1391
- */
1392
- __publicField(this, "_loadAndCompressYJSUpdates", async (docUpdates, doc, docId) => {
1393
- const SAVINGS_THRESHOLD = 0.2;
1394
- const updates = Object.values(docUpdates);
1395
- const sizeOnDisk = updates.reduce((acc, update) => {
1396
- return acc + update.length;
1397
- }, 0);
1398
- if (updates.length > 0) {
1399
- const docKeys = Object.keys(docUpdates);
1400
- this.keysById.set(docId, new Set(docKeys));
1401
- const mergedUpdate = Y.mergeUpdates(updates);
1402
- Y.applyUpdate(doc, mergedUpdate);
1403
- const garbageCollectedUpdate = Y.encodeStateAsUpdate(doc);
1404
- if (garbageCollectedUpdate.length < sizeOnDisk * (1 - SAVINGS_THRESHOLD)) {
1405
- const newKey = nanoid();
1406
- await this.driver.write_y_updates(
1407
- docId,
1408
- newKey,
1409
- garbageCollectedUpdate
1410
- );
1411
- await this.driver.delete_y_updates(docId, docKeys);
1412
- this.keysById.set(docId, /* @__PURE__ */ new Set([newKey]));
1413
- }
1414
- }
1387
+ __publicField(this, "storedKeysById", /* @__PURE__ */ new Map());
1388
+ // compact the updates into a single update and write it to the durable storage
1389
+ __publicField(this, "_compactYJSUpdates", async (doc, docId, storedKeys) => {
1390
+ const compactedUpdate = Y.encodeStateAsUpdate(doc);
1391
+ const newKey = nanoid();
1392
+ await this.driver.write_y_updates(docId, newKey, compactedUpdate);
1393
+ await this.driver.delete_y_updates(docId, storedKeys);
1394
+ this.storedKeysById.set(docId, [newKey]);
1415
1395
  });
1416
1396
  __publicField(this, "_loadYDocFromDurableStorage", async (doc, docId) => {
1417
1397
  const docUpdates = Object.fromEntries(
1418
1398
  await this.driver.iter_y_updates(docId)
1419
1399
  );
1420
- await this._loadAndCompressYJSUpdates(docUpdates, doc, docId);
1421
- this.lastUpdatesById.set(docId, {
1422
- currentKey: nanoid(),
1423
- lastVector: Y.encodeStateVector(doc)
1424
- });
1400
+ const updates = Object.values(docUpdates);
1401
+ const newupdate = Y.mergeUpdates(updates);
1402
+ const storedKeys = Object.keys(docUpdates);
1403
+ Y.applyUpdate(doc, newupdate);
1404
+ if (this.shouldCompact(storedKeys)) {
1405
+ await this._compactYJSUpdates(doc, docId, storedKeys);
1406
+ } else {
1407
+ this.storedKeysById.set(docId, storedKeys);
1408
+ }
1425
1409
  doc.emit("load", [doc]);
1426
1410
  return doc;
1427
1411
  });
1428
1412
  this.driver = driver;
1413
+ this.updateCountThreshold = updateCountThreshold;
1429
1414
  this.doc.on("subdocs", ({ removed }) => {
1430
1415
  removed.forEach((subdoc) => {
1431
1416
  subdoc.destroy();
@@ -1490,7 +1475,9 @@ var YjsStorage = class {
1490
1475
  }
1491
1476
  /**
1492
1477
  * @param update base64 encoded uint8array
1493
- * @returns
1478
+ * @returns { isUpdated: boolean; snapshotHash: string }
1479
+ * isUpdated: true if the update had an effect on the YDoc
1480
+ * snapshotHash: the hash of the new snapshot
1494
1481
  */
1495
1482
  async addYDocUpdate(logger, update, guid, isV2) {
1496
1483
  const doc = guid !== void 0 ? await this.getYSubdoc(guid) : this.doc;
@@ -1505,7 +1492,7 @@ var YjsStorage = class {
1505
1492
  const afterSnapshot = this._putLastSnapshot(doc);
1506
1493
  const updated = !Y.equalSnapshots(beforeSnapshot, afterSnapshot);
1507
1494
  if (updated) {
1508
- await this.handleYDocUpdate(doc);
1495
+ await this.handleYDocUpdate(doc, updateAsU8, isV2);
1509
1496
  }
1510
1497
  return {
1511
1498
  isUpdated: updated,
@@ -1583,43 +1570,27 @@ var YjsStorage = class {
1583
1570
  return subdoc;
1584
1571
  }
1585
1572
  // When the YJS doc changes, update it in durable storage
1586
- async handleYDocUpdate(doc) {
1573
+ async handleYDocUpdate(doc, update, isV2) {
1574
+ const v1update = isV2 ? Y.convertUpdateFormatV2ToV1(update) : update;
1587
1575
  const docId = doc.guid === this.doc.guid ? ROOT_YDOC_ID : doc.guid;
1588
- const docUpdateInfo = this.lastUpdatesById.get(docId);
1589
- const updateSinceLastVector = Y.encodeStateAsUpdate(
1590
- doc,
1591
- docUpdateInfo?.lastVector
1592
- );
1593
- const storageKey = docUpdateInfo?.currentKey ?? nanoid();
1594
- if (updateSinceLastVector.length > MAX_Y_UPDATE_SIZE) {
1595
- const newKey = nanoid();
1596
- await this.driver.write_y_updates(
1597
- docId,
1598
- newKey,
1599
- Y.encodeStateAsUpdate(doc)
1600
- );
1601
- await this.driver.delete_y_updates(
1602
- docId,
1603
- Array.from(this.keysById.getOrCreate(docId))
1604
- );
1605
- this.keysById.set(docId, /* @__PURE__ */ new Set([newKey]));
1606
- this.lastUpdatesById.set(docId, {
1607
- currentKey: nanoid(),
1608
- // start writing to a new key
1609
- lastVector: Y.encodeStateVector(doc)
1610
- });
1576
+ const storedKeys = this.storedKeysById.get(docId);
1577
+ if (this.shouldCompact(storedKeys)) {
1578
+ await this._compactYJSUpdates(doc, docId, storedKeys || []);
1579
+ return;
1580
+ }
1581
+ const newKey = nanoid();
1582
+ await this.driver.write_y_updates(docId, newKey, v1update);
1583
+ if (!storedKeys) {
1584
+ this.storedKeysById.set(docId, [newKey]);
1611
1585
  } else {
1612
- await this.driver.write_y_updates(
1613
- docId,
1614
- storageKey,
1615
- updateSinceLastVector
1616
- );
1617
- const keys = [storageKey];
1618
- const currentKeys = this.keysById.getOrCreate(docId);
1619
- for (const key of keys) {
1620
- currentKeys.add(key);
1621
- }
1586
+ storedKeys.push(newKey);
1587
+ }
1588
+ }
1589
+ shouldCompact(storedKeys) {
1590
+ if (!storedKeys) {
1591
+ return false;
1622
1592
  }
1593
+ return storedKeys.length >= this.updateCountThreshold;
1623
1594
  }
1624
1595
  };
1625
1596
 
@@ -1733,7 +1704,7 @@ function stripOpId(op2) {
1733
1704
  var __socket, __debug, __lastActiveAt, __hasNotifiedClientStorageUpdateError;
1734
1705
  var BrowserSession = class {
1735
1706
  /** @internal - Never create a BrowserSession instance manually. Use the room.startBrowserSession() API instead. */
1736
- constructor(ticket, socket, debug) {
1707
+ constructor(ticket, socket, debug, createdAt) {
1737
1708
  // ^^ User-defined Session Metadata
1738
1709
  // ^^ User-defined Client Metadata (sent to client in ROOM_STATE)
1739
1710
  __publicField(this, "version");
@@ -1762,7 +1733,7 @@ var BrowserSession = class {
1762
1733
  this.publicMeta = ticket.publicMeta;
1763
1734
  __privateSet(this, __socket, socket);
1764
1735
  __privateSet(this, __debug, debug);
1765
- const now = /* @__PURE__ */ new Date();
1736
+ const now = createdAt ?? /* @__PURE__ */ new Date();
1766
1737
  this.createdAt = now;
1767
1738
  __privateSet(this, __lastActiveAt, now);
1768
1739
  __privateSet(this, __hasNotifiedClientStorageUpdateError, false);
@@ -2005,8 +1976,13 @@ var Room = class {
2005
1976
  if (this.sessions.size > 0) {
2006
1977
  throw new Error("This API can only be called before any sessions exist");
2007
1978
  }
2008
- for (const { ticket, socket, lastActivity } of sessions) {
2009
- const newSession = new BrowserSession(ticket, socket, __privateGet(this, __debug2));
1979
+ for (const { ticket, socket, lastActivity, createdAt } of sessions) {
1980
+ const newSession = new BrowserSession(
1981
+ ticket,
1982
+ socket,
1983
+ __privateGet(this, __debug2),
1984
+ createdAt
1985
+ );
2010
1986
  this.sessions.set(ticket.sessionKey, newSession);
2011
1987
  newSession.markActive(lastActivity);
2012
1988
  }