@liveblocks/server 1.1.1-pnpmtest1 → 1.2.0

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
@@ -843,7 +843,7 @@ declare class YjsStorage {
843
843
  * @returns a base64 encoded array of YJS updates
844
844
  */
845
845
  getYDocUpdate(logger: Logger, stateVector?: string, guid?: Guid, isV2?: boolean): Promise<string | null>;
846
- getYDocUpdateBinary(logger: Logger, stateVector?: string, guid?: Guid, isV2?: boolean): Promise<Uint8Array | null>;
846
+ getYDocUpdateBinary(logger: Logger, stateVector?: string, guid?: Guid, isV2?: boolean): Promise<Uint8Array<ArrayBuffer> | null>;
847
847
  getYStateVector(guid?: Guid): Promise<string | null>;
848
848
  getSnapshotHash(options: {
849
849
  guid?: Guid;
@@ -895,6 +895,8 @@ declare class YjsStorage {
895
895
 
896
896
  type LoadingState = "initial" | "loading" | "loaded";
897
897
  type ActorID = Brand<number, "ActorID">;
898
+ /** Number of milliseconds since Unix epoch. */
899
+ type Millis = Brand<number, "Millis">;
898
900
  /**
899
901
  * Session keys are also known as the "nonce" in the protocol. It's a random,
900
902
  * unique, but PRIVATE, identifier for the session, and it's important that
@@ -943,14 +945,13 @@ declare class BrowserSession<SM, CM extends JsonObject> {
943
945
  #private;
944
946
  readonly version: ProtocolVersion;
945
947
  readonly actor: ActorID;
946
- readonly createdAt: Date;
948
+ readonly createdAt: Millis;
947
949
  readonly user: IUserData;
948
950
  readonly scopes: string[];
949
951
  readonly meta: SM;
950
952
  readonly publicMeta?: CM;
951
- get lastActiveAt(): Date;
953
+ get lastActiveAt(): Millis;
952
954
  get hasNotifiedClientStorageUpdateError(): boolean;
953
- markActive(now?: Date): void;
954
955
  setHasNotifiedClientStorageUpdateError(): void;
955
956
  sendPong(): number;
956
957
  send(serverMsg: ServerMsg | ServerMsg[] | PreSerializedServerMsg): number;
@@ -1053,6 +1054,15 @@ declare class Room<RM, SM, CM extends JsonObject, C = undefined> {
1053
1054
  meta: RM;
1054
1055
  readonly driver: IStorageDriver;
1055
1056
  logger: Logger;
1057
+ /**
1058
+ * While a room is in "maintenance mode", all WebSocket connections to the
1059
+ * room should be rejected until it's pulled out of maintenance mode again.
1060
+ * Maintenance mode should only last a couple of milliseconds, typically.
1061
+ *
1062
+ * This mutex ensures that concurrent destructive operations (like
1063
+ * init-storage, storage-reset, etc.) cannot interfere with one another.
1064
+ */
1065
+ private readonly _maintenanceMode;
1056
1066
  private _loadData$;
1057
1067
  private _data;
1058
1068
  private _qsize;
@@ -1065,6 +1075,17 @@ declare class Room<RM, SM, CM extends JsonObject, C = undefined> {
1065
1075
  get yjsStorage(): YjsStorage;
1066
1076
  get mutex(): Mutex;
1067
1077
  private get data();
1078
+ /**
1079
+ * Returns true if the room is currently in maintenance mode.
1080
+ * When in maintenance mode, callers should refuse new WebSocket connections.
1081
+ */
1082
+ get isInMaintenance(): boolean;
1083
+ /**
1084
+ * Tries to enter maintenance mode and run the given callback exclusively.
1085
+ * If the room is already in maintenance mode, throws E_ALREADY_LOCKED
1086
+ * immediately instead of queuing the request.
1087
+ */
1088
+ runInMaintenanceMode<T>(callback: () => Promise<T>): Promise<T>;
1068
1089
  /**
1069
1090
  * Initializes the Room, so it's ready to start accepting connections. Safe
1070
1091
  * to call multiple times. After awaiting `room.load()` the Room is ready to
@@ -1237,7 +1258,7 @@ declare class Room<RM, SM, CM extends JsonObject, C = undefined> {
1237
1258
  /**
1238
1259
  * Concatenates multiple Uint8Arrays into a single Uint8Array.
1239
1260
  */
1240
- declare function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array;
1261
+ declare function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array<ArrayBuffer>;
1241
1262
  /**
1242
1263
  * Check if a leased session is expired.
1243
1264
  * Returns true if the current time is greater than or equal to updatedAt + ttl.
@@ -1475,19 +1496,19 @@ declare class InMemoryDriver implements IStorageDriver {
1475
1496
  initialActor?: number;
1476
1497
  initialNodes?: Iterable<[string, SerializedCrdt]>;
1477
1498
  });
1478
- raw_iter_nodes(): IterableIterator<[string, SerializedCrdt]>;
1499
+ raw_iter_nodes(): MapIterator<[string, SerializedCrdt]>;
1479
1500
  /** Deletes all nodes and replaces them with the given document. */
1480
1501
  DANGEROUSLY_reset_nodes(doc: PlainLsonObject): void;
1481
1502
  get_meta(key: string): Promise<Json | undefined>;
1482
1503
  put_meta(key: string, value: Json): Promise<void>;
1483
1504
  delete_meta(key: string): Promise<void>;
1484
- list_leased_sessions(): Promise<IterableIterator<[string, LeasedSession]>>;
1505
+ list_leased_sessions(): Promise<MapIterator<[string, LeasedSession]>>;
1485
1506
  get_leased_session(sessionId: string): Promise<LeasedSession | undefined>;
1486
1507
  put_leased_session(session: LeasedSession): Promise<void>;
1487
1508
  delete_leased_session(sessionId: string): Promise<void>;
1488
1509
  takeRowsWritten(): number;
1489
1510
  next_actor(): number;
1490
- iter_y_updates(docId: YDocId): Promise<IterableIterator<[string, Uint8Array]>>;
1511
+ iter_y_updates(docId: YDocId): Promise<IterableIterator<[string, Uint8Array<ArrayBufferLike>]>>;
1491
1512
  write_y_updates(docId: YDocId, key: string, data: Uint8Array): Promise<void>;
1492
1513
  delete_y_updates(docId: YDocId, keys: string[]): Promise<void>;
1493
1514
  /** @private Only use this in unit tests, never in production. */
@@ -1495,4 +1516,4 @@ declare class InMemoryDriver implements IStorageDriver {
1495
1516
  load_nodes_api(): IStorageDriverNodeAPI;
1496
1517
  }
1497
1518
 
1498
- export { type ActorID, BackendSession, BrowserSession, ConsoleTarget, type CreateTicketOptions, DefaultMap, type FixOp, type Guid, type IReadableSnapshot, type IServerWebSocket, type IStorageDriver, type IStorageDriverNodeAPI, type IUserData, InMemoryDriver, type LeasedSession, type LoadingState, LogLevel, LogTarget, Logger, type MetadataDB, NestedMap, type NodeMap, type NodeStream, type NodeTuple, type Pos, type PreSerializedServerMsg, ProtocolVersion, ROOT_YDOC_ID, Room, type SessionKey, type Ticket, UniqueMap, type YDocId, type YUpdate, type YVector, ackIgnoredOp, clientMsgDecoder, concatUint8Arrays, guidDecoder, isLeasedSessionExpired, jsonObjectYolo, jsonYolo, makeInMemorySnapshot, makeMetadataDB, plainLsonToNodeStream, protocolVersionDecoder, quote, serialize as serializeServerMsg, snapshotToLossyJson_eager, snapshotToLossyJson_lazy, snapshotToNodeStream, snapshotToPlainLson_eager, snapshotToPlainLson_lazy, Storage as test_only__Storage, YjsStorage as test_only__YjsStorage, transientClientMsgDecoder, tryCatch };
1519
+ export { type ActorID, BackendSession, BrowserSession, ConsoleTarget, type CreateTicketOptions, DefaultMap, type FixOp, type Guid, type IReadableSnapshot, type IServerWebSocket, type IStorageDriver, type IStorageDriverNodeAPI, type IUserData, InMemoryDriver, type LeasedSession, type LoadingState, LogLevel, LogTarget, Logger, type MetadataDB, type Millis, NestedMap, type NodeMap, type NodeStream, type NodeTuple, type Pos, type PreSerializedServerMsg, ProtocolVersion, ROOT_YDOC_ID, Room, type SessionKey, type Ticket, UniqueMap, type YDocId, type YUpdate, type YVector, ackIgnoredOp, clientMsgDecoder, concatUint8Arrays, guidDecoder, isLeasedSessionExpired, jsonObjectYolo, jsonYolo, makeInMemorySnapshot, makeMetadataDB, plainLsonToNodeStream, protocolVersionDecoder, quote, serialize as serializeServerMsg, snapshotToLossyJson_eager, snapshotToLossyJson_lazy, snapshotToNodeStream, snapshotToPlainLson_eager, snapshotToPlainLson_lazy, Storage as test_only__Storage, YjsStorage as test_only__YjsStorage, transientClientMsgDecoder, tryCatch };
package/dist/index.d.ts CHANGED
@@ -843,7 +843,7 @@ declare class YjsStorage {
843
843
  * @returns a base64 encoded array of YJS updates
844
844
  */
845
845
  getYDocUpdate(logger: Logger, stateVector?: string, guid?: Guid, isV2?: boolean): Promise<string | null>;
846
- getYDocUpdateBinary(logger: Logger, stateVector?: string, guid?: Guid, isV2?: boolean): Promise<Uint8Array | null>;
846
+ getYDocUpdateBinary(logger: Logger, stateVector?: string, guid?: Guid, isV2?: boolean): Promise<Uint8Array<ArrayBuffer> | null>;
847
847
  getYStateVector(guid?: Guid): Promise<string | null>;
848
848
  getSnapshotHash(options: {
849
849
  guid?: Guid;
@@ -895,6 +895,8 @@ declare class YjsStorage {
895
895
 
896
896
  type LoadingState = "initial" | "loading" | "loaded";
897
897
  type ActorID = Brand<number, "ActorID">;
898
+ /** Number of milliseconds since Unix epoch. */
899
+ type Millis = Brand<number, "Millis">;
898
900
  /**
899
901
  * Session keys are also known as the "nonce" in the protocol. It's a random,
900
902
  * unique, but PRIVATE, identifier for the session, and it's important that
@@ -943,14 +945,13 @@ declare class BrowserSession<SM, CM extends JsonObject> {
943
945
  #private;
944
946
  readonly version: ProtocolVersion;
945
947
  readonly actor: ActorID;
946
- readonly createdAt: Date;
948
+ readonly createdAt: Millis;
947
949
  readonly user: IUserData;
948
950
  readonly scopes: string[];
949
951
  readonly meta: SM;
950
952
  readonly publicMeta?: CM;
951
- get lastActiveAt(): Date;
953
+ get lastActiveAt(): Millis;
952
954
  get hasNotifiedClientStorageUpdateError(): boolean;
953
- markActive(now?: Date): void;
954
955
  setHasNotifiedClientStorageUpdateError(): void;
955
956
  sendPong(): number;
956
957
  send(serverMsg: ServerMsg | ServerMsg[] | PreSerializedServerMsg): number;
@@ -1053,6 +1054,15 @@ declare class Room<RM, SM, CM extends JsonObject, C = undefined> {
1053
1054
  meta: RM;
1054
1055
  readonly driver: IStorageDriver;
1055
1056
  logger: Logger;
1057
+ /**
1058
+ * While a room is in "maintenance mode", all WebSocket connections to the
1059
+ * room should be rejected until it's pulled out of maintenance mode again.
1060
+ * Maintenance mode should only last a couple of milliseconds, typically.
1061
+ *
1062
+ * This mutex ensures that concurrent destructive operations (like
1063
+ * init-storage, storage-reset, etc.) cannot interfere with one another.
1064
+ */
1065
+ private readonly _maintenanceMode;
1056
1066
  private _loadData$;
1057
1067
  private _data;
1058
1068
  private _qsize;
@@ -1065,6 +1075,17 @@ declare class Room<RM, SM, CM extends JsonObject, C = undefined> {
1065
1075
  get yjsStorage(): YjsStorage;
1066
1076
  get mutex(): Mutex;
1067
1077
  private get data();
1078
+ /**
1079
+ * Returns true if the room is currently in maintenance mode.
1080
+ * When in maintenance mode, callers should refuse new WebSocket connections.
1081
+ */
1082
+ get isInMaintenance(): boolean;
1083
+ /**
1084
+ * Tries to enter maintenance mode and run the given callback exclusively.
1085
+ * If the room is already in maintenance mode, throws E_ALREADY_LOCKED
1086
+ * immediately instead of queuing the request.
1087
+ */
1088
+ runInMaintenanceMode<T>(callback: () => Promise<T>): Promise<T>;
1068
1089
  /**
1069
1090
  * Initializes the Room, so it's ready to start accepting connections. Safe
1070
1091
  * to call multiple times. After awaiting `room.load()` the Room is ready to
@@ -1237,7 +1258,7 @@ declare class Room<RM, SM, CM extends JsonObject, C = undefined> {
1237
1258
  /**
1238
1259
  * Concatenates multiple Uint8Arrays into a single Uint8Array.
1239
1260
  */
1240
- declare function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array;
1261
+ declare function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array<ArrayBuffer>;
1241
1262
  /**
1242
1263
  * Check if a leased session is expired.
1243
1264
  * Returns true if the current time is greater than or equal to updatedAt + ttl.
@@ -1475,19 +1496,19 @@ declare class InMemoryDriver implements IStorageDriver {
1475
1496
  initialActor?: number;
1476
1497
  initialNodes?: Iterable<[string, SerializedCrdt]>;
1477
1498
  });
1478
- raw_iter_nodes(): IterableIterator<[string, SerializedCrdt]>;
1499
+ raw_iter_nodes(): MapIterator<[string, SerializedCrdt]>;
1479
1500
  /** Deletes all nodes and replaces them with the given document. */
1480
1501
  DANGEROUSLY_reset_nodes(doc: PlainLsonObject): void;
1481
1502
  get_meta(key: string): Promise<Json | undefined>;
1482
1503
  put_meta(key: string, value: Json): Promise<void>;
1483
1504
  delete_meta(key: string): Promise<void>;
1484
- list_leased_sessions(): Promise<IterableIterator<[string, LeasedSession]>>;
1505
+ list_leased_sessions(): Promise<MapIterator<[string, LeasedSession]>>;
1485
1506
  get_leased_session(sessionId: string): Promise<LeasedSession | undefined>;
1486
1507
  put_leased_session(session: LeasedSession): Promise<void>;
1487
1508
  delete_leased_session(sessionId: string): Promise<void>;
1488
1509
  takeRowsWritten(): number;
1489
1510
  next_actor(): number;
1490
- iter_y_updates(docId: YDocId): Promise<IterableIterator<[string, Uint8Array]>>;
1511
+ iter_y_updates(docId: YDocId): Promise<IterableIterator<[string, Uint8Array<ArrayBufferLike>]>>;
1491
1512
  write_y_updates(docId: YDocId, key: string, data: Uint8Array): Promise<void>;
1492
1513
  delete_y_updates(docId: YDocId, keys: string[]): Promise<void>;
1493
1514
  /** @private Only use this in unit tests, never in production. */
@@ -1495,4 +1516,4 @@ declare class InMemoryDriver implements IStorageDriver {
1495
1516
  load_nodes_api(): IStorageDriverNodeAPI;
1496
1517
  }
1497
1518
 
1498
- export { type ActorID, BackendSession, BrowserSession, ConsoleTarget, type CreateTicketOptions, DefaultMap, type FixOp, type Guid, type IReadableSnapshot, type IServerWebSocket, type IStorageDriver, type IStorageDriverNodeAPI, type IUserData, InMemoryDriver, type LeasedSession, type LoadingState, LogLevel, LogTarget, Logger, type MetadataDB, NestedMap, type NodeMap, type NodeStream, type NodeTuple, type Pos, type PreSerializedServerMsg, ProtocolVersion, ROOT_YDOC_ID, Room, type SessionKey, type Ticket, UniqueMap, type YDocId, type YUpdate, type YVector, ackIgnoredOp, clientMsgDecoder, concatUint8Arrays, guidDecoder, isLeasedSessionExpired, jsonObjectYolo, jsonYolo, makeInMemorySnapshot, makeMetadataDB, plainLsonToNodeStream, protocolVersionDecoder, quote, serialize as serializeServerMsg, snapshotToLossyJson_eager, snapshotToLossyJson_lazy, snapshotToNodeStream, snapshotToPlainLson_eager, snapshotToPlainLson_lazy, Storage as test_only__Storage, YjsStorage as test_only__YjsStorage, transientClientMsgDecoder, tryCatch };
1519
+ export { type ActorID, BackendSession, BrowserSession, ConsoleTarget, type CreateTicketOptions, DefaultMap, type FixOp, type Guid, type IReadableSnapshot, type IServerWebSocket, type IStorageDriver, type IStorageDriverNodeAPI, type IUserData, InMemoryDriver, type LeasedSession, type LoadingState, LogLevel, LogTarget, Logger, type MetadataDB, type Millis, NestedMap, type NodeMap, type NodeStream, type NodeTuple, type Pos, type PreSerializedServerMsg, ProtocolVersion, ROOT_YDOC_ID, Room, type SessionKey, type Ticket, UniqueMap, type YDocId, type YUpdate, type YVector, ackIgnoredOp, clientMsgDecoder, concatUint8Arrays, guidDecoder, isLeasedSessionExpired, jsonObjectYolo, jsonYolo, makeInMemorySnapshot, makeMetadataDB, plainLsonToNodeStream, protocolVersionDecoder, quote, serialize as serializeServerMsg, snapshotToLossyJson_eager, snapshotToLossyJson_lazy, snapshotToNodeStream, snapshotToPlainLson_eager, snapshotToPlainLson_lazy, Storage as test_only__Storage, YjsStorage as test_only__YjsStorage, transientClientMsgDecoder, tryCatch };
package/dist/index.js CHANGED
@@ -650,7 +650,7 @@ import {
650
650
  tryParseJson,
651
651
  WebsocketCloseCodes as CloseCode
652
652
  } from "@liveblocks/core";
653
- import { Mutex } from "async-mutex";
653
+ import { Mutex, tryAcquire } from "async-mutex";
654
654
  import { array as array2, formatInline } from "decoders";
655
655
  import { chunked } from "itertools";
656
656
  import { nanoid as nanoid2 } from "nanoid";
@@ -1448,7 +1448,7 @@ var YjsStorage = class {
1448
1448
  let encodedTargetVector;
1449
1449
  try {
1450
1450
  encodedTargetVector = stateVector.length > 0 ? Base64.toUint8Array(stateVector) : void 0;
1451
- } catch (e) {
1451
+ } catch {
1452
1452
  logger.warn(
1453
1453
  "Could not get update from passed vector, returning all updates"
1454
1454
  );
@@ -1722,6 +1722,7 @@ var BrowserSession = class {
1722
1722
  // Metadata sent to client in ROOM_STATE message's "meta" field
1723
1723
  __privateAdd(this, __socket);
1724
1724
  __privateAdd(this, __debug);
1725
+ /** Updated on every incoming message (including pings) via handleData(). Used for idle timeout detection. */
1725
1726
  __privateAdd(this, __lastActiveAt);
1726
1727
  // We keep a status in-memory in the session of whether we already sent a rejected ops message to the client.
1727
1728
  __privateAdd(this, __hasNotifiedClientStorageUpdateError);
@@ -1733,15 +1734,15 @@ var BrowserSession = class {
1733
1734
  this.publicMeta = ticket.publicMeta;
1734
1735
  __privateSet(this, __socket, socket);
1735
1736
  __privateSet(this, __debug, debug);
1736
- const now = createdAt ?? /* @__PURE__ */ new Date();
1737
+ const now = createdAt?.getTime() ?? Date.now();
1737
1738
  this.createdAt = now;
1738
1739
  __privateSet(this, __lastActiveAt, now);
1739
1740
  __privateSet(this, __hasNotifiedClientStorageUpdateError, false);
1740
1741
  }
1741
1742
  get lastActiveAt() {
1742
1743
  const lastPing = __privateGet(this, __socket).getLastPongTimestamp?.();
1743
- if (lastPing && lastPing > __privateGet(this, __lastActiveAt)) {
1744
- return lastPing;
1744
+ if (lastPing && lastPing.getTime() > __privateGet(this, __lastActiveAt)) {
1745
+ return lastPing.getTime();
1745
1746
  } else {
1746
1747
  return __privateGet(this, __lastActiveAt);
1747
1748
  }
@@ -1749,10 +1750,9 @@ var BrowserSession = class {
1749
1750
  get hasNotifiedClientStorageUpdateError() {
1750
1751
  return __privateGet(this, __hasNotifiedClientStorageUpdateError);
1751
1752
  }
1752
- markActive(now = /* @__PURE__ */ new Date()) {
1753
- if (now > __privateGet(this, __lastActiveAt)) {
1754
- __privateSet(this, __lastActiveAt, now);
1755
- }
1753
+ /** @internal - This should have to be called only from Room, never externally */
1754
+ markActive(now) {
1755
+ __privateSet(this, __lastActiveAt, now ? now.getTime() : Date.now());
1756
1756
  }
1757
1757
  setHasNotifiedClientStorageUpdateError() {
1758
1758
  __privateSet(this, __hasNotifiedClientStorageUpdateError, true);
@@ -1824,6 +1824,15 @@ var Room = class {
1824
1824
  __publicField(this, "meta");
1825
1825
  __publicField(this, "driver");
1826
1826
  __publicField(this, "logger");
1827
+ /**
1828
+ * While a room is in "maintenance mode", all WebSocket connections to the
1829
+ * room should be rejected until it's pulled out of maintenance mode again.
1830
+ * Maintenance mode should only last a couple of milliseconds, typically.
1831
+ *
1832
+ * This mutex ensures that concurrent destructive operations (like
1833
+ * init-storage, storage-reset, etc.) cannot interfere with one another.
1834
+ */
1835
+ __publicField(this, "_maintenanceMode", new Mutex());
1827
1836
  __publicField(this, "_loadData$", null);
1828
1837
  __publicField(this, "_data", null);
1829
1838
  __publicField(this, "_qsize", 0);
@@ -1884,6 +1893,24 @@ var Room = class {
1884
1893
  }
1885
1894
  // prettier-ignore
1886
1895
  // ------------------------------------------------------------------------------------
1896
+ // Maintenance mode
1897
+ // ------------------------------------------------------------------------------------
1898
+ /**
1899
+ * Returns true if the room is currently in maintenance mode.
1900
+ * When in maintenance mode, callers should refuse new WebSocket connections.
1901
+ */
1902
+ get isInMaintenance() {
1903
+ return this._maintenanceMode.isLocked();
1904
+ }
1905
+ /**
1906
+ * Tries to enter maintenance mode and run the given callback exclusively.
1907
+ * If the room is already in maintenance mode, throws E_ALREADY_LOCKED
1908
+ * immediately instead of queuing the request.
1909
+ */
1910
+ async runInMaintenanceMode(callback) {
1911
+ return tryAcquire(this._maintenanceMode).runExclusive(callback);
1912
+ }
1913
+ // ------------------------------------------------------------------------------------
1887
1914
  // Public API
1888
1915
  // ------------------------------------------------------------------------------------
1889
1916
  /**
@@ -2127,6 +2154,7 @@ var Room = class {
2127
2154
  );
2128
2155
  }) {
2129
2156
  const text = typeof data === "string" ? data : raise3("Unsupported message format");
2157
+ this.sessions.get(key)?.markActive();
2130
2158
  if (text === "ping") {
2131
2159
  await this.handlePing(key, ctx);
2132
2160
  } else {