@liveblocks/server 1.6.0 → 1.6.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.
- package/dist/index.cjs +56 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -2
- package/dist/index.d.ts +31 -2
- package/dist/index.js +27 -10
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/liveblocks-backend/liveblocks-backend/shared/liveblocks-server/dist/index.cjs","../src/decoders/ClientMsg.ts","../src/protocol/feedMessages.ts","../src/protocol/feedErrors.ts","../src/protocol/ProtocolVersion.ts","../src/decoders/feedMetadata.ts","../src/decoders/jsonYolo.ts","../src/decoders/Op.ts","../src/decoders/y-types.ts","../src/formats/LossyJson.ts","../src/formats/NodeStream.ts","../src/formats/PlainLson.ts","../src/makeInMemorySnapshot.ts","../src/lib/DefaultMap.ts","../src/lib/NestedMap.ts","../src/MetadataDB.ts","../src/Room.ts","../src/lib/Logger.ts","../src/plugins/InMemoryDriver.ts","../src/lib/text.ts","../src/Storage.ts","../src/YjsStorage.ts","../src/lib/tryCatch.ts","../src/lib/UniqueMap.ts","../src/utils.ts"],"names":["ProtocolVersion","string","optional","object","constant","array","taggedUnion","snapshot","CrdtType","buildObject","buildNode","buildList","buildMap","emitObject","emit","emitList","emitMap","raise","LogLevel","target","nn","isRootStorageNode","asPos","op","OpCode","intent","applyUpdate","ServerMsgCode","ClientMsgCode"],"mappings":"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AACH,QAAA;AACA,QAAA;AAA6B,UAAA;AAE7B,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACE,UAAA;AAAA,YAAA;AACE,cAAA;AACY,cAAA;AACW,YAAA;AAEzB,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAEK,MAAA;AACH,QAAA;AACA,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAEK,MAAA;AACH,QAAA;AACA,QAAA;AAAgC,UAAA;AACK,YAAA;AACpB,YAAA;AAEd,UAAA;AACH,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAEK,MAAA;AACH,QAAA;AACA,QAAA;AAAgC,UAAA;AACzB,YAAA;AACO,YAAA;AACA,YAAA;AAEZ,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAEK,MAAA;AACH,QAAA;AACA,QAAA;AAAyB,UAAA;AAEzB,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAEA,MAAA;AACE,QAAA;AACE,UAAA;AACF,QAAA;AAEA,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACF;AAp6CE;AAsDS;AhBwyFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/liveblocks-backend/liveblocks-backend/shared/liveblocks-server/dist/index.cjs","sourcesContent":[null,"/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { Json, JsonObject } from \"@liveblocks/core\";\nimport { ClientMsgCode } from \"@liveblocks/core\";\nimport type { Decoder } from \"decoders\";\nimport {\n array,\n boolean,\n constant,\n jsonObject,\n nonEmptyString,\n number,\n object,\n optional,\n string,\n taggedUnion,\n} from \"decoders\";\n\nimport type {\n AddFeedClientMsg,\n AddFeedMessageClientMsg,\n BroadcastEventClientMsg,\n ClientMsg,\n DeleteFeedClientMsg,\n DeleteFeedMessageClientMsg,\n FetchFeedMessagesClientMsg,\n FetchFeedsClientMsg,\n FetchStorageClientMsg,\n FetchYDocClientMsg,\n UpdateFeedClientMsg,\n UpdateFeedMessageClientMsg,\n UpdatePresenceClientMsg,\n UpdateStorageClientMsg,\n UpdateYDocClientMsg,\n} from \"~/protocol\";\nimport { FeedMsgCode } from \"~/protocol\";\n\nimport {\n feedMetadataUpdateDecoder,\n fetchFeedsMetadataFilterDecoder,\n optionalFeedMetadataDecoder,\n} from \"./feedMetadata\";\nimport { jsonObjectYolo, jsonYolo } from \"./jsonYolo\";\nimport { op } from \"./Op\";\nimport type { YUpdate, YVector } from \"./y-types\";\nimport { guidDecoder } from \"./y-types\";\n\nconst updatePresenceClientMsg: Decoder<UpdatePresenceClientMsg<JsonObject>> =\n object({\n type: constant(ClientMsgCode.UPDATE_PRESENCE),\n data: jsonObjectYolo,\n targetActor: optional(number),\n });\n\nconst broadcastEventClientMsg: Decoder<BroadcastEventClientMsg<Json>> = object({\n type: constant(ClientMsgCode.BROADCAST_EVENT),\n event: jsonYolo,\n});\n\nconst fetchStorageClientMsg: Decoder<FetchStorageClientMsg> = object({\n type: constant(ClientMsgCode.FETCH_STORAGE),\n});\n\nconst updateStorageClientMsg: Decoder<UpdateStorageClientMsg> = object({\n type: constant(ClientMsgCode.UPDATE_STORAGE),\n ops: array(op),\n});\n\nconst fetchYDocClientMsg: Decoder<FetchYDocClientMsg> = object({\n type: constant(ClientMsgCode.FETCH_YDOC),\n vector: string.refineType<YVector>(),\n guid: optional(guidDecoder), // Don't specify to update the root doc\n v2: optional(boolean),\n});\n\nconst updateYDocClientMsg: Decoder<UpdateYDocClientMsg> = object({\n type: constant(ClientMsgCode.UPDATE_YDOC),\n update: string.refineType<YUpdate>(),\n guid: optional(guidDecoder), // Don't specify to update the root doc\n v2: optional(boolean),\n});\n\n// Feed message decoders\nconst fetchFeedsClientMsg: Decoder<FetchFeedsClientMsg> = object({\n type: constant(FeedMsgCode.FETCH_FEEDS),\n requestId: string,\n cursor: optional(string),\n since: optional(number),\n limit: optional(number),\n metadata: fetchFeedsMetadataFilterDecoder,\n});\n\nconst fetchFeedMessagesClientMsg: Decoder<FetchFeedMessagesClientMsg> = object({\n type: constant(FeedMsgCode.FETCH_FEED_MESSAGES),\n requestId: string,\n feedId: nonEmptyString,\n cursor: optional(string),\n since: optional(number),\n limit: optional(number),\n});\n\nconst addFeedClientMsg: Decoder<AddFeedClientMsg> = object({\n type: constant(FeedMsgCode.ADD_FEED),\n feedId: string,\n metadata: optionalFeedMetadataDecoder,\n timestamp: optional(number),\n requestId: optional(string),\n});\n\nconst updateFeedClientMsg: Decoder<UpdateFeedClientMsg> = object({\n type: constant(FeedMsgCode.UPDATE_FEED),\n feedId: string,\n metadata: feedMetadataUpdateDecoder,\n requestId: optional(string),\n});\n\nconst deleteFeedClientMsg: Decoder<DeleteFeedClientMsg> = object({\n type: constant(FeedMsgCode.DELETE_FEED),\n feedId: string,\n requestId: optional(string),\n});\n\nconst addFeedMessageClientMsg: Decoder<AddFeedMessageClientMsg> = object({\n type: constant(FeedMsgCode.ADD_FEED_MESSAGE),\n feedId: string,\n data: jsonObject,\n id: optional(string),\n timestamp: optional(number),\n requestId: optional(string),\n});\n\nconst updateFeedMessageClientMsg: Decoder<UpdateFeedMessageClientMsg> = object({\n type: constant(FeedMsgCode.UPDATE_FEED_MESSAGE),\n feedId: string,\n messageId: string,\n data: jsonObject,\n timestamp: optional(number),\n requestId: optional(string),\n});\n\nconst deleteFeedMessageClientMsg: Decoder<DeleteFeedMessageClientMsg> = object({\n type: constant(FeedMsgCode.DELETE_FEED_MESSAGE),\n feedId: string,\n messageId: string,\n requestId: optional(string),\n});\n\nexport const clientMsgDecoder: Decoder<ClientMsg<JsonObject, Json>> =\n taggedUnion(\"type\", {\n [ClientMsgCode.UPDATE_PRESENCE]: updatePresenceClientMsg,\n [ClientMsgCode.BROADCAST_EVENT]: broadcastEventClientMsg,\n [ClientMsgCode.FETCH_STORAGE]: fetchStorageClientMsg,\n [ClientMsgCode.UPDATE_STORAGE]: updateStorageClientMsg,\n [ClientMsgCode.FETCH_YDOC]: fetchYDocClientMsg,\n [ClientMsgCode.UPDATE_YDOC]: updateYDocClientMsg,\n [FeedMsgCode.FETCH_FEEDS]: fetchFeedsClientMsg,\n [FeedMsgCode.FETCH_FEED_MESSAGES]: fetchFeedMessagesClientMsg,\n [FeedMsgCode.ADD_FEED]: addFeedClientMsg,\n [FeedMsgCode.UPDATE_FEED]: updateFeedClientMsg,\n [FeedMsgCode.DELETE_FEED]: deleteFeedClientMsg,\n [FeedMsgCode.ADD_FEED_MESSAGE]: addFeedMessageClientMsg,\n [FeedMsgCode.UPDATE_FEED_MESSAGE]: updateFeedMessageClientMsg,\n [FeedMsgCode.DELETE_FEED_MESSAGE]: deleteFeedMessageClientMsg,\n } as unknown as Record<\n string | number,\n Decoder<ClientMsg<JsonObject, Json>>\n >).describe(\"Must be a valid client message\");\n\nexport const transientClientMsgDecoder: Decoder<ClientMsg<JsonObject, Json>> =\n taggedUnion(\"type\", {\n // [ClientMsgCode.UPDATE_PRESENCE]: updatePresenceClientMsg,\n // [ClientMsgCode.BROADCAST_EVENT]: broadcastEventClientMsg,\n // [ClientMsgCode.FETCH_STORAGE]: fetchStorageClientMsg,\n [ClientMsgCode.UPDATE_STORAGE]: updateStorageClientMsg,\n // [ClientMsgCode.FETCH_YDOC]: fetchYDocClientMsg,\n // [ClientMsgCode.UPDATE_YDOC]: updateYDocClientMsg,\n }).describe(\"Must be a valid transient client message\");\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\n/**\n * NOTE: this will be moved to @liveblocks/core in the future.\n * Feed WebSocket message codes.\n * 50x = server → client, 51x = client → server.\n */\nexport const FeedMsgCode = {\n // Server → client (50x)\n FEEDS_LIST: 500,\n FEEDS_ADDED: 501,\n FEEDS_UPDATED: 502,\n FEED_DELETED: 503,\n FEED_MESSAGES_LIST: 504,\n FEED_MESSAGES_ADDED: 505,\n FEED_MESSAGES_UPDATED: 506,\n FEED_MESSAGES_DELETED: 507,\n FEED_REQUEST_FAILED: 508,\n // Client → server (51x)\n FETCH_FEEDS: 510,\n FETCH_FEED_MESSAGES: 511,\n ADD_FEED: 512,\n UPDATE_FEED: 513,\n DELETE_FEED: 514,\n ADD_FEED_MESSAGE: 515,\n UPDATE_FEED_MESSAGE: 516,\n DELETE_FEED_MESSAGE: 517,\n} as const;\n\n/** Error codes for {@link FeedRequestFailedServerMsg}. */\nexport const FeedRequestErrorCode = {\n INTERNAL: \"INTERNAL\",\n FEED_ALREADY_EXISTS: \"FEED_ALREADY_EXISTS\",\n FEED_NOT_FOUND: \"FEED_NOT_FOUND\",\n FEED_MESSAGE_NOT_FOUND: \"FEED_MESSAGE_NOT_FOUND\",\n} as const;\n\nimport type { JsonObject } from \"@liveblocks/core\";\n\nimport type { Feed, FeedMessage } from \"../types\";\n\n// ─── Server messages (50x) ───────────────────────────────────────────────────\n\nexport type FeedsListServerMsg = {\n type: 500;\n requestId: string;\n feeds: Feed[];\n nextCursor?: string;\n};\n\nexport type FeedsAddedServerMsg = {\n type: 501;\n feeds: Feed[];\n};\n\nexport type FeedsUpdatedServerMsg = {\n type: 502;\n feeds: Feed[];\n};\n\nexport type FeedDeletedServerMsg = {\n type: 503;\n feedId: string;\n};\n\nexport type FeedRequestFailedServerMsg = {\n type: 508;\n requestId: string;\n code: string;\n reason?: string;\n};\n\nexport type FeedsServerMsg =\n | FeedsListServerMsg\n | FeedsAddedServerMsg\n | FeedsUpdatedServerMsg\n | FeedDeletedServerMsg\n | FeedRequestFailedServerMsg;\n\nexport type FeedMessagesListServerMsg = {\n type: 504;\n requestId: string;\n feedId: string;\n messages: FeedMessage[];\n nextCursor?: string;\n};\n\nexport type FeedMessagesAddedServerMsg = {\n type: 505;\n feedId: string;\n messages: FeedMessage[];\n};\n\nexport type FeedMessagesUpdatedServerMsg = {\n type: 506;\n feedId: string;\n messages: FeedMessage[];\n};\n\nexport type FeedMessagesDeletedServerMsg = {\n type: 507;\n feedId: string;\n messageIds: string[];\n};\n\nexport type FeedMessagesServerMsg =\n | FeedMessagesListServerMsg\n | FeedMessagesAddedServerMsg\n | FeedMessagesUpdatedServerMsg\n | FeedMessagesDeletedServerMsg\n | FeedRequestFailedServerMsg;\n\n// ─── Client messages (51x) ───────────────────────────────────────────────────\n\nexport type FetchFeedsClientMsg = {\n type: 510;\n requestId: string;\n cursor?: string;\n since?: number;\n limit?: number;\n /** Match feeds whose metadata contains these keys (same value rules as room metadata updates). */\n metadata?: Record<string, string | string[] | null>;\n};\n\nexport type FetchFeedMessagesClientMsg = {\n type: 511;\n requestId: string;\n feedId: string;\n cursor?: string;\n since?: number;\n limit?: number;\n};\n\nexport type AddFeedClientMsg = {\n type: 512;\n feedId: string;\n metadata?: Record<string, string[] | string>;\n timestamp?: number;\n requestId?: string;\n};\n\nexport type UpdateFeedClientMsg = {\n type: 513;\n feedId: string;\n metadata: Record<string, string[] | string | null>;\n requestId?: string;\n};\n\nexport type DeleteFeedClientMsg = {\n type: 514;\n feedId: string;\n requestId?: string;\n};\n\nexport type AddFeedMessageClientMsg = {\n type: 515;\n feedId: string;\n data: JsonObject;\n id?: string;\n timestamp?: number;\n requestId?: string;\n};\n\nexport type UpdateFeedMessageClientMsg = {\n type: 516;\n feedId: string;\n messageId: string;\n data: JsonObject;\n timestamp?: number;\n requestId?: string;\n};\n\nexport type DeleteFeedMessageClientMsg = {\n type: 517;\n feedId: string;\n messageId: string;\n requestId?: string;\n};\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport {\n FeedMsgCode,\n FeedRequestErrorCode,\n type FeedRequestFailedServerMsg,\n} from \"./feedMessages\";\n\nexport function feedRequestFailed(\n requestId: string | undefined,\n code: string,\n reason?: string\n): FeedRequestFailedServerMsg {\n return {\n type: FeedMsgCode.FEED_REQUEST_FAILED,\n requestId: requestId ?? \"\",\n code,\n reason,\n };\n}\n\n/**\n * Maps driver / Room errors to stable {@link FeedRequestFailedServerMsg} codes.\n * Prefer matching known {@link Error#message} shapes storage drivers.\n */\nexport function mapFeedError(err: unknown): { code: string; reason?: string } {\n if (!(err instanceof Error)) {\n return { code: FeedRequestErrorCode.INTERNAL };\n }\n\n const m = err.message;\n\n if (m.includes(\"already exists\")) {\n return { code: FeedRequestErrorCode.FEED_ALREADY_EXISTS, reason: m };\n }\n\n if (m.includes(\"Feed message\") && m.includes(\"not found\")) {\n return { code: FeedRequestErrorCode.FEED_MESSAGE_NOT_FOUND, reason: m };\n }\n\n if (m.includes(\"not found\")) {\n return { code: FeedRequestErrorCode.FEED_NOT_FOUND, reason: m };\n }\n\n return { code: FeedRequestErrorCode.INTERNAL, reason: m };\n}\n\n/** Maps an arbitrary thrown value to a {@link FeedRequestFailedServerMsg}. */\nexport function feedFailureServerMsg(\n requestId: string | undefined,\n err: unknown\n): FeedRequestFailedServerMsg {\n const mapped = mapFeedError(err);\n return feedRequestFailed(requestId, mapped.code, mapped.reason);\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport { enum_ } from \"decoders\";\n\nexport enum ProtocolVersion {\n //\n // NOTE:\n // The following versions were once used, but there is no usage of it anymore\n // in the wild, so we've removed support for them:\n //\n // V1 - Initial version\n // V2 - ?\n // V3 - started to broadcast storage operations to the sender to fix some\n // conflicts\n // V4 - created a virtual root to fix an issue where multiple people\n // initialize the storage at the same time\n // V5 - started to broadcast messages in a batch (arrays) for clients\n // V6 - started to validate inputs with decoders.\n //\n\n /**\n * V7 changes the URL params used to authorize the user.\n *\n * In V6 and lower, the ?token= URL param is used, which will only ever\n * contain a `pub-legacy` or `sec-legacy` token.\n *\n * URL PARAM CHANGES:\n * Starting with V7, the ?token= is no longer a legal URL param. Instead,\n * either of the following params is used:\n *\n * - ?tok=... for ID tokens\n * - ?tok=... for Access tokens\n * - ?tok=... for Secret Legacy tokens\n * - ?pubkey=... for public keys (no token, public key can be directly used here)\n *\n * Note that `pub-legacy` tokens are no longer accepted in V7, and are\n * replaced by the direct use of the public key.\n *\n * BEHAVIORAL CHANGES:\n * Starting with V7, the RoomState server message that gets sent when\n * a client initially connects will now include new fields:\n *\n * - `actor`\n * - `scopes`\n *\n * Since v1.2.0 (Jul 31, 2023)\n */\n V7 = 7,\n\n /**\n * V8 changes storage response format and allows streaming.\n *\n * MESSAGE FORMAT CHANGES:\n * - V8: sends 1+ STORAGE_CHUNK messages, followed by 1 final\n * STORAGE_STREAM_END message (with compact nodes)\n * - V7: sends 1 STORAGE_STATE_V7 message (with full nodes)\n *\n * STREAMING BEHAVIOR in V8:\n * - For SQLite-backed rooms: nodes are split into multiple STORAGE_CHUNK\n * messages, followed by STORAGE_STREAM_END\n * - For KV-backed rooms: all nodes are sent in a single STORAGE_CHUNK\n * message that will contain all nodes, followed by STORAGE_STREAM_END\n *\n * Since 3.14.0\n */\n V8 = 8,\n}\n\nexport const protocolVersionDecoder = enum_(ProtocolVersion).describe(\n \"Unsupported protocol version\"\n);\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\n/**\n * Feed metadata decoders — same rules as room/thread metadata (see @shared/common\n * createMetadataDecoder / updateRoomMetadataDecoder).\n */\n\nimport type { Decoder } from \"decoders\";\nimport {\n array,\n either,\n identifier,\n nullable,\n optional,\n poja,\n record,\n select,\n sized,\n string,\n} from \"decoders\";\n\n/** Same cap as createMetadataDecoder in @shared/common */\nconst MAX_METADATA_COUNT = 50;\nconst MAX_METADATA_VALUE_LIST_LENGTH = 50;\n\nexport const feedMetadataIdDecoder = sized(identifier, { min: 1, max: 40 });\n\nconst metadataStringValue = sized(string, { max: 256 });\n\nfunction createRoomMetadataValueDecoder(): Decoder<string | string[]> {\n return select(\n either(string, poja).describe(\"Must be string or string[]\"),\n (x) =>\n typeof x === \"string\"\n ? metadataStringValue\n : array(metadataStringValue).refine(\n (value) => value.length <= MAX_METADATA_VALUE_LIST_LENGTH,\n `Must be at most ${MAX_METADATA_VALUE_LIST_LENGTH} items`\n )\n );\n}\n\nconst roomMetadataValueDecoder = createRoomMetadataValueDecoder();\n\n/**\n * A single metadata value: string, string[], or null (null clears a key on update).\n */\nconst feedMetadataNullableValueDecoder = nullable(roomMetadataValueDecoder);\n\nconst feedMetadataRecordForCreate = record(\n feedMetadataIdDecoder,\n roomMetadataValueDecoder\n).refine(\n (value) => Object.keys(value).length <= MAX_METADATA_COUNT,\n `Must be at most ${MAX_METADATA_COUNT} items`\n);\n\n/**\n * Optional feed metadata on create (WebSocket ADD_FEED, HTTP POST …/feeds).\n * Same rules as createMetadataDecoder / room metadata on create (no null values).\n */\nexport const optionalFeedMetadataDecoder = optional(\n feedMetadataRecordForCreate\n);\n\n/**\n * Full metadata object for update (WebSocket UPDATE_FEED, HTTP PATCH …/feeds/:id).\n * Same shape as updateRoomMetadataDecoder.\n */\nexport const feedMetadataUpdateDecoder = record(\n feedMetadataIdDecoder,\n feedMetadataNullableValueDecoder\n);\n\nconst feedMetadataRecordForFilter = record(\n feedMetadataIdDecoder,\n feedMetadataNullableValueDecoder\n).refine(\n (value) => Object.keys(value).length <= MAX_METADATA_COUNT,\n `Must be at most ${MAX_METADATA_COUNT} items`\n);\n\n/**\n * Optional metadata filter for FETCH_FEEDS — same key/value rules as\n * {@link feedMetadataUpdateDecoder} / updateRoomMetadataDecoder in @shared/common.\n */\nexport const fetchFeedsMetadataFilterDecoder = optional(\n feedMetadataRecordForFilter\n);\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { Json, JsonObject } from \"@liveblocks/core\";\nimport type { Decoder } from \"decoders\";\nimport { unknown } from \"decoders\";\n\n/**\n * Drop-in replacement for the `json` decoder from the decoders standard\n * library, but implemented as a no-op. This is, of course, only safe to use in\n * contexts where you know that the input already is valid JSON.\n *\n * You know this for sure, for example, if you're decoding the result of\n * a `JSON.parse()` call.\n *\n * Done for performance reasons!\n */\nexport const jsonYolo: Decoder<Json> = unknown as Decoder<Json>;\n\n/**\n * Drop-in replacement for the `jsonObject` decoder from the decoders standard\n * library, but implemented as just a check for plain old JavaScript object.\n * This is, of course, only safe to use in contexts where you know that the\n * input already is valid JSON.\n *\n * You know this for sure, for example, if you're decoding the result of\n * a `JSON.parse()` call.\n *\n * Done for performance reasons!\n */\nexport const jsonObjectYolo: Decoder<JsonObject> = jsonYolo.refine(\n (value): value is JsonObject =>\n value !== null && typeof value === \"object\" && !Array.isArray(value),\n \"Must be JSON object\"\n);\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport { OpCode } from \"@liveblocks/core\";\nimport type { Decoder } from \"decoders\";\nimport {\n constant,\n object,\n oneOf,\n optional,\n string,\n taggedUnion,\n} from \"decoders\";\n\nimport type {\n ClientWireOp,\n CreateListOp,\n CreateMapOp,\n CreateObjectOp,\n CreateRegisterOp,\n DeleteCrdtOp,\n DeleteObjectKeyOp,\n SetParentKeyOp,\n UpdateObjectOp,\n} from \"~/protocol\";\n\nimport { jsonObjectYolo, jsonYolo } from \"./jsonYolo\";\n\ntype HasOpId = { opId: string };\n\nconst intent = oneOf([\"set\", \"push\"] as const);\n\nconst updateObjectOp: Decoder<UpdateObjectOp & HasOpId> = object({\n type: constant(OpCode.UPDATE_OBJECT),\n opId: string,\n id: string,\n data: jsonObjectYolo,\n});\n\nconst createObjectOp: Decoder<CreateObjectOp & HasOpId> = object({\n type: constant(OpCode.CREATE_OBJECT),\n opId: string,\n id: string,\n parentId: string,\n parentKey: string,\n data: jsonObjectYolo,\n intent: optional(intent),\n deletedId: optional(string),\n});\n\nconst createListOp: Decoder<CreateListOp & HasOpId> = object({\n type: constant(OpCode.CREATE_LIST),\n opId: string,\n id: string,\n parentId: string,\n parentKey: string,\n intent: optional(intent),\n deletedId: optional(string),\n});\n\nconst createMapOp: Decoder<CreateMapOp & HasOpId> = object({\n type: constant(OpCode.CREATE_MAP),\n opId: string,\n id: string,\n parentId: string,\n parentKey: string,\n intent: optional(intent),\n deletedId: optional(string),\n});\n\nconst createRegisterOp: Decoder<CreateRegisterOp & HasOpId> = object({\n type: constant(OpCode.CREATE_REGISTER),\n opId: string,\n id: string,\n parentId: string,\n parentKey: string,\n data: jsonYolo,\n intent: optional(intent),\n deletedId: optional(string),\n});\n\nconst deleteCrdtOp: Decoder<DeleteCrdtOp & HasOpId> = object({\n type: constant(OpCode.DELETE_CRDT),\n opId: string,\n id: string,\n});\n\nconst setParentKeyOp: Decoder<SetParentKeyOp & HasOpId> = object({\n type: constant(OpCode.SET_PARENT_KEY),\n opId: string,\n id: string,\n parentKey: string,\n});\n\nconst deleteObjectKeyOp: Decoder<DeleteObjectKeyOp & HasOpId> = object({\n type: constant(OpCode.DELETE_OBJECT_KEY),\n opId: string,\n id: string,\n key: string,\n});\n\nexport const op: Decoder<ClientWireOp> = taggedUnion(\"type\", {\n [OpCode.UPDATE_OBJECT]: updateObjectOp,\n [OpCode.CREATE_OBJECT]: createObjectOp,\n [OpCode.CREATE_LIST]: createListOp,\n [OpCode.CREATE_MAP]: createMapOp,\n [OpCode.CREATE_REGISTER]: createRegisterOp,\n [OpCode.DELETE_CRDT]: deleteCrdtOp,\n [OpCode.SET_PARENT_KEY]: setParentKeyOp,\n [OpCode.DELETE_OBJECT_KEY]: deleteObjectKeyOp,\n});\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { Brand } from \"@liveblocks/core\";\nimport { uuid } from \"decoders\";\n\n/**\n * A guid, a unique identifier for a Yjs sub document.\n */\nexport type Guid = Brand<string, \"Guid\">;\n\nexport const guidDecoder = uuid.refineType<Guid>();\n\nexport const ROOT_YDOC_ID = \"root\";\nexport type YDocId = typeof ROOT_YDOC_ID | Guid /* unique ID for subdoc */;\n\n/**\n * Any string that is a valid base64 encoded YJS update.\n */\nexport type YUpdate = Brand<string, \"YUpdate\">;\n\n/**\n * Any string that is a valid base64 encoded YJS state vector.\n */\nexport type YVector = Brand<string, \"YVector\">;\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { Json, JsonObject } from \"@liveblocks/core\";\nimport { CrdtType } from \"@liveblocks/core\";\n\nimport type { IReadableSnapshot } from \"~/interfaces\";\n\n// ---------------------------------------------------------------------------\n// Non-streaming version\n// ---------------------------------------------------------------------------\n\n/**\n * Serialize a storage snapshot to a simple JSON representation, returning a\n * full in-memory JsonObject. Faster than snapshotToLossyJson_lazy for\n * small/medium documents because the result can be passed straight to\n * JSON.stringify(). This format is lossy — the original storage structure\n * cannot be reconstructed from it, so it's output-only.\n */\nexport function snapshotToLossyJson_eager(\n snapshot: IReadableSnapshot\n): JsonObject {\n try {\n return buildObject(snapshot, \"root\", snapshot.get_root().data);\n } finally {\n snapshot.destroy();\n }\n}\n\nfunction buildNode(snapshot: IReadableSnapshot, id: string): Json {\n const node = snapshot.get_node(id);\n if (node.type === CrdtType.OBJECT) {\n return buildObject(snapshot, id, node.data);\n } else if (node.type === CrdtType.LIST) {\n return buildList(snapshot, id);\n } else if (node.type === CrdtType.MAP) {\n return buildMap(snapshot, id);\n } else {\n return node.data;\n }\n}\n\nfunction buildObject(\n snapshot: IReadableSnapshot,\n id: string,\n staticData: JsonObject\n): JsonObject {\n const data = Object.assign(Object.create(null), staticData) as JsonObject;\n for (const [key, childId] of snapshot.iter_children(id)) {\n data[key] = buildNode(snapshot, childId);\n }\n return data;\n}\n\nfunction buildList(snapshot: IReadableSnapshot, id: string): Json[] {\n const data: Json[] = [];\n for (const [_, childId] of snapshot.iter_children(id)) {\n data.push(buildNode(snapshot, childId));\n }\n return data;\n}\n\nfunction buildMap(snapshot: IReadableSnapshot, id: string): JsonObject {\n const data = Object.create(null) as JsonObject;\n for (const [key, childId] of snapshot.iter_children(id)) {\n data[key] = buildNode(snapshot, childId);\n }\n return data;\n}\n\n// ---------------------------------------------------------------------------\n// Streaming version\n// ---------------------------------------------------------------------------\n\n// Generator-of-strings type alias for brevity of signatures\ntype StringGen = Generator<string, void, never>;\n\n/**\n * Serialize a storage snapshot to a simple JSON representation. This format is\n * easy to consume but lossy — the original storage structure cannot be\n * reconstructed from it, so it's an output-only format. Slower than\n * snapshotToLossyJson_eager but can stream documents that don't fit entirely\n * in memory.\n *\n * This generator yields text chunks that together, when concatenated, form the\n * output JSON document.\n */\nexport function* snapshotToLossyJson_lazy(\n snapshot: IReadableSnapshot\n): StringGen {\n try {\n const staticJson = JSON.stringify(snapshot.get_root().data).slice(1, -1);\n yield* emitObject(snapshot, \"root\", staticJson);\n } finally {\n snapshot.destroy();\n }\n}\n\nfunction* emit(snapshot: IReadableSnapshot, id: string): StringGen {\n const node = snapshot.get_node(id);\n if (node.type === CrdtType.OBJECT) {\n yield* emitObject(snapshot, id, JSON.stringify(node.data).slice(1, -1));\n } else if (node.type === CrdtType.LIST) {\n yield* emitList(snapshot, id);\n } else if (node.type === CrdtType.MAP) {\n yield* emitMap(snapshot, id);\n } else if (node.type === CrdtType.REGISTER) {\n yield JSON.stringify(node.data);\n }\n}\n\n/**\n * @param staticJson - The object's static (non-CRDT) properties as a raw JSON\n * string without the surrounding braces, e.g. `\"foo\":1,\"bar\":\"hi\"`.\n *\n * Children are emitted _after_ the static properties. If a child key\n * collides with a static key (which shouldn't normally happen, but\n * defensively), the child wins because JSON.parse keeps the last value\n * for duplicate keys.\n */\nfunction* emitObject(\n snapshot: IReadableSnapshot,\n id: string,\n staticJson: string\n): StringGen {\n let comma = staticJson.length > 0;\n\n yield \"{\";\n yield staticJson;\n\n for (const [key, childId] of snapshot.iter_children(id)) {\n if (comma) yield \",\";\n else comma = true;\n\n yield `${JSON.stringify(key)}:`;\n yield* emit(snapshot, childId);\n }\n yield \"}\";\n}\n\nfunction* emitList(snapshot: IReadableSnapshot, id: string): StringGen {\n let comma = false;\n\n yield \"[\";\n for (const [_, childId] of snapshot.iter_children(id)) {\n if (comma) yield \",\";\n else comma = true;\n yield* emit(snapshot, childId);\n }\n yield \"]\";\n}\n\nfunction* emitMap(snapshot: IReadableSnapshot, id: string): StringGen {\n let comma = false;\n\n yield \"{\";\n for (const [key, childId] of snapshot.iter_children(id)) {\n if (comma) yield \",\";\n else comma = true;\n\n yield `${JSON.stringify(key)}:`;\n yield* emit(snapshot, childId);\n }\n yield \"}\";\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { StorageNode } from \"@liveblocks/core\";\n\nimport type { IReadableSnapshot } from \"~/interfaces\";\n\n/**\n * Yield all nodes from a snapshot as [id, crdt] tuples.\n * Destroys the snapshot when done (or aborted).\n */\nexport function* snapshotToNodeStream(\n snapshot: IReadableSnapshot\n): Generator<StorageNode, void, never> {\n try {\n yield* snapshot.iter_all();\n } finally {\n snapshot.destroy();\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type {\n JsonObject,\n ObjectStorageNode,\n PlainLson,\n PlainLsonFields,\n PlainLsonList,\n PlainLsonMap,\n PlainLsonObject,\n RootStorageNode,\n SerializedList,\n StorageNode,\n} from \"@liveblocks/core\";\nimport {\n assertNever,\n CrdtType,\n isJsonObject,\n makePosition,\n} from \"@liveblocks/core\";\n\nimport type { IReadableSnapshot } from \"~/interfaces\";\n\nconst SERVER_INIT_OP_PREFIX = \"si\";\n\nfunction generateId(state: { clock: number }) {\n return `${SERVER_INIT_OP_PREFIX}:${state.clock++}`;\n}\n\nfunction isSpecialPlainLsonValue(\n value: PlainLson\n): value is PlainLsonObject | PlainLsonMap | PlainLsonList {\n return isJsonObject(value) && value.liveblocksType !== undefined;\n}\n\n/**\n * Generator that yields NodeTuples for a JSON value.\n * Always yields parent nodes before their children.\n */\nfunction* iterJson(\n key: string,\n data: PlainLson,\n parent: StorageNode,\n state: { clock: number }\n): Generator<StorageNode, void, undefined> {\n if (isSpecialPlainLsonValue(data)) {\n switch (data.liveblocksType) {\n case \"LiveObject\":\n yield* iterObjectInner(key, data.data, parent, state);\n return;\n\n case \"LiveList\":\n yield* iterList(key, data.data, parent, state);\n return;\n\n case \"LiveMap\":\n yield* iterMap(key, data.data, parent, state);\n return;\n\n // istanbul ignore next\n default:\n assertNever(data, \"Unknown `liveblocksType` field\");\n }\n } else {\n yield [\n generateId(state),\n {\n type: CrdtType.REGISTER,\n data,\n parentId: parent[0],\n parentKey: key,\n },\n ];\n }\n}\n\n/**\n * Generator that yields NodeTuples for a LiveMap.\n * Yields the map node first, then its children.\n */\nfunction* iterMap(\n key: string,\n map: PlainLsonFields,\n parent: StorageNode,\n state: { clock: number }\n): Generator<StorageNode, void, undefined> {\n const mapTuple: StorageNode = [\n generateId(state),\n { type: CrdtType.MAP, parentId: parent[0], parentKey: key },\n ];\n\n // Yield the map node first (parent before children)\n yield mapTuple;\n\n // Then yield all children\n for (const [subKey, subValue] of Object.entries(map)) {\n yield* iterJson(subKey, subValue, mapTuple, state);\n }\n}\n\n/**\n * Generator that yields NodeTuples for a LiveList.\n * Yields the list node first, then its children.\n */\nfunction* iterList(\n key: string,\n list: PlainLson[],\n parent: StorageNode,\n state: { clock: number }\n): Generator<StorageNode, void, undefined> {\n const id = generateId(state);\n const crdt: SerializedList = {\n type: CrdtType.LIST,\n parentId: parent[0],\n parentKey: key,\n };\n const listTuple: StorageNode = [id, crdt];\n\n // Yield the list node first (parent before children)\n yield listTuple;\n\n // Then yield all children\n let position = makePosition();\n for (const subValue of list) {\n yield* iterJson(position, subValue, listTuple, state);\n position = makePosition(position);\n }\n}\n\n/**\n * Generator that yields NodeTuples for a LiveObject.\n * Yields the object node first, then its children.\n *\n * Note: The object's data field is populated with non-special values\n * (primitives, arrays, plain objects), while special values (LiveObject,\n * LiveList, LiveMap) are yielded as separate nodes.\n */\nfunction* iterObjectInner(\n key: string,\n value: PlainLsonFields,\n parent: StorageNode | null,\n state: { clock: number }\n): Generator<StorageNode, void, undefined> {\n // First pass: collect non-special data and identify special children\n const data: JsonObject = {};\n const specialChildren: Array<[string, PlainLson]> = [];\n\n for (const [subKey, subValue] of Object.entries(value)) {\n if (isSpecialPlainLsonValue(subValue)) {\n specialChildren.push([subKey, subValue]);\n } else {\n data[subKey] = subValue;\n }\n }\n\n // Create the object tuple with collected data\n const objectTuple: RootStorageNode | ObjectStorageNode =\n parent !== null\n ? [\n generateId(state),\n {\n type: CrdtType.OBJECT,\n data,\n parentId: parent[0],\n parentKey: key,\n },\n ]\n : [\"root\", { type: CrdtType.OBJECT, data }];\n\n // Yield the object node first (parent before children)\n yield objectTuple;\n\n // Then yield all special children\n for (const [subKey, subValue] of specialChildren) {\n yield* iterJson(subKey, subValue, objectTuple, state);\n }\n}\n\n/**\n * Transform a \"Plain LSON\" document to a lazy NodeStream. Used to initialize\n * the storage with a predefined state.\n * Always emits parent nodes before their children.\n */\nexport function* plainLsonToNodeStream(\n root: PlainLsonObject\n): Generator<StorageNode, void, undefined> {\n const state = { clock: 1 };\n yield* iterObjectInner(\"root\", root.data, null, state);\n}\n\n// ---------------------------------------------------------------------------\n// Non-streaming serialization: builds a full PlainLsonObject in memory.\n// ---------------------------------------------------------------------------\n\n/**\n * Serialize a storage snapshot to \"Plain LSON\" format, returning a full\n * in-memory PlainLsonObject. Faster than snapshotToPlainLson_lazy for\n * small/medium documents because the result can be passed straight to\n * JSON.stringify().\n */\nexport function snapshotToPlainLson_eager(\n snapshot: IReadableSnapshot\n): PlainLsonObject {\n try {\n return buildObject(snapshot, \"root\", snapshot.get_root().data);\n } finally {\n snapshot.destroy();\n }\n}\n\nfunction buildNode(snapshot: IReadableSnapshot, id: string): PlainLson {\n const node = snapshot.get_node(id);\n if (node.type === CrdtType.OBJECT) {\n return buildObject(snapshot, id, node.data);\n } else if (node.type === CrdtType.LIST) {\n return buildList(snapshot, id);\n } else if (node.type === CrdtType.MAP) {\n return buildMap(snapshot, id);\n } else {\n // TEMPORARY: `?? null` is only here to project legacy KV rooms that\n // contain data-less registers (under a LiveMap, representing `null`\n // entries), so the KV projection matches how the SQLite migrator\n // normalizes them. Remove once all rooms have been migrated to SQLite.\n return node.data ?? null;\n }\n}\n\nfunction buildObject(\n snapshot: IReadableSnapshot,\n id: string,\n staticData: JsonObject\n): PlainLsonObject {\n // Static data values are Json, which is a subset of PlainLson\n const data: PlainLsonFields = Object.assign(\n Object.create(null),\n staticData\n ) as PlainLsonFields;\n for (const [key, childId] of snapshot.iter_children(id)) {\n data[key] = buildNode(snapshot, childId);\n }\n return { liveblocksType: \"LiveObject\", data };\n}\n\nfunction buildList(snapshot: IReadableSnapshot, id: string): PlainLsonList {\n const data: PlainLson[] = [];\n for (const [_, childId] of snapshot.iter_children(id)) {\n data.push(buildNode(snapshot, childId));\n }\n return { liveblocksType: \"LiveList\", data };\n}\n\nfunction buildMap(snapshot: IReadableSnapshot, id: string): PlainLsonMap {\n const data = Object.create(null) as PlainLsonFields;\n for (const [key, childId] of snapshot.iter_children(id)) {\n data[key] = buildNode(snapshot, childId);\n }\n return { liveblocksType: \"LiveMap\", data };\n}\n\n// ---------------------------------------------------------------------------\n// Streaming serialization: yields string chunks that concatenate to JSON.\n// ---------------------------------------------------------------------------\n\n// Generator-of-strings type alias for brevity of signatures\ntype StringGen = Generator<string, void, never>;\n\n/**\n * Serialize a storage snapshot to \"Plain LSON\" format. Yields string chunks\n * that, when concatenated, form a valid JSON string representing the storage\n * document. Slower than snapshotToPlainLson_eager but can stream documents\n * that don't fit entirely in memory.\n */\nexport function* snapshotToPlainLson_lazy(\n snapshot: IReadableSnapshot\n): StringGen {\n try {\n const staticJson = JSON.stringify(snapshot.get_root().data).slice(1, -1);\n yield* emitObject(snapshot, \"root\", staticJson);\n } finally {\n snapshot.destroy();\n }\n}\n\nfunction* emit(snapshot: IReadableSnapshot, id: string): StringGen {\n const node = snapshot.get_node(id);\n if (node.type === CrdtType.OBJECT) {\n yield* emitObject(snapshot, id, JSON.stringify(node.data).slice(1, -1));\n } else if (node.type === CrdtType.LIST) {\n yield* emitList(snapshot, id);\n } else if (node.type === CrdtType.MAP) {\n yield* emitMap(snapshot, id);\n } else if (node.type === CrdtType.REGISTER) {\n // TEMPORARY: see buildNode — remove `?? null` once all rooms are\n // migrated to SQLite.\n yield JSON.stringify(node.data ?? null);\n }\n}\n\n/**\n * @param staticJson - The object's static (non-CRDT) properties as a raw JSON\n * string without the surrounding braces, e.g. `\"foo\":1,\"bar\":\"hi\"`.\n *\n * Children are emitted _after_ the static properties. If a child key\n * collides with a static key (which shouldn't normally happen, but\n * defensively), the child wins because JSON.parse keeps the last value\n * for duplicate keys.\n */\nfunction* emitObject(\n snapshot: IReadableSnapshot,\n id: string,\n staticJson: string\n): StringGen {\n let comma = staticJson.length > 0;\n\n yield '{\"liveblocksType\":\"LiveObject\",\"data\":{';\n yield staticJson;\n\n for (const [key, childId] of snapshot.iter_children(id)) {\n if (comma) yield \",\";\n else comma = true;\n\n yield `${JSON.stringify(key)}:`;\n yield* emit(snapshot, childId);\n }\n yield \"}}\";\n}\n\nfunction* emitList(snapshot: IReadableSnapshot, id: string): StringGen {\n let comma = false;\n\n yield '{\"liveblocksType\":\"LiveList\",\"data\":[';\n for (const [_, childId] of snapshot.iter_children(id)) {\n if (comma) yield \",\";\n else comma = true;\n yield* emit(snapshot, childId);\n }\n yield \"]}\";\n}\n\nfunction* emitMap(snapshot: IReadableSnapshot, id: string): StringGen {\n let comma = false;\n\n yield '{\"liveblocksType\":\"LiveMap\",\"data\":{';\n for (const [key, childId] of snapshot.iter_children(id)) {\n if (comma) yield \",\";\n else comma = true;\n\n yield `${JSON.stringify(key)}:`;\n yield* emit(snapshot, childId);\n }\n yield \"}}\";\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type {\n NodeMap,\n NodeStream,\n SerializedChild,\n SerializedCrdt,\n SerializedRootObject,\n StorageNode,\n} from \"@liveblocks/core\";\nimport { CrdtType, isRootStorageNode, nn } from \"@liveblocks/core\";\n\nimport type { IReadableSnapshot } from \"~/interfaces\";\nimport { NestedMap } from \"~/lib/NestedMap\";\n\n/**\n * Create a basic in-memory snapshot from a set of storage nodes.\n *\n * Takes a copy of the provided nodes, so the snapshot is isolated from\n * subsequent mutations to the source.\n */\nexport function makeInMemorySnapshot(\n values: NodeMap | NodeStream\n): IReadableSnapshot {\n const map: NodeMap = new Map<string, SerializedCrdt>(values as NodeStream);\n\n if (!map.has(\"root\")) {\n map.set(\"root\", { type: CrdtType.OBJECT, data: {} });\n }\n\n // Collect child entries, sort by (parentId, parentKey), then insert into\n // the revMap so that entriesAt() returns children in parent_key order\n // without needing to re-sort on every iter_children call.\n const entries: Array<[parentId: string, parentKey: string, id: string]> = [];\n const nodeStream = map as NodeStream;\n for (const node of nodeStream) {\n if (isRootStorageNode(node)) continue;\n const [id, crdt] = node;\n entries.push([crdt.parentId, crdt.parentKey, id]);\n }\n entries.sort((a, b) =>\n a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0\n );\n\n const revMap = new NestedMap<string, string, string>();\n for (const [parentId, parentKey, id] of entries) {\n revMap.set(parentId, parentKey, id);\n }\n\n function get_node(id: string): SerializedChild {\n return nn(map.get(id), `Node not found: ${id}`) as SerializedChild;\n }\n\n return {\n get_root: () =>\n nn(\n map.get(\"root\"),\n \"Root not found\"\n ) as SerializedCrdt as SerializedRootObject,\n get_node,\n iter_children: (nodeId) => revMap.entriesAt(nodeId),\n iter_all: () => map as Iterable<StorageNode>,\n destroy() {\n map.clear();\n revMap.clear();\n },\n };\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport { raise } from \"@liveblocks/core\";\n\n/**\n * Like ES6 map, but takes a default (factory) function which will be used\n * to create entries for missing keys on the fly.\n *\n * Useful for code like:\n *\n * const map = new DefaultMap(() => []);\n * map.getOrCreate('foo').push('hello');\n * map.getOrCreate('foo').push('world');\n * map.getOrCreate('foo')\n * // ['hello', 'world']\n *\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n #defaultFn?: (key: K) => V;\n\n /**\n * If the default function is not provided to the constructor, it has to be\n * provided in each .getOrCreate() call individually.\n */\n constructor(\n defaultFn?: (key: K) => V,\n entries?: readonly (readonly [K, V])[] | null\n ) {\n super(entries);\n this.#defaultFn = defaultFn;\n }\n\n /**\n * Gets the value at the given key, or creates it.\n *\n * Difference from normal Map: if the key does not exist, it will be created\n * on the fly using the factory function, and that value will get returned\n * instead of `undefined`.\n */\n getOrCreate(key: K, defaultFn?: (key: K) => V): V {\n if (super.has(key)) {\n return super.get(key)!;\n } else {\n const fn =\n defaultFn ??\n this.#defaultFn ??\n raise(\"DefaultMap used without a factory function\");\n\n const value = fn(key);\n this.set(key, value);\n return value;\n }\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport { DefaultMap } from \"./DefaultMap\";\n\nfunction emptyIterator(): IterableIterator<never> {\n return [][Symbol.iterator]();\n}\n\n/**\n * Like an ES6 Map, but two levels deep. Useful for building reverse lookup\n * tables. Will automatically delete second-level maps when they are empty.\n */\nexport class NestedMap<K1, K2, V> {\n #map: DefaultMap<K1, Map<K2, V>>;\n\n constructor() {\n this.#map = new DefaultMap(() => new Map<K2, V>());\n }\n\n get size(): number {\n let total = 0;\n for (const value of this.#map.values()) {\n total += value.size;\n }\n return total;\n }\n\n count(key1: K1): number {\n return this.#map.get(key1)?.size ?? 0;\n }\n\n *keys(): IterableIterator<[K1, K2]> {\n for (const [key1, nested] of this.#map) {\n for (const key2 of nested.keys()) {\n yield [key1, key2];\n }\n }\n }\n\n has(key1: K1, key2: K2): boolean {\n return this.#map.get(key1)?.has(key2) ?? false;\n }\n\n get(key1: K1, key2: K2): V | undefined {\n return this.#map.get(key1)?.get(key2);\n }\n\n set(key1: K1, key2: K2, value: V): this {\n this.#map.getOrCreate(key1).set(key2, value);\n return this;\n }\n\n delete(key1: K1, key2: K2): void {\n if (!this.#map.has(key1)) {\n return;\n }\n\n const nested = this.#map.get(key1)!;\n nested.delete(key2);\n if (nested.size === 0) {\n this.#map.delete(key1);\n }\n }\n\n clear(): void {\n this.#map.clear();\n }\n\n *[Symbol.iterator](): IterableIterator<[K1, K2, V]> {\n for (const [key1, nested] of this.#map) {\n for (const [key2, value] of nested) {\n yield [key1, key2, value];\n }\n }\n }\n\n entriesAt(key1: K1): IterableIterator<[K2, V]> {\n return this.#map.get(key1)?.entries() ?? emptyIterator();\n }\n\n *filterAt(key1: K1, keys: Iterable<K2>): Iterable<[K2, V]> {\n const nested = this.#map.get(key1);\n if (nested === undefined) {\n return;\n }\n\n for (const k2 of keys) {\n const value = nested.get(k2);\n if (value !== undefined) {\n yield [k2, value];\n }\n }\n }\n\n keysAt(key1: K1): IterableIterator<K2> {\n return this.#map.get(key1)?.keys() ?? emptyIterator();\n }\n\n valuesAt(key1: K1): IterableIterator<V> {\n return this.#map.get(key1)?.values() ?? emptyIterator();\n }\n\n deleteAll(key1: K1): void {\n this.#map.delete(key1);\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { Json } from \"@liveblocks/core\";\nimport type { Decoder } from \"decoders\";\n\nimport type { IStorageDriver } from \"~/interfaces\";\n\nexport interface MetadataDB {\n // Getter supports optional decoder\n get(key: string): Json | undefined;\n get<T>(decoder: Decoder<T>, key: string): T | undefined;\n\n put(key: string, value: Json): void;\n delete(key: string): void;\n}\n\n/**\n * Returns a thin wrapper around an IStorageDriver to provide MetadataDB\n * functionality, including type-safe reads.\n */\nexport function makeMetadataDB(driver: IStorageDriver): MetadataDB {\n function get(key: string): Json | undefined;\n function get<T>(decoder: Decoder<T>, key: string): T | undefined;\n function get<T>(a1: string | Decoder<T>, a2?: string): T | Json | undefined {\n if (a2 === undefined) {\n return driver.get_meta(a1 as string);\n } else {\n return (a1 as Decoder<T>).value(driver.get_meta(a2));\n }\n }\n\n return {\n get,\n put: driver.put_meta.bind(driver),\n delete: driver.delete_meta.bind(driver),\n };\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type {\n BaseUserMeta,\n Brand,\n CompactNode,\n IUserInfo,\n Json,\n JsonObject,\n} from \"@liveblocks/core\";\nimport {\n assertNever,\n ClientMsgCode,\n OpCode,\n raise,\n ServerMsgCode as CoreServerMsgCode,\n tryParseJson,\n WebsocketCloseCodes as CloseCode,\n} from \"@liveblocks/core\";\nimport { Mutex, tryAcquire } from \"async-mutex\";\nimport { array, formatInline } from \"decoders\";\nimport { chunkedByCost } from \"itertools\";\nimport { nanoid } from \"nanoid\";\n\nimport type { Guid } from \"~/decoders\";\nimport { clientMsgDecoder } from \"~/decoders\";\nimport type {\n IServerWebSocket,\n IStorageDriver,\n ListFeedMessagesOptions,\n ListFeedMessagesResult,\n ListFeedsOptions,\n ListFeedsResult,\n} from \"~/interfaces\";\nimport { Logger } from \"~/lib/Logger\";\nimport { makeNewInMemoryDriver } from \"~/plugins/InMemoryDriver\";\nimport type {\n AddFeedClientMsg,\n AddFeedMessageClientMsg,\n ClientMsg as GenericClientMsg,\n DeleteFeedClientMsg,\n DeleteFeedMessageClientMsg,\n FeedMessagesServerMsg,\n FeedsServerMsg,\n FetchFeedMessagesClientMsg,\n FetchFeedsClientMsg,\n IgnoredOp,\n Op,\n ServerMsg as GenericServerMsg,\n ServerWireOp,\n UpdateFeedClientMsg,\n UpdateFeedMessageClientMsg,\n} from \"~/protocol\";\nimport { Storage } from \"~/Storage\";\nimport { YjsStorage } from \"~/YjsStorage\";\n\nimport { tryCatch } from \"./lib/tryCatch\";\nimport { UniqueMap } from \"./lib/UniqueMap\";\nimport { feedFailureServerMsg, feedRequestFailed } from \"./protocol/feedErrors\";\nimport { FeedMsgCode, FeedRequestErrorCode } from \"./protocol/feedMessages\";\nimport { ProtocolVersion } from \"./protocol/ProtocolVersion\";\nimport type { Feed, FeedMessage, jstring, LeasedSession } from \"./types\";\nimport { isLeasedSessionExpired, makeRoomStateMsg } from \"./utils\";\n\nconst MB = 1024 * 1024;\n\nconst messagesDecoder = array(clientMsgDecoder);\n\n// Temporary patch\nconst ServerMsgCode = { ...CoreServerMsgCode, ...FeedMsgCode };\nconst HIGHEST_PROTOCOL_VERSION = Math.max(\n ...Object.values(ProtocolVersion).filter(\n (v): v is number => typeof v === \"number\"\n )\n) as ProtocolVersion;\n\n// Reverse lookup for ServerMsgCodes\nconst SERVER_MSG_CODE_NAMES = Object.fromEntries(\n Object.entries(ServerMsgCode).map(([k, v]) => [v, k])\n) as Record<(typeof ServerMsgCode)[keyof typeof ServerMsgCode], string>;\n\nconst BLACK_HOLE = new Logger([\n /* No targets, i.e. black hole logger */\n]);\n\n// Stream pre-built CompactNode tuple strings into raw STORAGE_CHUNK frames\n// (no parse/re-stringify of jdata). Each node is its own atomic unit for\n// chunkedByCost, so a single oversized row (≤ 2 MB CF limit) is the worst\n// case any output frame can exceed MAX_SIZE by.\nfunction groupNodesForWebSocketMessages(\n input: Iterable<jstring<CompactNode>>\n): Iterable<jstring<CompactNode>[]> {\n // Cap each outgoing WebSocket message at 16 MB. CF's 32 MB message size\n // limit only applies inbound, so the real constraint here is memory\n // pressure: the worker has 128 MB total, and stringifying a 16 MB message\n // costs roughly that much again in transient allocations.\n const MAX_SIZE = 16 * MB;\n\n // Chunk to form ideal message sizes, per chunk:\n // chunks 0..2 → 1 MB each (fast start)\n // chunks 3..9 → 2, 3, 4, 5, 6, 7, 8 MB (slowly ramp up)\n // chunks 10+ → 8 MB (steady state)\n const idealSize = (chunkIndex: number): number =>\n chunkIndex < 3 ? 1 * MB : Math.min(8, chunkIndex - 1) * MB;\n\n return chunkedByCost(input, (tup) => tup.length, MAX_SIZE, idealSize);\n}\n\nexport type ActorID = Brand<number, \"ActorID\">;\n/** Number of milliseconds since Unix epoch. */\nexport type Millis = Brand<number, \"Millis\">;\n\n/**\n * Session keys are also known as the \"nonce\" in the protocol. It's a random,\n * unique, but PRIVATE, identifier for the session, and it's important that\n * this ID is never shared to anyone except the connected client, which\n * receives it as part of its ROOM_STATE message.\n */\nexport type SessionKey = Brand<string, \"SessionKey\">;\n\ntype ClientMsg =\n | GenericClientMsg<JsonObject, Json>\n | FetchFeedsClientMsg\n | FetchFeedMessagesClientMsg\n | AddFeedClientMsg\n | UpdateFeedClientMsg\n | DeleteFeedClientMsg\n | AddFeedMessageClientMsg\n | UpdateFeedMessageClientMsg\n | DeleteFeedMessageClientMsg;\ntype ServerMsg =\n | GenericServerMsg<JsonObject, BaseUserMeta, Json>\n | FeedMessagesServerMsg\n | FeedsServerMsg; // Temp patched server msgs\n\n/**\n * Creates a collector for deferred promises (side effects that should run\n * outside a mutex). Call `defer` to collect promises, then `waitAll` to\n * await them all.\n */\nfunction collectSideEffects() {\n const deferred: Promise<void>[] = [];\n return {\n defer: (p: Promise<void>) => void deferred.push(p),\n waitAll: () => Promise.allSettled(deferred),\n };\n}\n\nfunction serialize(msgs: ServerMsg | readonly ServerMsg[]): jstring<ServerMsg> {\n return JSON.stringify(msgs) as jstring<ServerMsg>;\n}\n\nexport function ackIgnoredOp(opId: string): IgnoredOp {\n return { type: OpCode.DELETE_CRDT, id: \"ACK\", opId }; // (H)Ack Op\n}\n\nfunction stripOpId(op: Op): ServerWireOp {\n // TODO: Optimize later! Instead of duplicating every op and\n // stripping the opId explicitly, it would be generally more\n // efficient if we treated the opIds as \"envelopes\" around Ops (or\n // send them in a separate array altogether at the protocol level\n // in V8 soon--even better, as it would not even require any stripping!)\n const { opId: _, ...rest } = op; // Strip opIds from all outgoing messages!\n return rest;\n}\n\n/**\n * A known or anonymous user.\n *\n * BY DEFINITION:\n * A User with an assigned `id` property is a non-anonymous user.\n * A User with an assigned `anonymousId` property is an anonymous user.\n * A User with neither of those properties is also an anonymous user.\n *\n * WHAT'S THE DIFFERENCE?\n * When creating a non-anonymous user, other users in the room will be able to\n * observe the assigned `id` property in Presence (e.g. via the `other.user.id`\n * in the Liveblocks client).\n *\n * When creating an anonymous user, you can _optionally_ provide an anonymous\n * ID to (re)use. While not authorized, this still allows you to correlate\n * unique users.\n */\nexport type IUserData = AuthorizedUser | AnonymousUser; // YYY Remove this export before launch. It's a private API, but only needed temporarily, while refactoring our CF server\n\ntype AuthorizedUser = {\n readonly id: string;\n readonly anonymousId?: never;\n readonly info?: IUserInfo;\n};\n\n// Anonymous users, by definition, have no ID, or have an explicitly-assigned\n// anonymous ID (in case you need to control anonymous ID generation, e.g. by\n// tracking a cookie). The anonymous ID will not show up in other clients. To\n// those clients, it will appear as a user without an ID.\ntype AnonymousUser = {\n readonly anonymousId: string;\n readonly id?: never;\n readonly info?: IUserInfo;\n};\n\n/*\n\nSession Types: \n| | Browser Session | Backend Session | Leased Session |\n|-----------------------------------|-----------------|-----------------|-----------------|\n| Sends enter/leave/presence events | ✓ | | ✓ |\n| Visible to other users in room | ✓ | | ✓ |\n| Has WebSocket connection | ✓ | | |\n| Updated from | Browser | REST API | REST API |\n\n*/\n\n/**\n * Each BrowserSession is an abstraction around a socket instance, and maintains\n * metadata about the connection.\n */\nexport class BrowserSession<SM, CM extends JsonObject> {\n // ^^ User-defined Session Metadata\n // ^^ User-defined Client Metadata (sent to client in ROOM_STATE)\n\n public readonly version: ProtocolVersion; // Liveblocks protocol version this client will speak\n public readonly actor: ActorID; // Must be unique within the room\n public readonly createdAt: Millis;\n\n // Externally provided (public!) user metadata. This information will get shared with other clients\n public readonly user: IUserData;\n public readonly scopes: string[]; // Permissions for this session, sent to connected clients (so consider public info)\n public readonly meta: SM; // Arbitrary *private* meta data to attach to this session (will NOT be shared)\n public readonly publicMeta?: CM; // Metadata sent to client in ROOM_STATE message's \"meta\" field\n\n readonly #_socket: IServerWebSocket;\n readonly #_debug: boolean;\n /** Updated on every incoming message (including pings) via handleData(). Used for idle timeout detection. */\n #_lastActiveAt: Millis;\n\n // We keep a status in-memory in the session of whether we already sent a rejected ops message to the client.\n #_hasNotifiedClientStorageUpdateError: boolean;\n\n /** @internal - Never create a BrowserSession instance manually. Use the room.startBrowserSession() API instead. */\n constructor(\n ticket: Ticket<SM, CM>,\n socket: IServerWebSocket,\n debug: boolean,\n /** When restoring from hibernation, pass the original socket creation time so session duration is correct. */\n createdAt?: Date\n ) {\n this.version = ticket.version;\n this.actor = ticket.actor;\n this.user = ticket.user;\n this.scopes = ticket.scopes;\n this.meta = ticket.meta ?? (undefined as unknown as SM);\n this.publicMeta = ticket.publicMeta;\n this.#_socket = socket;\n this.#_debug = debug;\n\n const now = (createdAt?.getTime() ?? Date.now()) as Millis;\n this.createdAt = now;\n this.#_lastActiveAt = now;\n this.#_hasNotifiedClientStorageUpdateError = false;\n }\n\n get lastActiveAt(): Millis {\n const lastPing = this.#_socket.getLastPongTimestamp?.();\n if (lastPing && lastPing.getTime() > this.#_lastActiveAt) {\n return lastPing.getTime() as Millis;\n } else {\n return this.#_lastActiveAt;\n }\n }\n\n get hasNotifiedClientStorageUpdateError(): boolean {\n return this.#_hasNotifiedClientStorageUpdateError;\n }\n\n /** @internal - This should have to be called only from Room, never externally */\n markActive(now?: Date): void {\n this.#_lastActiveAt = (now ? now.getTime() : Date.now()) as Millis;\n }\n\n setHasNotifiedClientStorageUpdateError(): void {\n this.#_hasNotifiedClientStorageUpdateError = true;\n }\n\n sendPong(): number {\n this.markActive();\n\n const sent = this.#_socket.send(\"pong\");\n if (this.#_debug) {\n if (sent < 0) {\n console.error(\n `failed to send \"pong\" to actor=${this.actor} (back pressure)`\n );\n } else if (sent === 0) {\n console.error(\n `failed to send \"pong\" to actor=${this.actor} (connection issue)`\n );\n } else {\n // Success\n console.log(`sent to actor=${this.actor}: \"pong\"`);\n }\n }\n return sent;\n }\n\n send(serverMsg: ServerMsg | ServerMsg[] | jstring<ServerMsg>): number {\n const data =\n typeof serverMsg === \"string\" ? serverMsg : serialize(serverMsg);\n const sent = this.#_socket.send(data);\n if (this.#_debug) {\n if (sent < 0) {\n console.error(\n `failed to send message to actor=${this.actor} (back pressure)`\n );\n } else if (sent === 0) {\n console.error(\n `failed to send message to actor=${this.actor} (connection issue)`\n );\n }\n\n const msgs = JSON.parse(data) as ServerMsg | ServerMsg[];\n for (const msg of Array.isArray(msgs) ? msgs : [msgs]) {\n console.log(\n `sent to actor=${this.actor}: [${\n SERVER_MSG_CODE_NAMES[msg.type] ?? msg.type\n }] ${JSON.stringify(msg)}`\n );\n }\n }\n return sent;\n }\n\n /**\n * @internal\n * Closes the socket associated to this BrowserSession.\n *\n * NOTE: Never call this API directly! Call .endBrowserSession() instead.\n */\n closeSocket(code: number, reason?: string): void {\n this.#_socket.close(code, reason);\n }\n}\n\nexport class BackendSession extends BrowserSession<never, never> {\n /** @internal Never call this constructor directly */\n constructor(\n ticket: Ticket<never, never>,\n socket: IServerWebSocket,\n debug: boolean\n ) {\n super(ticket, socket, debug);\n }\n}\n\nexport type Ticket<SM, CM extends JsonObject> = {\n readonly sessionKey: SessionKey; // Should stay private\n readonly version: ProtocolVersion;\n readonly actor: ActorID;\n readonly meta?: SM; // Private Session metadata\n readonly publicMeta?: CM; // Client metadata is *public* metadata sent to client in ROOM_STATE message\n readonly user: IUserData; // User-provided, public, metadata\n readonly scopes: string[];\n};\n\nexport type CreateTicketOptions<SM, CM extends JsonObject> = {\n /** The Liveblocks protocol version this client will speak */\n version?: ProtocolVersion;\n meta?: SM;\n publicMeta?: CM;\n /** A user-provided ID to externally recognize the user by */\n id?: string;\n /**\n * A user-provided anonymous ID to use. When `id` is provided, this field is\n * ignored. When both fields are missing, a new anonymous ID will be\n * generated.\n */\n anonymousId?: string;\n /** Static user metadata to assign this session, will get broadcasted to other clients */\n info?: IUserInfo;\n /** Permissions to assign this session */\n scopes?: string[];\n\n /** An explicit actor ID to use. Supported for legacy use cases only. It's best to not set this and let it get assigned dynamically, as it's important for this identifier to be unique. */\n actor?: ActorID;\n};\n\ntype RoomOptions<SM, CM extends JsonObject, C> = {\n /**\n * Bring your own persistence backend\n */\n storage?: IStorageDriver;\n logger?: Logger;\n\n // YYY Restructure these hooks to all take a single `event` param\n hooks?: {\n /** Customize which incoming messages from a client are allowed or disallowed. */\n isClientMsgAllowed?: (\n msg: ClientMsg,\n session: BrowserSession<SM, CM>\n ) => { allowed: true } | { allowed: false; reason: string };\n\n /** Called whenever the server acknowledged a ping with a pong */\n onDidPong?: (ctx?: C) => void | Promise<void>;\n\n /** Called right before the room is attempted to be unloaded. Synchronous. May throw to abort the unloading. */\n onRoomWillUnload?: (ctx?: C) => void;\n\n /** Called when a new user entered the room. */\n onSessionDidStart?: (\n session: BrowserSession<SM, CM>,\n ctx?: C\n ) => void | Promise<void>;\n /** Called when a user left the room. */\n onSessionDidEnd?: (\n session: BrowserSession<SM, CM>,\n ctx?: C\n ) => void | Promise<void>;\n\n /**\n * Called when Liveblocks Storage for the room was updated.\n *\n * IMPORTANT! If you implement these as async functions, it's important to\n * note that these run outside of the storage mutex that guarantees\n * a consistent view of storage.\n * Therefore, only ever use this hook to implement a side effect (like\n * trigger a notification), don't read storage in this hook directly.\n */\n postClientMsgStorageDidUpdate?: (ctx?: C) => void | Promise<void>;\n /**\n * Called when Yjs Storage for the room was updated.\n *\n * IMPORTANT! If you implement these as async functions, it's important to\n * note that these run outside of the storage mutex that guarantees\n * a consistent view of storage.\n * Therefore, only ever use this hook to implement a side effect (like\n * trigger a notification), don't read storage in this hook directly.\n */\n postClientMsgYdocDidUpdate?: (\n ctx?: C,\n sess?: BrowserSession<SM, CM>\n ) => void | Promise<void>;\n };\n\n /** Enable debug logging */\n enableDebugLogging?: boolean;\n};\n\n/**\n * A Liveblocks Room server.\n */\nexport class Room<RM, SM, CM extends JsonObject, C = undefined> {\n // ^^^^^^^^^^ User-defined Room Metadata, Session Metadata, and Client Metadata\n\n public meta: RM;\n public readonly driver: IStorageDriver;\n #logger: Logger;\n\n /**\n * While a room is in \"maintenance mode\", all WebSocket connections to the\n * room should be rejected until it's pulled out of maintenance mode again.\n * Maintenance mode should only last a couple of milliseconds, typically.\n *\n * This mutex ensures that concurrent destructive operations (like\n * init-storage, storage-reset, etc.) cannot interfere with one another.\n */\n private readonly _maintenanceMode = new Mutex();\n\n private _storage: Storage;\n private _yjsStorage: YjsStorage;\n public readonly mutex: Mutex = new Mutex();\n\n public get storage(): Storage { return this._storage; } // prettier-ignore\n public get yjsStorage(): YjsStorage { return this._yjsStorage; } // prettier-ignore\n\n private _qsize = 0;\n\n private readonly sessions = new UniqueMap<\n SessionKey,\n BrowserSession<SM, CM>,\n ActorID\n >((s) => s.actor);\n\n private readonly hooks: {\n isClientMsgAllowed: (\n msg: ClientMsg,\n session: BrowserSession<SM, CM>\n ) => { allowed: true } | { allowed: false; reason: string };\n\n onDidPong?: (ctx?: C) => void | Promise<void>;\n\n onRoomWillUnload?: (ctx?: C) => void;\n\n onSessionDidStart?: (\n session: BrowserSession<SM, CM>,\n ctx: C | undefined\n ) => void | Promise<void>;\n onSessionDidEnd?: (\n session: BrowserSession<SM, CM>,\n ctx: C | undefined\n ) => void | Promise<void>;\n\n // Don't like these callback names yet. Think about how to better abstract it later.\n postClientMsgStorageDidUpdate?: (ctx?: C) => void | Promise<void>;\n postClientMsgYdocDidUpdate?: (\n ctx?: C,\n sess?: BrowserSession<SM, CM>\n ) => void | Promise<void>;\n };\n\n readonly #_debug: boolean;\n\n constructor(meta: RM, options?: RoomOptions<SM, CM, C>) {\n const driver = options?.storage ?? makeNewInMemoryDriver();\n this.meta = meta;\n this.driver = driver;\n this._storage = new Storage(this.driver);\n this._yjsStorage = new YjsStorage(this.driver);\n\n this.#logger = options?.logger ?? BLACK_HOLE;\n this.hooks = {\n isClientMsgAllowed:\n options?.hooks?.isClientMsgAllowed ??\n (() => {\n return {\n allowed: true,\n };\n }),\n\n onRoomWillUnload: options?.hooks?.onRoomWillUnload,\n\n onSessionDidStart: options?.hooks?.onSessionDidStart,\n onSessionDidEnd: options?.hooks?.onSessionDidEnd,\n\n postClientMsgStorageDidUpdate:\n options?.hooks?.postClientMsgStorageDidUpdate,\n postClientMsgYdocDidUpdate: options?.hooks?.postClientMsgYdocDidUpdate,\n };\n this.#_debug = options?.enableDebugLogging ?? false;\n }\n\n public get logger(): Logger {\n return this.#logger;\n }\n\n public addLoggerContext(attrs: JsonObject): void {\n this.#logger = this.#logger.withContext(attrs);\n }\n\n public get numSessions(): number { return this.sessions.size; } // prettier-ignore\n\n // ------------------------------------------------------------------------------------\n // Maintenance mode\n // ------------------------------------------------------------------------------------\n\n /**\n * Returns true if the room is currently in maintenance mode.\n * When in maintenance mode, callers should refuse new WebSocket connections.\n */\n public get isInMaintenance(): boolean {\n return this._maintenanceMode.isLocked();\n }\n\n /**\n * Tries to enter maintenance mode and run the given callback exclusively.\n * If the room is already in maintenance mode, throws E_ALREADY_LOCKED\n * immediately instead of queuing the request.\n */\n public async runInMaintenanceMode<T>(\n callback: () => T | Promise<T>\n ): Promise<T> {\n return tryAcquire(this._maintenanceMode).runExclusive(callback);\n }\n\n // ------------------------------------------------------------------------------------\n // Public API\n // ------------------------------------------------------------------------------------\n\n /**\n * Releases the currently-loaded storage tree and Yjs documents from worker\n * memory, freeing them up to be garbage collected. The next time the room\n * is used, fresh instances will lazily be created.\n */\n public unload(ctx?: C): void {\n this.hooks.onRoomWillUnload?.(ctx); // May throw to cancel unloading\n\n // NOTE: this.mutex is deliberately left untouched here. It guards\n // in-flight message processing for the lifetime of this Room instance,\n // and force-releasing a held lock would allow concurrent writers.\n this.driver.reinitialize();\n this._storage = new Storage(this.driver);\n this._yjsStorage = new YjsStorage(this.driver);\n }\n\n /**\n * Issues a Ticket with a new/unique actor ID\n *\n * IMPORTANT! As the caller of this function, you are responsible for\n * ensuring you trust the values passed in here. Never pass unauthorized\n * values in here.\n *\n * The returned Ticket can be turned into a active Session once the socket\n * connection is established. If the socket is never established, this\n * unused Ticket will simply get garbage collected.\n */\n public createTicket(options?: CreateTicketOptions<SM, CM>): Ticket<SM, CM> {\n const actor = options?.actor ?? this.getNextActor();\n const sessionKey = nanoid() as SessionKey;\n const info = options?.info;\n const ticket: Ticket<SM, CM> = {\n version: options?.version ?? HIGHEST_PROTOCOL_VERSION,\n actor,\n sessionKey,\n meta: options?.meta,\n publicMeta: options?.publicMeta,\n user: options?.id\n ? { id: options.id, info }\n : { anonymousId: options?.anonymousId ?? nanoid(), info },\n scopes: options?.scopes ?? [\"room:write\"],\n };\n if (this.#_debug) {\n console.log(`new ticket created: ${JSON.stringify(ticket)}`);\n }\n return ticket;\n }\n\n public createBackendSession_experimental(): [\n session: BackendSession,\n outgoingMessages: jstring<ServerMsg>[],\n ] {\n const ticket = this.createTicket() as Ticket<never, never>;\n const capturedServerMsgs: jstring<ServerMsg>[] = [];\n const stub = {\n send: (data) => {\n if (typeof data === \"string\") {\n capturedServerMsgs.push(data as jstring<ServerMsg>);\n }\n return 0;\n },\n close: () => {}, // noop\n } satisfies IServerWebSocket;\n const session = new BackendSession(ticket, stub, false);\n return [session, capturedServerMsgs];\n }\n\n /**\n * Restores the given sessions as the Room server's session list. Can only be\n * called as long as there are no existing sessions.\n *\n * The key difference with the .startBrowserSession() API is that restoreSessions is\n * used in cases where a session was hibernated and needs to be restored,\n * without _conceptually_ starting a new session.\n *\n * Because there are no side effects to restoreSession, it's synchronous.\n */\n public restoreSessions(\n sessions: {\n ticket: Ticket<SM, CM>;\n socket: IServerWebSocket;\n lastActivity: Date;\n /** Original session creation time (e.g. from persisted attachment). Required for correct session duration after hibernation. */\n createdAt?: Date;\n }[]\n ): void {\n if (this.sessions.size > 0) {\n throw new Error(\"This API can only be called before any sessions exist\");\n }\n\n for (const { ticket, socket, lastActivity, createdAt } of sessions) {\n const newSession = new BrowserSession(\n ticket,\n socket,\n this.#_debug,\n createdAt\n );\n this.sessions.set(ticket.sessionKey, newSession);\n newSession.markActive(lastActivity);\n }\n }\n\n private sendSessionStartMessages(\n newSession: BrowserSession<SM, CM>,\n ticket: Ticket<SM, CM>,\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to sendSessionStartMessages() to collect async side effects.\"\n );\n }\n ): void {\n const users: Record<ActorID, BaseUserMeta & { scopes: string[] }> = {};\n // Add regular sessions\n for (const session of this.otherSessions(ticket.sessionKey)) {\n users[session.actor] = {\n id: session.user.id,\n info: session.user.info,\n scopes: session.scopes,\n };\n }\n\n // List all active server sessions\n const leasedSessions: LeasedSession[] = this.listLeasedSessions(ctx, defer);\n // Add server sessions\n for (const leasedSession of leasedSessions) {\n users[leasedSession.actorId as ActorID] = {\n id: leasedSession.sessionId,\n info: leasedSession.info,\n scopes: [],\n };\n }\n\n // this must happen before presence messages are sent\n newSession.send(\n makeRoomStateMsg(\n newSession.actor,\n ticket.sessionKey, // called \"nonce\" in the protocol\n newSession.scopes,\n users,\n ticket.publicMeta\n )\n );\n\n // Send each server session's full presence to the new user\n // NOTE: this doesn't exist for other browser sessions because those other sessions send from their frontend clients in response to room state messages.\n for (const leasedSession of leasedSessions) {\n newSession.send({\n type: ServerMsgCode.UPDATE_PRESENCE,\n actor: leasedSession.actorId as ActorID,\n targetActor: newSession.actor, // full presence to new user\n data: leasedSession.presence as JsonObject,\n });\n }\n }\n\n /**\n * Registers a new BrowserSession into the Room server's session list, along with\n * the socket connection to use for that BrowserSession, now that it is known.\n *\n * This kicks off a few side effects:\n * - Sends a ROOM_STATE message to the socket.\n * - Broadcasts a USER_JOINED message to all other sessions in the room.\n */\n public startBrowserSession(\n ticket: Ticket<SM, CM>,\n socket: IServerWebSocket,\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to startBrowserSession() to collect async side effects.\"\n );\n }\n ): void {\n let existing: SessionKey | undefined;\n while (\n (existing = this.sessions.lookupPrimaryKey(ticket.actor)) !== undefined\n ) {\n // If this happens, it means a new connection attempt is happening for an\n // existing actor ID. It's most likely from a reconnection attempt using\n // a legacy token (which has the actor ID hardcoded in it), where the old\n // session hasn't been closed explicitly. We'll actively kill it now.\n\n // Terminate old session\n this.endBrowserSession(\n existing,\n CloseCode.KICKED,\n \"Closed stale connection\",\n ctx,\n defer\n );\n\n this.#logger.warn(\n `Previous session for actor ${ticket.actor} killed in favor of new session`\n );\n }\n\n const newSession = new BrowserSession(ticket, socket, this.#_debug);\n this.sessions.set(ticket.sessionKey, newSession);\n\n // send sessions start messages\n this.sendSessionStartMessages(newSession, ticket, ctx, defer);\n\n this.sendToOthers(\n ticket.sessionKey,\n {\n type: ServerMsgCode.USER_JOINED,\n actor: newSession.actor,\n id: newSession.user.id,\n info: newSession.user.info,\n scopes: newSession.scopes,\n },\n ctx,\n defer\n );\n\n // Call the hook, but don't await the results here\n const p$ = this.hooks.onSessionDidStart?.(newSession, ctx);\n if (p$) defer(p$);\n }\n\n /**\n * Unregisters the BrowserSession for the given actor. Call this when the socket has\n * been closed from the client's end.\n *\n * This kicks off a few side effects:\n * - Broadcasts a USER_LEFT message to all other sessions in the room.\n */\n public endBrowserSession(\n key: SessionKey,\n code: number,\n reason: string,\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"Your onSessionDidEnd handler returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to endBrowserSession() to collect async side effects.\"\n );\n }\n ): void {\n const sessions = this.sessions;\n\n const session = sessions.get(key);\n if (session === undefined) return;\n\n session.closeSocket(code, reason);\n\n const deleted = sessions.delete(key);\n if (deleted) {\n for (const other of this.otherSessions(key)) {\n other.send({ type: ServerMsgCode.USER_LEFT, actor: session.actor });\n }\n\n // Call the hook\n const p$ = this.hooks.onSessionDidEnd?.(session, ctx);\n if (p$) defer(p$);\n }\n }\n\n /**\n * Force-closes all sessions matching the given predicate.\n */\n public endSessionBy(\n predicate: (session: BrowserSession<SM, CM>) => boolean,\n code: number,\n reason: string,\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"Your onSessionDidEnd handler returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to endSessionBy() to collect async side effects.\"\n );\n }\n ): number {\n let count = 0;\n for (const [key, session] of this.sessions) {\n if (predicate(session)) {\n count++;\n this.endBrowserSession(key, code, reason, ctx, defer);\n }\n }\n return count;\n }\n\n /**\n * Handles a raw incoming socket message, which can be a ping, or an\n * JSON-encoded message batch.\n */\n public async handleData(\n key: SessionKey,\n data: unknown,\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to handleData() to collect async side effects.\"\n );\n }\n ): Promise<void> {\n const text =\n typeof data === \"string\" ? data : raise(\"Unsupported message format\");\n\n this.sessions.get(key)?.markActive();\n if (text === \"ping\") {\n await this.handlePing(key, ctx);\n } else {\n const json = tryParseJson(text);\n const messages = messagesDecoder.decode(json);\n\n if (!messages.ok) {\n const reason =\n process.env.NODE_ENV !== \"production\"\n ? formatInline(messages.error)\n : \"Invalid message format\";\n\n this.endBrowserSession(\n key,\n CloseCode.INVALID_MESSAGE_FORMAT,\n reason,\n ctx,\n defer\n );\n return;\n }\n\n // TODO: Decide on these limits later.\n // If qsize is > 0, then it means there is a traffic jam. This shouldn't\n // be a problem for a while, but it grows beyond a certain (soft or hard)\n // limit, we may want to take measures.\n if (this._qsize > 10_000) {\n // Over hard limit\n // TODO: Maybe disconnect this sockets with a 42xx close code? This\n // will make the client back off more aggressively. See\n // https://github.com/liveblocks/liveblocks/blob/223f7ce0d77380fecd3b08ed9454ca8c330bbe16/packages/liveblocks-core/src/types/IWebSocket.ts#L53\n } else if (this._qsize > 5_000) {\n // Over soft limit\n // TODO: Maybe instruct clients to increase their throttle values?\n }\n\n this._qsize++;\n\n // Run this.handleMsgs(), but guarded by a mutex lock, ensuring that no\n // two messages will get processed simultaneously. This provides similar\n // concurrency protection as Cloudflare's I/O gates\n try {\n await this.processClientMsg(key, messages.value, ctx);\n } finally {\n this._qsize--;\n }\n }\n }\n\n /**\n * Processes an incoming batch of 1 or more ClientMsgs on behalf of\n * a (regular user/browser) session.\n *\n * IMPORTANT: Only use this API on \"trusted\" data!\n * To handle untrusted input data, use `.handleData()` instead.\n *\n * Before calling this API, make sure:\n * 1. The call site is entitled to call this message on behalf of this session; and\n * 2. The ClientMsg payload has been validated to be correct.\n */\n public async processClientMsg(\n key: SessionKey,\n messages: ClientMsg[],\n ctx?: C\n ): Promise<void> {\n const { defer, waitAll } = collectSideEffects();\n await this.mutex.runExclusive(() =>\n this._processClientMsg_withExclusiveAccess(key, messages, ctx, defer)\n );\n\n // Run all deferred work (like queueing messages, sending notifications,\n // etc) outside of the mutex\n await waitAll();\n }\n\n /**\n * Processes an incoming batch of 1 or more ClientMsgs on behalf of\n * a BACKEND session.\n *\n * Difference 1: HTTP RESPONSE instead of WEB SOCKET RESPONSE\n * ----------------------------------------------------------\n * For \"normal\" sessions that have a socket attached, any \"responses\" (i.e.\n * server messages like acks or fixops) will be sent back through that\n * existing socket connection.\n *\n * The key difference when using this method is that there is no such socket,\n * so any \"response\" ServerMsgs will get sent back as an HTTP response.\n *\n * Difference 2: No auth check\n * ---------------------------\n * Another key difference is that when processing a backend session, no\n * \"isClientMsgAllowed()\" check is performed, because those checks assume\n * a session.\n */\n public async processClientMsgFromBackendSession(\n session: BackendSession,\n messages: ClientMsg[],\n ctx?: C\n ): Promise<void> {\n const { defer, waitAll } = collectSideEffects();\n await this.mutex.runExclusive(() =>\n this._processClientMsgFromBackendSession_withExclusiveAccess(\n session,\n messages,\n ctx,\n defer\n )\n );\n\n // Run all deferred work (like queueing messages, sending notifications,\n // etc) outside of the mutex\n await waitAll();\n }\n\n public getSession(\n sessionKey: SessionKey\n ): BrowserSession<SM, CM> | undefined {\n return this.sessions.get(sessionKey);\n }\n\n public listSessions(): BrowserSession<SM, CM>[] {\n return Array.from(this.sessions.values());\n }\n\n /**\n * Upsert a leased session. Creates a new session if it doesn't exist (or is expired),\n * or updates an existing session with merged presence.\n */\n public upsertLeasedSession(\n sessionId: string,\n presence: JsonObject,\n ttl: number,\n info: IUserInfo,\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to upsertLeasedSession() to collect async side effects.\"\n );\n }\n ): void {\n const existingSession = this.driver.get_leased_session(sessionId);\n const isExpired =\n existingSession !== undefined && isLeasedSessionExpired(existingSession);\n\n if (isExpired) {\n this.deleteLeasedSession(existingSession, ctx, defer);\n }\n\n if (existingSession === undefined || isExpired) {\n // Creating new session (or was expired)\n const actorId = this.getNextActor();\n const now = Date.now();\n const session: LeasedSession = {\n sessionId,\n presence,\n updatedAt: now,\n info,\n ttl,\n actorId,\n };\n\n this.driver.put_leased_session(session);\n\n // Broadcast USER_JOINED to all existing sessions\n this.sendToAll(\n {\n type: ServerMsgCode.USER_JOINED,\n actor: actorId,\n id: sessionId,\n info,\n scopes: [],\n },\n ctx,\n defer\n );\n // now send the presence to all sessions\n this.sendToAll(\n {\n type: ServerMsgCode.UPDATE_PRESENCE,\n actor: actorId,\n data: presence,\n targetActor: 1,\n },\n ctx,\n defer\n );\n } else {\n // Updating existing session (and not expired)\n // Merge/patch the presence\n const mergedPresence = {\n ...(existingSession.presence as JsonObject),\n ...presence,\n };\n const updatedSession: LeasedSession = {\n ...existingSession,\n //info, UserInfo is immutable after creation\n presence: mergedPresence,\n updatedAt: Date.now(),\n ttl,\n };\n\n this.driver.put_leased_session(updatedSession);\n\n // Broadcast UPDATE_PRESENCE WITHOUT targetActor to all sessions (patch)\n this.sendToAll(\n {\n type: ServerMsgCode.UPDATE_PRESENCE,\n actor: existingSession.actorId,\n data: presence, // Send only the patch, not the full merged presence\n // NO targetActor - this makes it a partial presence patch\n },\n ctx,\n defer\n );\n }\n }\n\n /**\n * List all server sessions. As a side effect, it will delete expired sessions.\n */\n public listLeasedSessions(\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to listLeasedSessions() to collect async side effects.\"\n );\n }\n ): LeasedSession[] {\n const sessions = this.driver.list_leased_sessions();\n const validSessions: LeasedSession[] = [];\n const toDelete: LeasedSession[] = [];\n for (const [_, session] of sessions) {\n if (isLeasedSessionExpired(session)) {\n toDelete.push(session);\n } else {\n validSessions.push(session);\n }\n }\n\n for (const session of toDelete) {\n this.deleteLeasedSession(session, ctx, defer);\n }\n\n return validSessions;\n }\n\n /**\n * Delete a server session and broadcast USER_LEFT to all sessions.\n */\n public deleteLeasedSession(\n session: LeasedSession,\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to deleteLeasedSession() to collect async side effects.\"\n );\n }\n ): void {\n // Broadcast USER_LEFT to all sessions\n this.sendToAll(\n {\n type: ServerMsgCode.USER_LEFT,\n actor: session.actorId,\n },\n ctx,\n defer\n );\n this.driver.delete_leased_session(session.sessionId);\n }\n\n /**\n * Delete all server sessions and broadcast USER_LEFT to all sessions.\n */\n public deleteAllLeasedSessions(\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to deleteAllLeasedSessions() to collect async side effects.\"\n );\n }\n ): void {\n const sessions = this.driver.list_leased_sessions();\n for (const [_, session] of sessions) {\n this.deleteLeasedSession(session, ctx, defer);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Feed APIs\n // ---------------------------------------------------------------------------\n\n /**\n * List feeds with pagination and filtering.\n */\n public listFeeds(options?: ListFeedsOptions): ListFeedsResult {\n return this.driver.list_feeds(options);\n }\n\n /**\n * Get a specific feed by feed ID.\n */\n public getFeed(feedId: string): Feed | undefined {\n return this.driver.get_feed(feedId);\n }\n\n /**\n * Create a new feed.\n * If timestamp is not provided, current server time is used.\n */\n public createFeed(\n feed: Omit<Feed, \"createdAt\" | \"updatedAt\"> & { timestamp?: number }\n ): Feed {\n const now = feed.timestamp ?? Date.now();\n const fullFeed: Feed = {\n ...feed,\n createdAt: now,\n updatedAt: now,\n };\n this.driver.create_feed(fullFeed);\n return fullFeed;\n }\n\n /**\n * Update a feed's metadata.\n */\n public updateFeedMetadata(feedId: string, metadata: Json): void {\n this.driver.update_feed_metadata(feedId, metadata);\n }\n\n /**\n * Delete a feed.\n */\n public deleteFeed(feedId: string): void {\n this.driver.delete_feed(feedId);\n }\n\n /**\n * List feed messages for a feed with pagination.\n */\n public listFeedMessages(\n feedId: string,\n options?: ListFeedMessagesOptions\n ): ListFeedMessagesResult {\n return this.driver.list_feed_messages(feedId, options);\n }\n\n /**\n * Add a message to a feed.\n * If message id is not provided, a unique ID is automatically generated.\n * If timestamp is not provided, current server time is used.\n */\n public addFeedMessage(\n feedId: string,\n message: Omit<FeedMessage, \"id\" | \"createdAt\" | \"updatedAt\"> &\n Partial<Pick<FeedMessage, \"id\">> & { timestamp?: number }\n ): FeedMessage {\n const now = message.timestamp ?? Date.now();\n const fullMessage: FeedMessage = {\n id: message.id ?? nanoid(),\n createdAt: now,\n updatedAt: now,\n data: message.data,\n };\n this.driver.add_feed_message(feedId, fullMessage);\n return fullMessage;\n }\n\n /**\n * Update a feed message's data.\n * Returns the updated message.\n */\n public updateFeedMessage(\n feedId: string,\n messageId: string,\n data: Json,\n timestamp?: number\n ): FeedMessage {\n return this.driver.update_feed_message(\n feedId,\n messageId,\n data,\n timestamp ?? Date.now()\n );\n }\n\n /**\n * Delete a feed message.\n */\n public deleteFeedMessage(feedId: string, messageId: string): void {\n this.driver.delete_feed_message(feedId, messageId);\n }\n\n /**\n * Will send the given ServerMsg through all Session, except the Session\n * where the message originates from.\n */\n public sendToOthers(\n sender: SessionKey,\n serverMsg: ServerMsg | readonly ServerMsg[],\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to sendToOthers() to collect async side effects.\"\n );\n }\n ): void {\n const msg = serialize(serverMsg);\n for (const [key, session] of this.otherSessionEntries(sender)) {\n const success = session.send(msg);\n if (success === 0) {\n // If there is a connection issue, terminate the session at once.\n // Note that in the case of -1 (= back pressure), we don't terminate\n // the connection.\n this.endBrowserSession(\n key,\n CloseCode.KICKED,\n \"Closed broken connection\",\n ctx,\n defer\n );\n }\n }\n }\n\n /**\n * Will broadcast the given ServerMsg to all Sessions in the Room.\n */\n public sendToAll(\n serverMsg: ServerMsg | readonly ServerMsg[],\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to sendToAll() to collect async side effects.\"\n );\n }\n ): void {\n const msg = serialize(serverMsg);\n for (const [key, session] of this.sessions) {\n const success = session.send(msg);\n if (success === 0) {\n // If there is a connection issue, terminate the session at once.\n // Note that in the case of -1 (= back pressure), we don't terminate\n // the connection.\n this.endBrowserSession(\n key,\n CloseCode.KICKED,\n \"Closed broken connection\",\n ctx,\n defer\n );\n }\n }\n }\n\n /**\n * Returns a new, unique, actor ID.\n */\n public getNextActor(): ActorID {\n return this.driver.next_actor() as ActorID;\n }\n\n // ------------------------------------------------------------------------------------\n // Private APIs\n // ------------------------------------------------------------------------------------\n\n /**\n * Iterates over all *other* Sessions and their session keys.\n */\n private *otherSessionEntries(\n currentKey: SessionKey\n ): Generator<[SessionKey, BrowserSession<SM, CM>]> {\n for (const [key, session] of this.sessions) {\n if (key !== currentKey) {\n yield [key, session];\n }\n }\n }\n\n /**\n * Iterates over all *other* Sessions.\n */\n private *otherSessions(\n currentKey: SessionKey\n ): Generator<BrowserSession<SM, CM>> {\n for (const [key, session] of this.sessions) {\n if (key !== currentKey) {\n yield session;\n }\n }\n }\n\n /**\n * @internal\n * Handles an incoming ping, by sending a pong back.\n */\n private async handlePing(sessionKey: SessionKey, ctx?: C): Promise<void> {\n const session = this.sessions.get(sessionKey);\n if (session === undefined) {\n this.#logger\n .withContext({ sessionKey })\n .warn(\"[probe] in handlePing, no such session exists\");\n return;\n }\n\n const sent = session.sendPong();\n\n // 0 means there was a connection issue\n // -1 means there was back pressure, which is no issue (we'll just count the ping)\n if (sent !== 0) {\n await this.hooks.onDidPong?.(ctx);\n }\n }\n\n private _processClientMsg_withExclusiveAccess(\n sessionKey: SessionKey,\n messages: ClientMsg[],\n ctx: C | undefined,\n defer: (p: Promise<void>) => void\n ): void {\n const session = this.sessions.get(sessionKey);\n if (!session) {\n this.#logger\n .withContext({ sessionKey })\n .warn(\"[probe] in handleClientMsgs, no such session exists\");\n return;\n }\n\n // Keep two ServerMsg buffers to send at the end:\n // - Messages to fan-out to all *others* (current session not included)\n // - Messages to reply back to the current sender (i.e. acks and rejections)\n const toFanOut: ServerMsg[] = [];\n const toReply: ServerMsg[] = [];\n const replyImmediately = (\n msg: ServerMsg | ServerMsg[] | jstring<ServerMsg>\n ) => void session.send(msg);\n const scheduleFanOut = (msg: ServerMsg) => void toFanOut.push(msg);\n const scheduleReply = (msg: ServerMsg) => void toReply.push(msg);\n\n for (const msg of messages) {\n const isMsgAllowed = this.hooks.isClientMsgAllowed(msg, session);\n if (isMsgAllowed.allowed) {\n this.handleOne(\n session,\n msg,\n replyImmediately,\n scheduleFanOut,\n scheduleReply,\n ctx,\n defer\n );\n } else {\n if (!session.hasNotifiedClientStorageUpdateError) {\n toReply.push({\n type: ServerMsgCode.REJECT_STORAGE_OP,\n opIds:\n msg.type === ClientMsgCode.UPDATE_STORAGE\n ? msg.ops.map((op) => op.opId)\n : [],\n reason: isMsgAllowed.reason,\n });\n session.setHasNotifiedClientStorageUpdateError();\n }\n }\n }\n\n if (toFanOut.length > 0) {\n this.sendToOthers(sessionKey, toFanOut, ctx, defer);\n }\n\n if (toReply.length > 0) {\n session.send(toReply);\n }\n }\n\n // TODO It's a bit bothering how much duplication there is between this method\n // and the _processClientMsg_withExclusiveAccess version. A better\n // abstraction is needed.\n private _processClientMsgFromBackendSession_withExclusiveAccess(\n session: BackendSession,\n messages: ClientMsg[],\n ctx: C | undefined,\n defer: (p: Promise<void>) => void\n ): void {\n // Keep two ServerMsg buffers to send at the end:\n // - Messages to fan-out to all *others* (current session not included)\n // - Messages to reply back to the current sender (i.e. acks and rejections)\n const toFanOut: ServerMsg[] = [];\n const toReplyImmediately: ServerMsg[] = [];\n const toReplyAfter: ServerMsg[] = [];\n\n const replyImmediately = (\n msg: ServerMsg | ServerMsg[] | jstring<ServerMsg>\n ) => {\n if (typeof msg === \"string\") {\n // Pre-serialized payload: flush any pending typed messages first so\n // wire order matches call order, then send the raw string directly.\n if (toReplyImmediately.length > 0) {\n session.send(toReplyImmediately);\n toReplyImmediately.length = 0;\n }\n session.send(msg);\n } else if (Array.isArray(msg)) {\n for (const m of msg) {\n toReplyImmediately.push(m);\n }\n } else {\n toReplyImmediately.push(msg);\n }\n };\n const scheduleFanOut = (msg: ServerMsg) => void toFanOut.push(msg);\n const scheduleReply = (msg: ServerMsg) => void toReplyAfter.push(msg);\n\n for (const msg of messages) {\n this.handleOne(\n session,\n msg,\n replyImmediately,\n scheduleFanOut,\n scheduleReply,\n ctx,\n defer\n );\n }\n\n if (toReplyImmediately.length > 0) {\n session.send(toReplyImmediately);\n toReplyImmediately.length = 0;\n }\n\n if (toFanOut.length > 0) {\n this.sendToOthers(\"(transient)\" as SessionKey, toFanOut, ctx, defer);\n toFanOut.length = 0;\n }\n\n if (toReplyAfter.length > 0) {\n session.send(toReplyAfter);\n toReplyAfter.length = 0;\n }\n }\n\n private handleOne(\n session: BrowserSession<SM, CM>,\n msg: ClientMsg,\n replyImmediately: (\n msg: ServerMsg | ServerMsg[] | jstring<ServerMsg>\n ) => void,\n scheduleFanOut: (msg: ServerMsg) => void,\n scheduleReply: (msg: ServerMsg) => void,\n ctx: C | undefined,\n defer: (p: Promise<void>) => void\n ): void {\n if (!this.mutex.isLocked()) {\n throw new Error(\"Handling messages requires exclusive access\");\n }\n\n switch (msg.type) {\n case ClientMsgCode.UPDATE_PRESENCE: {\n // YYY Maybe consider calling session.sendToOthers() directly here instead of queueing for fan-out?\n scheduleFanOut({\n type: ServerMsgCode.UPDATE_PRESENCE,\n actor: session.actor,\n data: msg.data,\n targetActor: msg.targetActor,\n });\n break;\n }\n\n case ClientMsgCode.BROADCAST_EVENT: {\n // YYY Maybe consider calling session.sendToOthers() directly here instead of queueing for fan-out?\n scheduleFanOut({\n type: ServerMsgCode.BROADCASTED_EVENT,\n actor: session.actor,\n event: msg.event,\n });\n break;\n }\n\n case ClientMsgCode.FETCH_STORAGE: {\n if (session.version >= ProtocolVersion.V8) {\n //\n // Turn storage nodes into WebSocket messages. They get chunked so\n // that each individual WebSocket message won't be too big, but the\n // total number of WebSocket messages needed is kept in check.\n //\n // NOTE: We use iter_nodes_optimized() here to avoid the JS overhead\n // of parsing, mutating, serializing, and measuring each individual\n // node, which can add up in large documents.\n //\n const rawStream = this.storage.driver.iter_nodes_optimized();\n for (const chunk of groupNodesForWebSocketMessages(rawStream)) {\n const frame =\n `{\"type\":${ServerMsgCode.STORAGE_CHUNK},\"nodes\":[${chunk.join(\",\")}]}` as jstring<ServerMsg>;\n replyImmediately(frame);\n }\n replyImmediately({ type: ServerMsgCode.STORAGE_STREAM_END });\n } else {\n replyImmediately({\n type: ServerMsgCode.STORAGE_STATE_V7,\n items: Array.from(this.storage.driver.iter_nodes()),\n });\n }\n break;\n }\n\n case ClientMsgCode.UPDATE_STORAGE: {\n // Bump storage version to indicate data will get mutated\n // A driver can use this information to implement copy-on-write\n // semantics to provide snapshot isolation.\n this.driver.bump_storage_version?.();\n\n const result = this.storage.applyOps(msg.ops);\n\n const opsToForward: ServerWireOp[] = result.flatMap((r) =>\n r.action === \"accepted\" ? [r.op] : []\n );\n\n const opsToSendBack: ServerWireOp[] = result.flatMap((r) => {\n switch (r.action) {\n case \"ignored\":\n // HACK! We send a cleverly composed message, that will act\n // as an acknowledgement to all old clients out there in\n // the wild.\n return r.ignoredOpId !== undefined\n ? [ackIgnoredOp(r.ignoredOpId)]\n : [];\n\n case \"accepted\":\n return r.fix !== undefined ? [r.fix] : [];\n\n // istanbul ignore next\n default:\n return assertNever(r, \"Unhandled case\");\n }\n });\n\n if (opsToForward.length > 0) {\n scheduleFanOut({\n type: ServerMsgCode.UPDATE_STORAGE,\n ops: opsToForward.map(stripOpId),\n });\n scheduleReply({\n type: ServerMsgCode.UPDATE_STORAGE,\n ops: opsToForward,\n });\n }\n\n if (opsToSendBack.length > 0) {\n replyImmediately({\n type: ServerMsgCode.UPDATE_STORAGE,\n ops: opsToSendBack,\n });\n }\n\n if (opsToForward.length > 0) {\n // NOTE! These are being called after *every* handleOne() call\n // currently. Should we not just call these once at the end of\n // handleClientMsgs()?\n const p$ = this.hooks.postClientMsgStorageDidUpdate?.(ctx);\n if (p$) defer(p$);\n }\n break;\n }\n\n case ClientMsgCode.FETCH_YDOC: {\n const vector = msg.vector;\n const guid = msg.guid as Guid | undefined;\n const isV2 = msg.v2;\n const update = this.yjsStorage.getYDocUpdate(\n this.#logger,\n vector,\n guid,\n isV2\n );\n const stateVector = this.yjsStorage.getYStateVector(this.#logger, guid);\n const snapshotHash = this.yjsStorage.getSnapshotHash(this.#logger, {\n guid,\n isV2,\n });\n\n if (update !== null && snapshotHash !== null) {\n replyImmediately({\n type: ServerMsgCode.UPDATE_YDOC,\n update,\n isSync: true, // this is no longer used by the client, instead we use the presence of stateVector\n stateVector,\n guid,\n v2: isV2,\n remoteSnapshotHash: snapshotHash,\n });\n }\n break;\n }\n\n case ClientMsgCode.UPDATE_YDOC: {\n const update = msg.update;\n const guid = msg.guid as Guid | undefined;\n const isV2 = msg.v2;\n const [result, error] = tryCatch(() =>\n this.yjsStorage.addYDocUpdate(this.#logger, update, guid, isV2)\n );\n\n if (error)\n // Ignore any errors\n break;\n\n this.sendToAll(\n {\n type: ServerMsgCode.UPDATE_YDOC,\n update,\n guid,\n isSync: false,\n stateVector: null,\n v2: isV2,\n remoteSnapshotHash: result.snapshotHash,\n },\n ctx,\n defer\n );\n if (result.isUpdated) {\n const p$ = this.hooks.postClientMsgYdocDidUpdate?.(ctx, session);\n if (p$) defer(p$);\n }\n\n break;\n }\n\n // Feed messages\n case FeedMsgCode.FETCH_FEEDS: {\n const fetchMsg = msg;\n const [result, err] = tryCatch(() =>\n this.listFeeds({\n cursor: fetchMsg.cursor,\n since: fetchMsg.since,\n limit: fetchMsg.limit,\n metadata: fetchMsg.metadata,\n })\n );\n if (err) {\n replyImmediately(feedFailureServerMsg(fetchMsg.requestId, err));\n break;\n }\n replyImmediately({\n type: FeedMsgCode.FEEDS_LIST,\n requestId: fetchMsg.requestId,\n feeds: result.feeds,\n nextCursor: result.nextCursor,\n });\n break;\n }\n\n case FeedMsgCode.FETCH_FEED_MESSAGES: {\n const fetchMsg = msg;\n const [result, err] = tryCatch(() =>\n this.listFeedMessages(fetchMsg.feedId, {\n cursor: fetchMsg.cursor,\n since: fetchMsg.since,\n limit: fetchMsg.limit,\n })\n );\n if (err) {\n replyImmediately(feedFailureServerMsg(fetchMsg.requestId, err));\n break;\n }\n replyImmediately({\n type: FeedMsgCode.FEED_MESSAGES_LIST,\n requestId: fetchMsg.requestId,\n feedId: fetchMsg.feedId,\n messages: result.messages,\n nextCursor: result.nextCursor,\n });\n break;\n }\n\n case FeedMsgCode.ADD_FEED: {\n const addMsg = msg;\n const [feed, err] = tryCatch(() =>\n this.createFeed({\n feedId: addMsg.feedId,\n metadata: (addMsg.metadata as Json) ?? {},\n })\n );\n if (err) {\n replyImmediately(feedFailureServerMsg(addMsg.requestId, err));\n break;\n }\n const feedsMsg = {\n type: FeedMsgCode.FEEDS_ADDED,\n feeds: [feed],\n };\n replyImmediately(feedsMsg);\n scheduleFanOut(feedsMsg);\n break;\n }\n\n case FeedMsgCode.UPDATE_FEED: {\n const updateMsg = msg;\n const [, metaErr] = tryCatch(() =>\n this.updateFeedMetadata(updateMsg.feedId, updateMsg.metadata as Json)\n );\n if (metaErr) {\n replyImmediately(feedFailureServerMsg(updateMsg.requestId, metaErr));\n break;\n }\n const feed = this.getFeed(updateMsg.feedId);\n if (!feed) {\n replyImmediately(\n feedRequestFailed(\n updateMsg.requestId,\n FeedRequestErrorCode.FEED_NOT_FOUND\n )\n );\n break;\n }\n const feedsMsg = {\n type: FeedMsgCode.FEEDS_UPDATED,\n feeds: [feed],\n };\n replyImmediately(feedsMsg);\n scheduleFanOut(feedsMsg);\n break;\n }\n\n case FeedMsgCode.DELETE_FEED: {\n const deleteMsg = msg;\n const [, err] = tryCatch(() => this.deleteFeed(deleteMsg.feedId));\n if (err) {\n replyImmediately(feedFailureServerMsg(deleteMsg.requestId, err));\n break;\n }\n const feedDeletedMsg = {\n type: FeedMsgCode.FEED_DELETED,\n feedId: deleteMsg.feedId,\n };\n replyImmediately(feedDeletedMsg);\n scheduleFanOut(feedDeletedMsg);\n break;\n }\n\n case FeedMsgCode.ADD_FEED_MESSAGE: {\n const addMsg = msg;\n const [message, err] = tryCatch(() =>\n this.addFeedMessage(addMsg.feedId, {\n data: addMsg.data as Json,\n id: addMsg.id,\n })\n );\n if (err) {\n replyImmediately(feedFailureServerMsg(addMsg.requestId, err));\n break;\n }\n const feedMessagesMsg = {\n type: FeedMsgCode.FEED_MESSAGES_ADDED,\n feedId: addMsg.feedId,\n messages: [message],\n };\n replyImmediately(feedMessagesMsg);\n scheduleFanOut(feedMessagesMsg);\n break;\n }\n\n case FeedMsgCode.UPDATE_FEED_MESSAGE: {\n const updateMsg = msg;\n const [message, err] = tryCatch(() =>\n this.updateFeedMessage(\n updateMsg.feedId,\n updateMsg.messageId,\n updateMsg.data as Json\n )\n );\n if (err) {\n replyImmediately(feedFailureServerMsg(updateMsg.requestId, err));\n break;\n }\n const feedMessagesMsg = {\n type: FeedMsgCode.FEED_MESSAGES_UPDATED,\n feedId: updateMsg.feedId,\n messages: [message],\n };\n replyImmediately(feedMessagesMsg);\n scheduleFanOut(feedMessagesMsg);\n break;\n }\n\n case FeedMsgCode.DELETE_FEED_MESSAGE: {\n const deleteMsg = msg;\n const [, err] = tryCatch(() =>\n this.deleteFeedMessage(deleteMsg.feedId, deleteMsg.messageId)\n );\n if (err) {\n replyImmediately(feedFailureServerMsg(deleteMsg.requestId, err));\n break;\n }\n const feedMessagesMsg = {\n type: FeedMsgCode.FEED_MESSAGES_DELETED,\n feedId: deleteMsg.feedId,\n messageIds: [deleteMsg.messageId],\n };\n replyImmediately(feedMessagesMsg);\n scheduleFanOut(feedMessagesMsg);\n break;\n }\n\n default: {\n try {\n return assertNever(msg, \"Unrecognized client msg\");\n } catch {\n // Ignore\n }\n }\n }\n }\n}\n\nexport { serialize as serializeServerMsg };\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { JsonObject } from \"@liveblocks/core\";\nimport { raise } from \"@liveblocks/core\";\n\nexport enum LogLevel {\n DEBUG = 0,\n INFO = 1,\n WARNING = 2,\n ERROR = 3,\n}\n\nfunction formatError(err: Error) {\n const prefix = `${err.name}: ${err.message}`;\n return (\n err.stack?.startsWith(prefix) ? err.stack : `${prefix}\\n${err.stack ?? \"\"}`\n ).trimEnd();\n}\n\n/**\n * Inherit from this abstract log target to implement your own custom\n * LogTarget.\n */\nexport abstract class LogTarget {\n public readonly level: LogLevel;\n\n #cache = new WeakMap<JsonObject, string>();\n\n constructor(level: LogLevel | keyof typeof LogLevelNames = LogLevel.INFO) {\n this.level =\n typeof level === \"number\"\n ? level\n : (LogLevelNames[level] ?? LogLevel.INFO);\n }\n\n /** Helper for formatting a log level */\n protected formatLevel(level: LogLevel): string {\n switch (level) {\n case LogLevel.DEBUG:\n return \"debug\";\n case LogLevel.INFO:\n return \"info\";\n case LogLevel.WARNING:\n return \"warn\";\n case LogLevel.ERROR:\n return \"error\";\n default:\n return raise(\"Invalid log level\");\n }\n }\n\n /** Helper for formatting an Arg */\n protected formatArg(arg: string | Error): string {\n return typeof arg === \"object\"\n ? arg instanceof Error\n ? formatError(arg)\n : JSON.stringify(arg)\n : String(arg); // Coerce to string in case TypeScript is bypassed\n }\n\n /**\n * Helper for formatting a Context. Override this in a subclass to change the\n * formatting.\n */\n protected formatContextImpl(context: JsonObject): string {\n const parts = [];\n for (const [k, v] of Object.entries(context ?? {})) {\n if (v !== undefined) {\n // Object, or null, or array\n const sv = typeof v === \"object\" ? JSON.stringify(v) : v;\n parts.push(`${k}=${sv}`);\n }\n }\n return parts.length > 0 ? `[${parts.join(\" \")}]` : \"\";\n }\n\n /**\n * Helper for formatting a Context. Will only compute the string once for\n * every Context instance, and keep its computed string value cached for\n * performance.\n */\n protected formatContext(context: JsonObject): string {\n let formatted = this.#cache.get(context);\n if (formatted === undefined) {\n formatted = this.formatContextImpl(context);\n this.#cache.set(context, formatted);\n }\n return formatted;\n }\n\n /**\n * Implement this in a concrete subclass. The goal is to do whatever to log\n * the given log level, context, and log arg. You'll typically want to\n * utilize the pre-defined helper methods .formatContext() and .formatArg()\n * to implement this.\n */\n abstract log(level: LogLevel, context: JsonObject, arg: string | Error): void;\n}\n\n//\n// Console log target ----------------------------------------------------------\n//\n\nconst CONSOLE_METHOD = {\n [LogLevel.DEBUG]: \"info\",\n [LogLevel.INFO]: \"info\",\n [LogLevel.WARNING]: \"warn\",\n [LogLevel.ERROR]: \"error\",\n} as const;\n\nexport class ConsoleTarget extends LogTarget {\n log(level: LogLevel, context: JsonObject, arg: string | Error): void {\n console[CONSOLE_METHOD[level]](\n this.formatArg(arg),\n this.formatContext(context)\n );\n }\n}\n\n//\n// Logger implementation ------------------------------------------------------\n//\n\n// Friendly names to pass to the constructor\nconst LogLevelNames = {\n debug: LogLevel.DEBUG,\n info: LogLevel.INFO,\n warning: LogLevel.WARNING,\n error: LogLevel.ERROR,\n} as const;\n\ntype LogFn = (arg: string | Error) => void;\n\n/**\n * Structured logger with configurable log targets.\n */\nexport class Logger {\n public readonly debug: LogFn;\n public readonly info: LogFn;\n public readonly warn: LogFn;\n public readonly error: LogFn;\n\n public readonly o: {\n readonly debug?: LogFn;\n readonly info?: LogFn;\n readonly warn?: LogFn;\n readonly error?: LogFn;\n };\n\n private readonly _context: JsonObject;\n private readonly _targets: readonly LogTarget[];\n\n constructor(\n target: LogTarget | readonly LogTarget[] = new ConsoleTarget(),\n context: JsonObject = {}\n ) {\n this._context = context;\n this._targets = Array.isArray(target) ? target : [target];\n\n const minLevel: number = Math.min(...this._targets.map((t) => t.level));\n\n const noop = () => {};\n const makeLogFn = (lvl: LogLevel) => (arg: string | Error) =>\n this._targets.forEach((target) => {\n if (target.level <= lvl) {\n target.log(lvl, this._context, arg);\n }\n });\n\n this.o = {\n debug: minLevel <= LogLevel.DEBUG ? makeLogFn(LogLevel.DEBUG) : undefined,\n info: minLevel <= LogLevel.INFO ? makeLogFn(LogLevel.INFO) : undefined,\n warn:\n minLevel <= LogLevel.WARNING ? makeLogFn(LogLevel.WARNING) : undefined,\n error: minLevel <= LogLevel.ERROR ? makeLogFn(LogLevel.ERROR) : undefined,\n };\n\n this.debug = this.o.debug ?? noop;\n this.info = this.o.info ?? noop;\n this.warn = this.o.warn ?? noop;\n this.error = this.o.error ?? noop;\n }\n\n /**\n * Creates a new Logger instance with the given extra context applied. All\n * log calls made from that new Logger will carry all current _and_ the extra\n * context, with the extra context taking precedence. Assign an explicit\n * `undefined` value to a key to \"remove\" it from the context.\n */\n withContext(extra: JsonObject): Logger {\n const combined: JsonObject = { ...this._context, ...extra };\n return new Logger(this._targets, combined);\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\n/* eslint-disable @typescript-eslint/explicit-module-boundary-types */\nimport type {\n CompactNode,\n Json,\n JsonObject,\n NodeMap,\n NodeStream,\n PlainLsonObject,\n SerializedChild,\n SerializedCrdt,\n SerializedObject,\n SerializedRootObject,\n} from \"@liveblocks/core\";\nimport {\n asPos,\n CrdtType,\n isRootStorageNode,\n nn,\n nodeStreamToCompactNodes,\n} from \"@liveblocks/core\";\nimport { ifilter, imap } from \"itertools\";\n\nimport type { YDocId } from \"~/decoders/y-types\";\nimport { plainLsonToNodeStream } from \"~/formats/PlainLson\";\nimport type {\n IReadableSnapshot,\n IStorageDriver,\n LeasedSession,\n ListFeedMessagesOptions,\n ListFeedMessagesResult,\n ListFeedsOptions,\n ListFeedsResult,\n} from \"~/interfaces\";\nimport { NestedMap } from \"~/lib/NestedMap\";\nimport { quote } from \"~/lib/text\";\nimport { makeInMemorySnapshot } from \"~/makeInMemorySnapshot\";\nimport type { Feed, FeedMessage, jstring, Pos } from \"~/types\";\n\nfunction buildRevNodes(nodeStream: NodeStream) {\n const result = new NestedMap<string, string, string>();\n for (const node of nodeStream) {\n if (isRootStorageNode(node)) continue;\n\n // Highest node id wins in case of conflict (deterministic across backends)\n const [id, crdt] = node;\n const existing = result.get(crdt.parentId, crdt.parentKey);\n if (existing === undefined || id > existing) {\n result.set(crdt.parentId, crdt.parentKey, id);\n }\n }\n return result;\n}\n\n/**\n * Builds the reverse node index, and corrects any data corruption found\n * along the way.\n */\nfunction buildReverseLookup(nodes: NodeMap) {\n const revNodes = buildRevNodes(nodes as NodeStream);\n\n const queue: string[] = [\"root\"];\n const reachableNodes: Set<string> = new Set();\n\n while (queue.length > 0) {\n const nodeId = queue.pop()!;\n const node = nn(nodes.get(nodeId));\n\n if (node.type === CrdtType.OBJECT) {\n for (const key of revNodes.keysAt(nodeId)) {\n delete node.data[key]; // Remove static data that conflicts with child nodes\n }\n }\n\n if (node.type !== CrdtType.REGISTER) {\n queue.push(...revNodes.valuesAt(nodeId));\n } else {\n const parent = nodes.get(node.parentId);\n if (parent?.type === CrdtType.OBJECT) {\n continue;\n }\n }\n\n reachableNodes.add(nodeId);\n }\n\n // Delete unreachable nodes (safe to delete from Map during iteration)\n let deletedCount = 0;\n for (const [id] of nodes) {\n if (!reachableNodes.has(id)) {\n nodes.delete(id);\n deletedCount++;\n }\n }\n\n // If no nodes were dropped (99% happy path), revNodes is correct already.\n // Otherwise, recompute it.\n return deletedCount === 0 ? revNodes : buildRevNodes(nodes as NodeStream);\n}\n\nfunction hasStaticDataAt(\n node: SerializedCrdt,\n key: string\n): node is SerializedObject | SerializedRootObject {\n return (\n node.type === CrdtType.OBJECT &&\n Object.prototype.hasOwnProperty.call(node.data, key) &&\n node.data[key] !== undefined\n );\n}\n\n/**\n * The lazily-initialized part of the driver that implements all node-related\n * APIs.\n */\ntype NodesAPI = Pick<\n IStorageDriver,\n | \"get_node\"\n | \"iter_nodes\"\n | \"iter_nodes_optimized\"\n | \"has_node\"\n | \"get_child_at\"\n | \"has_child_at\"\n | \"get_next_sibling\"\n | \"get_last_sibling\"\n | \"set_child\"\n | \"move_sibling\"\n | \"delete_node\"\n | \"delete_child_key\"\n | \"set_object_data\"\n | \"get_snapshot\"\n>;\n\n/**\n * Implements the most basic in-memory store. Used if no explicit store is\n * provided.\n */\nexport class InMemoryDriver implements IStorageDriver {\n private _nextActor;\n private _nodes: NodeMap;\n private _nodesApi?: NodesAPI;\n private _metadb: Map<string, Json>;\n private _ydb: Map<string, Uint8Array>;\n private _leasedSessions: Map<string, LeasedSession>;\n private _feeds: Map<string, Feed>;\n private _feedMessages: Map<string, FeedMessage>; // Key: `${feedId}:${messageId}`\n\n constructor(options?: {\n initialActor?: number;\n initialNodes?: Iterable<[string, SerializedCrdt]>;\n }) {\n this._nodes = new Map();\n this._metadb = new Map();\n this._ydb = new Map();\n this._leasedSessions = new Map();\n this._feeds = new Map();\n this._feedMessages = new Map();\n\n this._nextActor = options?.initialActor ?? -1;\n\n for (const [key, value] of options?.initialNodes ?? []) {\n this._nodes.set(key, value);\n }\n }\n\n raw_iter_nodes() {\n return this._nodes[Symbol.iterator]();\n }\n\n reinitialize(): void {\n this._nodesApi = undefined;\n }\n\n /** Deletes all nodes and replaces them with the given document. */\n DANGEROUSLY_reset_nodes(doc: PlainLsonObject) {\n // Invalidate the cached node API: its reverse-lookup index is built from\n // the node map we're about to replace. The next access rebuilds it.\n this.reinitialize();\n\n this._nodes.clear();\n for (const [id, node] of plainLsonToNodeStream(doc)) {\n this._nodes.set(id, node);\n }\n }\n\n get_meta(key: string) {\n return this._metadb.get(key);\n }\n put_meta(key: string, value: Json) {\n this._metadb.set(key, value);\n }\n delete_meta(key: string) {\n this._metadb.delete(key);\n }\n\n list_leased_sessions() {\n return this._leasedSessions.entries();\n }\n\n get_leased_session(sessionId: string) {\n return this._leasedSessions.get(sessionId);\n }\n\n put_leased_session(session: LeasedSession) {\n this._leasedSessions.set(session.sessionId, session);\n }\n\n delete_leased_session(sessionId: string) {\n this._leasedSessions.delete(sessionId);\n }\n\n takeRowsWritten(): number {\n return 0;\n }\n\n // ---------------------------------------------------------------------------\n // Feed APIs\n // ---------------------------------------------------------------------------\n\n list_feeds(options?: ListFeedsOptions): ListFeedsResult {\n const limit = Math.min(options?.limit ?? 20, 100);\n const since = options?.since;\n const cursor = options?.cursor;\n const metadata = options?.metadata;\n\n let feeds: Feed[] = [];\n\n // Collect all feeds\n for (const [_, feed] of this._feeds.entries()) {\n // Apply metadata filtering\n if (metadata !== undefined) {\n const feedMetadata = feed.metadata as JsonObject;\n let matches = true;\n for (const [key, value] of Object.entries(metadata)) {\n if (feedMetadata[key] !== value) {\n matches = false;\n break;\n }\n }\n if (!matches) continue;\n }\n\n // Apply since filter (uses createdAt for stable ordering)\n if (since !== undefined && feed.createdAt < since) {\n continue;\n }\n\n feeds.push(feed);\n }\n\n // Sort by createdAt descending, then by feedId descending\n feeds.sort((a, b) => {\n if (b.createdAt !== a.createdAt) {\n return b.createdAt - a.createdAt;\n }\n return b.feedId.localeCompare(a.feedId);\n });\n\n // Apply cursor-based pagination\n if (cursor !== undefined) {\n try {\n // Simple base64url decode (using btoa/atob for browser compatibility)\n const decoded = JSON.parse(\n decodeURIComponent(\n escape(atob(cursor.replace(/-/g, \"+\").replace(/_/g, \"/\")))\n )\n ) as [string, number];\n const [cursorFeedId, cursorCreatedAt] = decoded;\n feeds = feeds.filter((s) => {\n return (\n s.createdAt < cursorCreatedAt ||\n (s.createdAt === cursorCreatedAt && s.feedId < cursorFeedId)\n );\n });\n } catch {\n // Invalid cursor, ignore it\n }\n }\n\n // Apply limit (get one extra to determine if there's a next page)\n let nextCursor: string | undefined;\n if (feeds.length > limit) {\n feeds = feeds.slice(0, limit);\n // Create cursor from last returned item\n const last = feeds[feeds.length - 1];\n if (last) {\n const cursorData: [string, number] = [last.feedId, last.createdAt];\n // Simple base64url encode\n nextCursor = btoa(\n unescape(encodeURIComponent(JSON.stringify(cursorData)))\n )\n .replace(\"/\", \"_\")\n .replace(\"+\", \"-\")\n .replace(/=+$/, \"\");\n }\n }\n\n return { feeds, nextCursor };\n }\n\n get_feed(feedId: string): Feed | undefined {\n const feed = this._feeds.get(feedId);\n if (feed === undefined) {\n return undefined;\n }\n\n return feed;\n }\n\n create_feed(feed: Feed): void {\n // Check if feed already exists\n if (this._feeds.has(feed.feedId)) {\n throw new Error(`Feed ${feed.feedId} already exists`);\n }\n\n // Store feed metadata\n this._feeds.set(feed.feedId, {\n feedId: feed.feedId,\n metadata: feed.metadata,\n createdAt: feed.createdAt,\n updatedAt: feed.updatedAt,\n });\n }\n\n update_feed_metadata(feedId: string, metadata: Json): void {\n const existing = this._feeds.get(feedId);\n if (existing === undefined) {\n throw new Error(`Feed ${feedId} not found`);\n }\n\n this._feeds.set(feedId, {\n ...existing,\n metadata,\n });\n }\n\n delete_feed(feedId: string): void {\n // Delete all messages for this feed\n const messageKeys: string[] = [];\n for (const [key] of this._feedMessages.entries()) {\n if (key.startsWith(`${feedId}:`)) {\n messageKeys.push(key);\n }\n }\n for (const key of messageKeys) {\n this._feedMessages.delete(key);\n }\n\n // Delete feed\n this._feeds.delete(feedId);\n }\n\n list_feed_messages(\n feedId: string,\n options?: ListFeedMessagesOptions\n ): ListFeedMessagesResult {\n const limit = Math.min(options?.limit ?? 20, 100);\n const since = options?.since;\n const cursor = options?.cursor;\n\n let messages: FeedMessage[] = [];\n\n // Collect all messages for this feed\n const prefix = `${feedId}:`;\n for (const [key, message] of this._feedMessages.entries()) {\n if (key.startsWith(prefix)) {\n // Apply since filter (uses createdAt for stable ordering)\n if (since !== undefined && message.createdAt < since) {\n continue;\n }\n\n messages.push(message);\n }\n }\n\n // Sort by createdAt descending, then by id descending\n messages.sort((a, b) => {\n if (b.createdAt !== a.createdAt) {\n return b.createdAt - a.createdAt;\n }\n return b.id.localeCompare(a.id);\n });\n\n // Apply cursor-based pagination\n if (cursor !== undefined) {\n try {\n const decoded = JSON.parse(\n decodeURIComponent(\n escape(atob(cursor.replace(/-/g, \"+\").replace(/_/g, \"/\")))\n )\n ) as [string, number];\n const [cursorMessageId, cursorCreatedAt] = decoded;\n messages = messages.filter((m) => {\n return (\n m.createdAt < cursorCreatedAt ||\n (m.createdAt === cursorCreatedAt && m.id < cursorMessageId)\n );\n });\n } catch {\n // Invalid cursor, ignore it\n }\n }\n\n // Apply limit (get one extra to determine if there's a next page)\n let nextCursor: string | undefined;\n if (messages.length > limit) {\n messages = messages.slice(0, limit);\n // Create cursor from last returned item\n const last = messages[messages.length - 1];\n if (last) {\n const cursorData: [string, number] = [last.id, last.createdAt];\n nextCursor = btoa(\n unescape(encodeURIComponent(JSON.stringify(cursorData)))\n )\n .replace(\"/\", \"_\")\n .replace(\"+\", \"-\")\n .replace(/=+$/, \"\");\n }\n }\n\n return { messages, nextCursor };\n }\n\n add_feed_message(feedId: string, message: FeedMessage): void {\n // Verify feed exists\n const feed = this._feeds.get(feedId);\n if (feed === undefined) {\n throw new Error(`Feed ${feedId} not found`);\n }\n\n this._feedMessages.set(`${feedId}:${message.id}`, message);\n }\n\n update_feed_message(\n feedId: string,\n messageId: string,\n data: Json,\n timestamp?: number\n ): FeedMessage {\n const key = `${feedId}:${messageId}`;\n const message = this._feedMessages.get(key);\n\n if (message === undefined) {\n throw new Error(`Feed message ${messageId} not found in feed ${feedId}`);\n }\n\n const effectiveTimestamp = timestamp ?? Date.now();\n if (effectiveTimestamp < message.updatedAt) {\n return message;\n }\n\n const updatedMessage: FeedMessage = {\n ...message,\n updatedAt: effectiveTimestamp,\n data,\n };\n\n this._feedMessages.set(key, updatedMessage);\n\n return updatedMessage;\n }\n\n delete_feed_message(feedId: string, messageId: string): void {\n this._feedMessages.delete(`${feedId}:${messageId}`);\n }\n\n next_actor() {\n return ++this._nextActor;\n }\n\n iter_y_updates(docId: YDocId) {\n const prefix = `${docId}@|@`;\n return imap(\n ifilter(this._ydb.entries(), ([k]) => k.startsWith(prefix)),\n ([k, v]) => [k.slice(prefix.length), v] as [string, Uint8Array]\n );\n }\n write_y_updates(docId: YDocId, key: string, data: Uint8Array) {\n this._ydb.set(`${docId}@|@${key}`, data);\n }\n delete_y_updates(docId: YDocId, keys: string[]) {\n for (const key of keys) {\n this._ydb.delete(`${docId}@|@${key}`);\n }\n }\n\n /** @private Only use this in unit tests, never in production. */\n DANGEROUSLY_wipe_all_y_updates() {\n this._ydb.clear();\n }\n\n // ---------------------------------------------------------------------------\n // Node APIs\n // ---------------------------------------------------------------------------\n\n /**\n * The lazily-built node API. Building it ensures the root node exists and\n * constructs the reverse-lookup index. Invalidated by\n * DANGEROUSLY_reset_nodes(), after which the next access rebuilds it.\n */\n private get loadedApi(): NodesAPI {\n return (this._nodesApi ??= this._loadNodesApi());\n }\n\n get_node(id: string): SerializedCrdt | undefined {\n return this.loadedApi.get_node(id);\n }\n\n iter_nodes(): NodeStream {\n return this.loadedApi.iter_nodes();\n }\n\n iter_nodes_optimized(): Iterable<jstring<CompactNode>> {\n return this.loadedApi.iter_nodes_optimized();\n }\n\n has_node(id: string): boolean {\n return this.loadedApi.has_node(id);\n }\n\n get_child_at(parentId: string, parentKey: string): string | undefined {\n return this.loadedApi.get_child_at(parentId, parentKey);\n }\n\n has_child_at(parentId: string, parentKey: string): boolean {\n return this.loadedApi.has_child_at(parentId, parentKey);\n }\n\n get_next_sibling(parentId: string, pos: Pos): Pos | undefined {\n return this.loadedApi.get_next_sibling(parentId, pos);\n }\n\n get_last_sibling(parentId: string): Pos | undefined {\n return this.loadedApi.get_last_sibling(parentId);\n }\n\n set_child(id: string, node: SerializedChild, allowOverwrite?: boolean): void {\n this.loadedApi.set_child(id, node, allowOverwrite);\n }\n\n move_sibling(id: string, newPos: Pos): void {\n this.loadedApi.move_sibling(id, newPos);\n }\n\n delete_node(id: string): void {\n this.loadedApi.delete_node(id);\n }\n\n delete_child_key(id: string, key: string): void {\n this.loadedApi.delete_child_key(id, key);\n }\n\n set_object_data(\n id: string,\n data: JsonObject,\n allowOverwrite?: boolean\n ): void {\n this.loadedApi.set_object_data(id, data, allowOverwrite);\n }\n\n get_snapshot(lowMemory?: boolean): IReadableSnapshot {\n return this.loadedApi.get_snapshot(lowMemory);\n }\n\n private _loadNodesApi(): NodesAPI {\n // For the in-memory backend, this._nodes IS the \"on-disk\" storage,\n // so we operate on it directly (no separate cache needed).\n const nodes = this._nodes;\n if (!nodes.has(\"root\")) {\n nodes.set(\"root\", { type: CrdtType.OBJECT, data: {} });\n }\n\n const revNodes = buildReverseLookup(nodes);\n\n function get_next_sibling(parentId: string, pos: Pos): Pos | undefined {\n let nextPos: Pos | undefined;\n // Find the smallest position greater than current\n for (const siblingKey of revNodes.keysAt(parentId)) {\n const siblingPos = asPos(siblingKey);\n if (\n siblingPos > pos &&\n (nextPos === undefined || siblingPos < nextPos)\n ) {\n nextPos = siblingPos;\n }\n }\n return nextPos;\n }\n\n function get_last_sibling(parentId: string): Pos | undefined {\n let lastPos: Pos | undefined;\n // Find the largest position under this parent\n for (const siblingKey of revNodes.keysAt(parentId)) {\n const siblingPos = asPos(siblingKey);\n if (lastPos === undefined || siblingPos > lastPos) {\n lastPos = siblingPos;\n }\n }\n return lastPos;\n }\n\n /**\n * Inserts a node in the storage tree, deleting any nodes that already exist\n * under this key (including all of its children), if any.\n */\n function set_child(\n id: string,\n node: SerializedChild,\n allowOverwrite = false\n ): void {\n const parentNode = nodes.get(node.parentId);\n // Reject orphans - parent must exist\n if (parentNode === undefined) {\n throw new Error(`No such parent ${quote(node.parentId)}`);\n }\n\n if (\n node.type === CrdtType.REGISTER &&\n parentNode.type === CrdtType.OBJECT\n ) {\n throw new Error(\"Cannot add register under object\");\n }\n\n const conflictingSiblingId = revNodes.get(node.parentId, node.parentKey);\n if (conflictingSiblingId !== id) {\n // Conflict!\n const parentNode = nodes.get(node.parentId);\n const hasConflictingData =\n parentNode !== undefined &&\n hasStaticDataAt(parentNode, node.parentKey);\n if (conflictingSiblingId !== undefined || hasConflictingData) {\n if (allowOverwrite) {\n delete_child_key(node.parentId, node.parentKey);\n } else {\n throw new Error(`Key ${quote(node.parentKey)} already exists`); // prettier-ignore\n }\n }\n\n // Finally, modify revNodes\n revNodes.set(node.parentId, node.parentKey, id);\n }\n\n nodes.set(id, node);\n }\n\n /**\n * Conceptually this is like \"detaching\" the node from its parent, and\n * \"reattaching\" it at the new position.\n *\n * However, this is a native operation, because doing a naive\n * delete-then-insert would would immediately destroy all (grand)children\n * when it's deleted.\n */\n function move_sibling(id: string, newPos: Pos): void {\n const node = nodes.get(id);\n if (node?.parentId === undefined) {\n return;\n }\n\n // If there is a conflicting sibling at the new position, disallow the move\n if (revNodes.has(node.parentId, newPos))\n throw new Error(`Pos ${quote(newPos)} already taken`); // prettier-ignore\n\n revNodes.delete(node.parentId, node.parentKey);\n const newNode = { ...node, parentKey: newPos };\n nodes.set(id, newNode);\n revNodes.set(node.parentId, newPos, id);\n }\n\n /**\n * Sets some static data on a node. The node must be an OBJECT node, or this\n * method will be a no-op.\n *\n * If any keys exist that also conflict with a child node, then the conflict\n * mode will determine what will happen. By default, an error will be thrown.\n * But if `allowOverwrite` is set to true, the conflicting child node (and\n * its entire subtree) will be deleted to make room for the new static data.\n */\n function set_object_data(\n id: string,\n data: JsonObject,\n allowOverwrite = false\n ): void {\n const node = nodes.get(id);\n if (node?.type !== CrdtType.OBJECT) {\n // Nothing to do\n return;\n }\n\n for (const key of Object.keys(data)) {\n // Handle if conflict!\n const childId = revNodes.get(id, key);\n if (childId !== undefined) {\n if (allowOverwrite) {\n delete_node(childId);\n } else {\n throw new Error(`Child node already exists under ${quote(key)}`); // prettier-ignore\n }\n }\n }\n\n nodes.set(id, { ...node, data: { ...node.data, ...data } });\n }\n\n /**\n * Delete a node from the tree, including all of its children.\n */\n function delete_node(id: string): void {\n const node = nodes.get(id);\n if (node?.parentId === undefined) {\n return;\n }\n\n // Delete the entry in the parent's children administration for this node\n revNodes.delete(node.parentId, node.parentKey);\n\n // Now proceed to deleting the node tree recursively\n const queue = [id];\n while (queue.length > 0) {\n const currid = queue.pop()!;\n queue.push(...revNodes.valuesAt(currid));\n nodes.delete(currid);\n revNodes.deleteAll(currid);\n }\n }\n\n /**\n * Deletes the child key under a given node, whether it's a static object\n * field, or a child node.\n */\n function delete_child_key(id: string, key: string): void {\n // At most one of these will do something, the other is a no-op\n const node = nodes.get(id);\n if (node !== undefined && hasStaticDataAt(node, key)) {\n const { [key]: _, ...rest } = node.data;\n nodes.set(id, { ...node, data: rest });\n }\n\n const childId = revNodes.get(id, key);\n if (childId !== undefined) {\n delete_node(childId);\n }\n }\n\n const api: NodesAPI = {\n /**\n * Return the node with the given id, or undefined if no such node exists.\n * Must always return a valid root node for id=\"root\", even if empty.\n */\n get_node: (id) => nodes.get(id),\n\n /**\n * Yield all nodes as [id, node] pairs. Must always include the root node.\n */\n iter_nodes: () => nodes as NodeStream,\n\n /**\n * Yield each node as a pre-built CompactNode JSON tuple string.\n *\n * This implementation IS the canonical reference for the invariant:\n * iter_nodes_optimized` ≡ `nodeStreamToCompactNodes(iter_nodes())\n */\n *iter_nodes_optimized() {\n for (const compact of nodeStreamToCompactNodes(nodes as NodeStream)) {\n yield JSON.stringify(compact) as jstring<CompactNode>;\n }\n },\n\n /**\n * Return true iff a node with the given id exists. Must return true for \"root\".\n */\n has_node: (id) => nodes.has(id),\n\n /**\n * Return the id of the child node at (parentId, parentKey), or undefined if\n * none. Only checks child nodes registered via set_child, NOT static data\n * keys on OBJECT nodes.\n */\n get_child_at: (id, key) => revNodes.get(id, key),\n\n /**\n * Return true iff a child node exists at (parentId, parentKey). Static data\n * keys on OBJECT nodes do not count—return false for those.\n */\n has_child_at: (id, key) => revNodes.has(id, key),\n\n /**\n * Return the position of the closest sibling \"to the right\" of `pos` under\n * parentId, or undefined if no such sibling exists. The given `pos` may, but\n * does not have to exist already. Positions compare lexicographically.\n */\n get_next_sibling,\n\n /**\n * Return the position of the last (rightmost) child under parentId, or\n * undefined if the node has no children.\n */\n get_last_sibling,\n\n /**\n * Insert a child node with the given id.\n *\n * If allowOverwrite=false (default): throw if a node with this id exists.\n * If allowOverwrite=true: replace any existing node at this id, deleting its\n * entire subtree if it has children.\n */\n set_child,\n\n /**\n * Change a node's parentKey, effectively repositioning the node within its\n * parent. The new position must be free.\n * Throw if another node already occupies (parentId, newPos).\n */\n move_sibling,\n\n /**\n * Delete a node and its entire subtree recursively.\n * Ignore if id=\"root\" (root is immortal).\n */\n delete_node,\n\n /**\n * Delete a key from node `id`. Handle two cases:\n *\n * 1. If id is an OBJECT with `key` in its data: remove that data field.\n * 2. If a child exists at (id, key): delete that child and all its\n * descendants recursively.\n *\n * No-op if neither applies or if the node doesn't exist.\n */\n delete_child_key,\n\n /**\n * Replace the data object of an OBJECT node.\n *\n * If allowOverwrite=false (default): throw if any key in `data` conflicts\n * with an existing child's parentKey.\n * If allowOverwrite=true: first delete any conflicting children (and their\n * entire subtrees), then set the data.\n */\n set_object_data,\n\n /**\n * Return a readable snapshot of the storage tree.\n *\n * @param lowMemory When true, the call site hints that the snapshot should\n * be optimized for lower memory consumption, even if that means slower\n * access.\n */\n get_snapshot(_lowMemory?: boolean): IReadableSnapshot {\n return makeInMemorySnapshot(nodes);\n },\n };\n return api;\n }\n}\n\nexport function makeNewInMemoryDriver(options?: {\n initialActor?: number;\n initialNodes?: Iterable<[string, SerializedCrdt]>;\n}): IStorageDriver {\n return new InMemoryDriver(options);\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\n/**\n * Wraps single-quotes around any string value. Useful for displaying field\n * names or other identifiers in error messages or logs.\n *\n * Examples:\n * quote(\"hi\") // \"'hi'\"\n * quote(\"i'm\") // \"'i'm'\"\n *\n * Note: no \"escaping\" happens here to the string value. This is because this\n * is intended to be used for human consumption, not machine consumption.\n */\nexport function quote(value: string | undefined): string {\n return value !== undefined ? `'${value}'` : \"???\";\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { SerializedChild, SerializedCrdt } from \"@liveblocks/core\";\nimport {\n asPos,\n assertNever,\n CrdtType,\n makePosition,\n OpCode,\n} from \"@liveblocks/core\";\n\nimport type { IStorageDriver } from \"~/interfaces\";\nimport type {\n ClientWireOp,\n CreateOp,\n DeleteCrdtOp,\n DeleteObjectKeyOp,\n FixOp,\n HasOpId,\n SetParentKeyOp,\n UpdateObjectOp,\n} from \"~/protocol\";\nimport type { Pos } from \"~/types\";\n\ntype ApplyOpResult = OpAccepted | OpIgnored;\n\nexport type OpAccepted = {\n action: \"accepted\";\n op: ClientWireOp;\n fix?: FixOp;\n};\n\nexport type OpIgnored = {\n action: \"ignored\";\n ignoredOpId?: string;\n};\n\nfunction accept(op: ClientWireOp, fix?: FixOp): OpAccepted {\n return { action: \"accepted\", op, fix };\n}\n\nfunction ignore(ignoredOp: ClientWireOp): OpIgnored {\n return { action: \"ignored\", ignoredOpId: ignoredOp.opId };\n}\n\nfunction nodeFromCreateChildOp(op: CreateOp): SerializedChild {\n switch (op.type) {\n case OpCode.CREATE_LIST:\n return {\n type: CrdtType.LIST,\n parentId: op.parentId,\n parentKey: op.parentKey,\n };\n\n case OpCode.CREATE_MAP:\n return {\n type: CrdtType.MAP,\n parentId: op.parentId,\n parentKey: op.parentKey,\n };\n\n case OpCode.CREATE_OBJECT:\n return {\n type: CrdtType.OBJECT,\n parentId: op.parentId,\n parentKey: op.parentKey,\n data: op.data,\n };\n\n case OpCode.CREATE_REGISTER:\n return {\n type: CrdtType.REGISTER,\n parentId: op.parentId,\n parentKey: op.parentKey,\n data: op.data,\n };\n\n // istanbul ignore next\n default:\n return assertNever(op, \"Unknown op code\");\n }\n}\n\nexport class Storage {\n // The actual underlying storage API (could be backed by in-memory store,\n // SQLite, Postgres, etc.)\n public readonly driver: IStorageDriver;\n\n constructor(driver: IStorageDriver) {\n this.driver = driver;\n }\n\n // -------------------------------------------------------------------------\n // Public API (for Storage)\n // -------------------------------------------------------------------------\n\n // REFACTOR NOTE: Eventually raw_iter_nodes has to be removed here\n raw_iter_nodes(): Iterable<[string, SerializedCrdt]> {\n return this.driver.raw_iter_nodes();\n }\n\n /**\n * Applies a batch of Ops.\n */\n applyOps(ops: ClientWireOp[]): ApplyOpResult[] {\n const results: ApplyOpResult[] = [];\n for (const op of ops) {\n results.push(this.applyOp(op));\n }\n return results;\n }\n\n // -------------------------------------------------------------------------\n // Private APIs (for Storage)\n // -------------------------------------------------------------------------\n\n /**\n * Applies a single Op.\n */\n private applyOp(op: ClientWireOp): ApplyOpResult {\n switch (op.type) {\n case OpCode.CREATE_LIST:\n case OpCode.CREATE_MAP:\n case OpCode.CREATE_REGISTER:\n case OpCode.CREATE_OBJECT:\n return this.applyCreateOp(op);\n\n case OpCode.UPDATE_OBJECT:\n return this.applyUpdateObjectOp(op);\n\n case OpCode.SET_PARENT_KEY:\n return this.applySetParentKeyOp(op);\n\n case OpCode.DELETE_OBJECT_KEY:\n return this.applyDeleteObjectKeyOp(op);\n\n case OpCode.DELETE_CRDT:\n return this.applyDeleteCrdtOp(op);\n\n // istanbul ignore next\n default:\n if (process.env.NODE_ENV === \"production\") {\n return ignore(op);\n } else {\n return assertNever(op, \"Invalid op\");\n }\n }\n }\n\n private applyCreateOp(op: CreateOp & HasOpId): ApplyOpResult {\n if (this.driver.has_node(op.id)) {\n // Node already exists, the operation is ignored\n return ignore(op);\n }\n\n const node = nodeFromCreateChildOp(op);\n\n const parent = this.driver.get_node(node.parentId);\n if (parent === undefined) {\n // Parent does not exist because the op is invalid or because it was deleted in race condition.\n return ignore(op);\n }\n\n // How to create this node in the node map depends on the parent node's type\n switch (parent.type) {\n case CrdtType.OBJECT:\n // Register children under object nodes are forbidden. We'll simply\n // ignore these Ops. This matches the eventual storage behavior: if\n // we'd persist them, they would get ignored when re-loading the\n // persisted room data into memory the next time the room loads.\n if (op.type === OpCode.CREATE_REGISTER) {\n return ignore(op);\n }\n // fall through\n\n case CrdtType.MAP:\n // Children of maps and objects require no special needs\n this.driver.set_child(op.id, node, true);\n return accept(op);\n\n case CrdtType.LIST:\n // List items need special handling around conflicting resolution,\n // which depends on the users intention\n return this.createChildAsListItem(op, node);\n\n case CrdtType.REGISTER:\n // It's illegal for registers to have children\n return ignore(op);\n\n // istanbul ignore next\n default:\n return assertNever(parent, \"Unhandled CRDT type\");\n }\n }\n\n private createChildAsListItem(\n op: CreateOp & HasOpId,\n node: SerializedChild\n ): ApplyOpResult {\n // The default intent, when not explicitly provided, is to insert, not set,\n // into the list.\n const intent: \"insert\" | \"set\" | \"push\" = op.intent ?? \"insert\";\n\n // istanbul ignore else\n if (intent === \"insert\") {\n // Insert at the client's preferred position, resolving any collision to a\n // nearby free slot.\n return this.acceptAndFix(op, node, this.insertIntoList(op.id, node));\n } else if (intent === \"push\") {\n // Server-authoritative append: place the node after the authoritative\n // end of the list (see `appendToList`), regardless of the client's preference.\n return this.acceptAndFix(op, node, this.appendToList(op.id, node));\n } else if (intent === \"set\") {\n let fix: FixOp | undefined;\n\n // The intent here is to \"set\", not insert, into the list, replacing the\n // existing item that\n\n // Special handling required here. They will include a \"deletedId\" that\n // points to the object they expect to be replacing. If in the mean time,\n // that object disappeared there (because it was moved, for example), be\n // sure to delete it anyway.\n // We should not just trust the given value, because we're about to\n // delete a node. It's only safe to delete the node if it indeed is\n // a sibling of the current node.\n const deletedId =\n op.deletedId !== undefined &&\n op.deletedId !== op.id &&\n this.driver.get_node(op.deletedId)?.parentId === node.parentId\n ? op.deletedId\n : undefined;\n\n if (deletedId !== undefined) {\n this.driver.delete_node(deletedId);\n }\n\n const prevItemId = this.driver.get_child_at(\n node.parentId,\n node.parentKey\n );\n if (prevItemId !== undefined && prevItemId !== deletedId) {\n // If this \"set\" operation indeed removed an item, but it wasn't the\n // expected `deletedId`, let the invoking client know that they'll\n // have to delete this object, too.\n fix = {\n type: OpCode.DELETE_CRDT,\n id: prevItemId,\n };\n }\n\n this.driver.set_child(op.id, node, true);\n\n return accept(op, fix);\n } else {\n return assertNever(intent, \"Invalid intent\");\n }\n }\n\n /**\n * Accept a freshly placed list item. If the server chose a different\n * position in the end (conflict resolution), broadcast only the corrected Op\n * to all clients and send a \"fix\" op back to the originating client.\n */\n private acceptAndFix(\n op: CreateOp & HasOpId,\n node: SerializedChild,\n finalKey: string\n ): ApplyOpResult {\n if (finalKey !== node.parentKey) {\n return accept(\n { ...op, parentKey: finalKey },\n { type: OpCode.SET_PARENT_KEY, id: op.id, parentKey: finalKey }\n );\n }\n return accept(op);\n }\n\n private applyDeleteObjectKeyOp(\n op: DeleteObjectKeyOp & HasOpId\n ): ApplyOpResult {\n this.driver.delete_child_key(op.id, op.key);\n return accept(op);\n }\n\n private applyUpdateObjectOp(op: UpdateObjectOp & HasOpId): ApplyOpResult {\n this.driver.set_object_data(op.id, op.data, true);\n return accept(op);\n }\n\n private applyDeleteCrdtOp(op: DeleteCrdtOp & HasOpId): ApplyOpResult {\n this.driver.delete_node(op.id);\n return accept(op);\n }\n\n private applySetParentKeyOp(op: SetParentKeyOp & HasOpId): ApplyOpResult {\n const newPosition = this.moveToPosInList(op.id, op.parentKey);\n if (newPosition === undefined) {\n // The operation got rejected because it didn't make sense, ignore it\n return ignore(op);\n }\n\n // If the inserted node is different from the input, it means there was\n // a conflict and the node has been inserted in a new, free, list position.\n // We should broadcast a modified Op to all clients that has the modified\n // position, and send a \"fix\" op back to the originating client.\n if (newPosition !== op.parentKey) {\n const modifiedOp = { ...op, parentKey: newPosition };\n const fix: FixOp = {\n type: OpCode.SET_PARENT_KEY,\n id: op.id,\n parentKey: newPosition,\n };\n return accept(modifiedOp, fix);\n } else {\n return accept(op);\n }\n }\n\n /**\n * Inserts a new node in the storage tree, under a list parent. If an\n * existing sibling node already exist under this key, however, it will look\n * for another free position under that parent and insert it under\n * a different parent key that is guaranteed to be available.\n *\n * Returns the key that was used for the insertion.\n */\n private insertIntoList(id: string, node: SerializedChild): string {\n // First, compute the key to use to insert this node\n const key = this.findFreeListPosition(node.parentId, asPos(node.parentKey));\n if (key !== node.parentKey) {\n node = { ...node, parentKey: key };\n }\n this.driver.set_child(id, node);\n return node.parentKey;\n }\n\n /**\n * Server-authoritative append: places the node strictly after every existing\n * sibling under its list parent. If the client's preferred key already sorts\n * after the current last sibling it's kept as-is (guaranteed free, since\n * it's beyond the max); otherwise the node is placed right after the last\n * sibling. Because Ops are processed serially, the chosen key is always\n * free, so concurrent pushes never collide.\n *\n * Returns the final key that was used for the insertion.\n */\n private appendToList(id: string, node: SerializedChild): string {\n const lastPos = this.driver.get_last_sibling(node.parentId);\n const preferredPos = asPos(node.parentKey);\n const finalKey =\n lastPos === undefined || preferredPos > lastPos\n ? preferredPos\n : makePosition(lastPos);\n this.driver.set_child(\n id,\n finalKey !== node.parentKey ? { ...node, parentKey: finalKey } : node\n );\n return finalKey;\n }\n\n /**\n * Tries to move a node to the given position under the same parent. If\n * a conflicting sibling node already exist at this position, it will use\n * another free position instead, to avoid the conflict.\n *\n * Returns the position (parentKey) that the node was eventually placed at.\n * If the node could be inserted without conflict, it will return the same\n * parentKey position.\n *\n * Will return `undefined` if this action could not be interpreted. Will be\n * a no-op for non-list items.\n */\n private moveToPosInList(id: string, targetKey: string): string | undefined {\n const node = this.driver.get_node(id);\n if (node?.parentId === undefined) {\n return; /* reject */\n }\n\n if (this.driver.get_node(node.parentId)?.type !== CrdtType.LIST) {\n // SetParentKeyOp is a no-op for all nodes, except list items\n return; /* reject */\n }\n\n if (node.parentKey === targetKey) {\n // Already there\n return targetKey; /* no-op */\n }\n\n // First, compute the key to use to insert this node\n const key = this.findFreeListPosition(node.parentId, asPos(targetKey));\n if (key !== node.parentKey) {\n this.driver.move_sibling(id, key);\n }\n return key;\n }\n\n /**\n * Checks whether the given parentKey is a \"free position\" under the\n * parentId, i.e. there are no siblings that have the same key. If a sibling\n * exists under that key, it tries to generate new positions until it finds\n * a free slot, and returns that. The returned value is therefore always safe\n * to use as parentKey.\n */\n private findFreeListPosition(parentId: string, parentPos: Pos): Pos {\n if (!this.driver.has_child_at(parentId, parentPos)) {\n return parentPos;\n }\n\n const currPos = parentPos;\n const nextPos = this.driver.get_next_sibling(parentId, currPos);\n if (nextPos !== undefined) {\n return makePosition(currPos, nextPos); // Between current and next\n } else {\n return makePosition(currPos); // After current (fallback)\n }\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport { createHash } from \"node:crypto\";\n\nimport { Base64 } from \"js-base64\";\nimport { nanoid } from \"nanoid\";\nimport * as Y from \"yjs\";\n\nimport type { Guid, YDocId } from \"~/decoders\";\nimport { ROOT_YDOC_ID } from \"~/decoders\";\nimport type { IStorageDriver } from \"~/interfaces\";\nimport type { Logger } from \"~/lib/Logger\";\n\n// How many updates to store before compacting\nconst UPDATE_COUNT_THRESHOLD = 1_000;\n\n// How much smaller the merged update can be than the sum of the updates before compacting\nconst MERGE_SHRINK_THRESHOLD = 0.8;\n\nexport class YjsStorage {\n private readonly driver: IStorageDriver;\n private readonly updateCountThreshold: number;\n\n /**\n * The root Y.Doc instance, which may not have been hydrated from storage\n * yet. Use getRootDoc() instead, which lazily hydrates it on first access —\n * only touch this field directly when the unhydrated instance is fine\n * (e.g. identity checks, subdoc traversal).\n */\n private readonly rawRootDoc: Y.Doc = new Y.Doc();\n private readonly lastSnapshotById = new Map<YDocId, Y.Snapshot>();\n private readonly docsById: Map<YDocId, Y.Doc> = new Map();\n private readonly storedKeysById: Map<YDocId, string[]> = new Map();\n\n constructor(\n driver: IStorageDriver,\n updateCountThreshold: number = UPDATE_COUNT_THRESHOLD\n ) {\n this.driver = driver;\n this.updateCountThreshold = updateCountThreshold;\n this.rawRootDoc.on(\"subdocs\", ({ removed }) => {\n removed.forEach((subdoc: Y.Doc) => {\n subdoc.destroy(); // will remove listeners\n });\n });\n }\n\n // ------------------------------------------------------------------------------------\n // Public API\n // ------------------------------------------------------------------------------------\n\n public getYDoc(logger: Logger, docId: YDocId): Y.Doc {\n if (docId !== ROOT_YDOC_ID) {\n // Subdocs hang off the root doc, so make sure that one is loaded first\n // (otherwise the subdoc lookup below would come up empty)\n this.getRootDoc(logger);\n }\n\n let loaded = this.docsById.get(docId);\n let doc =\n docId === ROOT_YDOC_ID ? this.rawRootDoc : this.findYSubdocByGuid(docId);\n if (!doc) {\n // An API call can load a subdoc without the root doc being loaded, we account for that by just instantiating a doc here.\n doc = new Y.Doc();\n }\n if (loaded === undefined) {\n loaded = this._loadYDocFromDurableStorage(doc, docId);\n this.docsById.set(docId, loaded);\n }\n return loaded;\n }\n\n /**\n * Returns the root Y.Doc, lazily loading it from storage first if it\n * hasn't been loaded yet during this instance's lifetime.\n */\n private getRootDoc(logger: Logger): Y.Doc {\n return this.getYDoc(logger, ROOT_YDOC_ID);\n }\n\n /**\n * If passed a state vector, an update with diff will be returned, if not the entire doc is returned.\n *\n * @param stateVector a base64 encoded target state vector created by running Y.encodeStateVector(Doc) on the client\n * @returns a base64 encoded array of YJS updates\n */\n public getYDocUpdate(\n logger: Logger,\n stateVector: string = \"\",\n guid?: Guid,\n isV2: boolean = false\n ): string | null {\n const update = this.getYDocUpdateBinary(logger, stateVector, guid, isV2);\n if (!update) return null;\n return Base64.fromUint8Array(update);\n }\n\n public getYDocUpdateBinary(\n logger: Logger,\n stateVector: string = \"\",\n guid?: Guid,\n isV2: boolean = false\n ): Uint8Array<ArrayBuffer> | null {\n const doc =\n guid !== undefined\n ? this.getYSubdoc(logger, guid)\n : this.getRootDoc(logger);\n if (!doc) {\n return null;\n }\n let encodedTargetVector;\n try {\n // if given a state vector, attempt to decode it a single diffed update\n encodedTargetVector =\n stateVector.length > 0 ? Base64.toUint8Array(stateVector) : undefined;\n } catch {\n logger.warn(\n \"Could not get update from passed vector, returning all updates\"\n );\n }\n if (isV2) {\n return Y.encodeStateAsUpdateV2(doc, encodedTargetVector);\n }\n return Y.encodeStateAsUpdate(doc, encodedTargetVector);\n }\n\n public getYStateVector(logger: Logger, guid?: Guid): string | null {\n const doc =\n guid !== undefined\n ? this.getYSubdoc(logger, guid)\n : this.getRootDoc(logger);\n if (!doc) {\n return null;\n }\n return Base64.fromUint8Array(Y.encodeStateVector(doc));\n }\n\n public getSnapshotHash(\n logger: Logger,\n options: { guid?: Guid; isV2?: boolean }\n ): string | null {\n const doc =\n options.guid !== undefined\n ? this.getYSubdoc(logger, options.guid)\n : this.getRootDoc(logger);\n if (!doc) {\n return null;\n }\n const snapshot = this._getOrPutLastSnapshot(doc);\n return this.calculateSnapshotHash(snapshot, { isV2: options.isV2 });\n }\n\n /**\n * @param update base64 encoded uint8array\n * @returns { isUpdated: boolean; snapshotHash: string }\n * isUpdated: true if the update had an effect on the YDoc\n * snapshotHash: the hash of the new snapshot\n */\n public addYDocUpdate(\n logger: Logger,\n update: string | Uint8Array,\n guid?: Guid,\n isV2?: boolean\n ): { isUpdated: boolean; snapshotHash: string } {\n const doc =\n guid !== undefined\n ? this.getYSubdoc(logger, guid)\n : this.getRootDoc(logger);\n if (!doc) {\n throw new Error(`YDoc with guid ${guid} not found`);\n }\n\n try {\n // takes a snapshot if none is stored in memory - NOTE: snapshots are a combination of statevector + deleteset, not a full doc\n const beforeSnapshot = this._getOrPutLastSnapshot(doc);\n const updateAsU8 =\n typeof update === \"string\" ? Base64.toUint8Array(update) : update;\n const applyUpdate = isV2 ? Y.applyUpdateV2 : Y.applyUpdate;\n applyUpdate(doc, updateAsU8, \"client\");\n // put the new \"after update\" snapshot\n const afterSnapshot = this._putLastSnapshot(doc);\n // Check the snapshot before/after to see if the update had an effect\n const updated = !Y.equalSnapshots(beforeSnapshot, afterSnapshot);\n if (updated) {\n this.handleYDocUpdate(doc, updateAsU8, isV2);\n }\n\n return {\n isUpdated: updated,\n snapshotHash: this.calculateSnapshotHash(afterSnapshot, { isV2 }),\n };\n } catch (e) {\n // The only reason this would happen is if a user would send bad data\n logger.warn(`Ignored bad YDoc update: ${String(e)}`);\n throw new Error(\n \"Bad YDoc update. Data is corrupted, or data does not match the encoding.\"\n );\n }\n }\n\n // ------------------------------------------------------------------------------------\n // Private APIs\n // ------------------------------------------------------------------------------------\n\n // NOTE: We could instead store the hash of snapshot instead of the whole snapshot to optimize memory usage.\n private _getOrPutLastSnapshot(doc: Y.Doc): Y.Snapshot {\n const docId: YDocId =\n doc.guid === this.rawRootDoc.guid ? ROOT_YDOC_ID : (doc.guid as Guid);\n const snapshot = this.lastSnapshotById.get(docId);\n if (snapshot) {\n return snapshot;\n }\n return this._putLastSnapshot(doc);\n }\n\n // NOTE: We could instead store the hash of snapshot instead of the whole snapshot to optimize memory usage.\n private _putLastSnapshot(doc: Y.Doc): Y.Snapshot {\n const docId: YDocId =\n doc.guid === this.rawRootDoc.guid ? ROOT_YDOC_ID : (doc.guid as Guid);\n const snapshot = Y.snapshot(doc);\n this.lastSnapshotById.set(docId, snapshot);\n return snapshot;\n }\n\n // compact the updates into a single update and write it to the durable storage\n private _compactYJSUpdates = (\n doc: Y.Doc,\n docId: YDocId,\n storedKeys: string[]\n ): void => {\n const compactedUpdate = Y.encodeStateAsUpdate(doc);\n const newKey = nanoid();\n this.driver.write_y_updates(docId, newKey, compactedUpdate);\n // Todo: after we kill the kv driver, we should have an overwrite method in the driverso we don't need to delete and write\n this.driver.delete_y_updates(docId, storedKeys);\n this.storedKeysById.set(docId, [newKey]);\n };\n\n private _loadYDocFromDurableStorage = (doc: Y.Doc, docId: YDocId): Y.Doc => {\n const docUpdates = Object.fromEntries(this.driver.iter_y_updates(docId));\n const updates = Object.values(docUpdates);\n const beforeSize = updates.reduce((acc, update) => acc + update.length, 0);\n const newupdate = Y.mergeUpdates(updates);\n const storedKeys = Object.keys(docUpdates);\n Y.applyUpdate(doc, newupdate);\n // after compaction, there will only be one unique key.\n if (\n this.shouldCompactByKeyCount(storedKeys) ||\n this.shouldCompactBySize(beforeSize, newupdate.length)\n ) {\n this._compactYJSUpdates(doc, docId, storedKeys);\n } else {\n this.storedKeysById.set(docId, storedKeys);\n }\n doc.emit(\"load\", [doc]); // sets the \"isLoaded\" to true on the doc\n\n return doc;\n };\n\n private findYSubdocByGuid(guid: Guid): Y.Doc | null {\n for (const subdoc of this.rawRootDoc.getSubdocs()) {\n if (subdoc.guid === guid) {\n return subdoc;\n }\n }\n return null;\n }\n\n private calculateSnapshotHash(\n snapshot: Y.Snapshot,\n { isV2 }: { isV2?: boolean }\n ): string {\n const encodedSnapshot = isV2\n ? Y.encodeSnapshotV2(snapshot)\n : Y.encodeSnapshot(snapshot);\n return createHash(\"sha256\").update(encodedSnapshot).digest(\"base64\");\n }\n\n // gets a subdoc, it will be loaded if not already loaded\n private getYSubdoc(logger: Logger, guid: Guid): Y.Doc | null {\n // Subdocs hang off the root doc, so make sure that one is loaded first\n this.getRootDoc(logger);\n\n const subdoc = this.findYSubdocByGuid(guid);\n if (!subdoc) {\n return null;\n }\n this.getYDoc(logger, guid);\n return subdoc;\n }\n\n // When the YJS doc changes, update it in durable storage\n private handleYDocUpdate(\n doc: Y.Doc,\n update: Uint8Array,\n isV2: boolean | undefined\n ): void {\n // Todo: in the future, we should pass this detail to the driver so it can store the version as metadata\n // this will be easy for sqlite drivers, but not for the KV driver\n const v1update = isV2 ? Y.convertUpdateFormatV2ToV1(update) : update;\n const docId: YDocId =\n doc.guid === this.rawRootDoc.guid ? ROOT_YDOC_ID : (doc.guid as Guid);\n\n const storedKeys = this.storedKeysById.get(docId);\n\n // Every UPDATE_COUNT_THRESHOLD updates, we compact the updates\n if (this.shouldCompactByKeyCount(storedKeys)) {\n this._compactYJSUpdates(doc, docId, storedKeys || []);\n return;\n }\n\n // the whole concept of storing keys is not needed when we kill the kv driver, all of this stuff is trivial in sqlite\n const newKey = nanoid();\n this.driver.write_y_updates(docId, newKey, v1update);\n\n // update the stored keys, which we'll need for compaction.\n if (!storedKeys) {\n this.storedKeysById.set(docId, [newKey]);\n } else {\n storedKeys.push(newKey);\n }\n }\n\n private shouldCompactBySize(beforeSize: number, afterSize: number): boolean {\n return beforeSize * MERGE_SHRINK_THRESHOLD > afterSize;\n }\n\n private shouldCompactByKeyCount(storedKeys: string[] | undefined): boolean {\n if (!storedKeys) {\n return false;\n }\n return storedKeys.length >= this.updateCountThreshold;\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\n/**\n * Given a promise or promise factory, returns a 2-tuple of success or failure.\n * This pattern avoids having to build deeply nested try / catch clauses, where\n * success variables need to be defined as a `let` outside of the `try` block.\n *\n * Turns:\n *\n * let result;\n * try {\n * result = await doSomething();\n * } catch (error) {\n * // do something with error\n * }\n *\n * doAnotherThing(result);\n *\n * Into:\n *\n * const [result, error] = await tryCatch(doSomething());\n * if (error) {\n * // do something with error\n * }\n * doAnotherThing(result);\n *\n * When given a sync callback, the tuple is returned synchronously (no await\n * needed):\n *\n * const [result, error] = tryCatch(() => doSomethingSync());\n *\n */\nfunction isThenable<T>(value: T | PromiseLike<T>): value is PromiseLike<T> {\n return (\n value !== null &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n typeof (value as { then?: unknown }).then === \"function\"\n );\n}\n\nexport function tryCatch<T, E = Error>(\n promise: PromiseLike<T> | (() => PromiseLike<T>)\n): Promise<[T, undefined] | [undefined, E]>;\nexport function tryCatch<T, E = Error>(\n fn: () => T\n): [T, undefined] | [undefined, E];\nexport function tryCatch<T, E = Error>(\n promise: PromiseLike<T> | (() => PromiseLike<T>) | (() => T)\n): Promise<[T, undefined] | [undefined, E]> | [T, undefined] | [undefined, E] {\n try {\n const result = typeof promise === \"function\" ? promise() : promise;\n if (isThenable(result)) {\n return Promise.resolve(result).then(\n (data: T): [T, undefined] => [data, undefined],\n (error: unknown): [undefined, E] => [undefined, error as E]\n );\n }\n return [result, undefined];\n } catch (error) {\n return [undefined, error as E];\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\n/**\n * Like ES6 map, but also provides a unique reverse lookup index for values\n * stored in the map.\n *\n * Useful for code like:\n *\n * // Store a list of persons by their IDs, but each person's email must also\n * // be unique\n * const map = new UniqueMap((person) => person.email);\n * map.set(1, { name: 'John Doe', email: 'john@example.org' });\n * map.set(2, { name: 'John Foo', email: 'john@example.org' }); // Will error!\n * map.delete(1);\n * map.set(3, { name: 'Johnny', email: 'john@example.org' }); // Now it's allowed\n *\n * map.getReverseKey('john@example.org') // 3\n * map.getReverse('john@example.org') // { name: 'Johnny', email: 'john@example.org' }\n *\n */\nexport class UniqueMap<K, V, UK> extends Map<K, V> {\n // / \\\n // Primary key Unique key\n #_revMap: Map<UK, K>;\n #_keyFn: (value: V) => UK;\n\n constructor(\n keyFn: (value: V) => UK\n // entries?: readonly (readonly [K, V])[] | null\n ) {\n super(); // super(entries)\n this.#_keyFn = keyFn;\n this.#_revMap = new Map();\n }\n\n lookupPrimaryKey(uniqKey: UK): K | undefined {\n return this.#_revMap.get(uniqKey);\n }\n\n lookup(uniqKey: UK): V | undefined {\n const key = this.#_revMap.get(uniqKey);\n return key !== undefined ? this.get(key) : undefined;\n }\n\n set(key: K, value: V): this {\n const uniqKey = this.#_keyFn(value);\n const primaryKey = this.#_revMap.get(uniqKey);\n if (primaryKey !== undefined && primaryKey !== key) {\n throw new Error(`Unique key ${String(uniqKey)} already exists`);\n }\n this.#_revMap.set(uniqKey, key);\n return super.set(key, value);\n }\n\n delete(primaryKey: K): boolean {\n const value = this.get(primaryKey);\n if (value !== undefined) {\n const indexedKey = this.#_keyFn(value);\n this.#_revMap.delete(indexedKey);\n }\n return super.delete(primaryKey);\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { BaseUserMeta, JsonObject } from \"@liveblocks/core\";\nimport { ServerMsgCode } from \"@liveblocks/core\";\n\nimport type { RoomStateServerMsg } from \"~/protocol\";\nimport type { LeasedSession } from \"~/types\";\n\n/**\n * Concatenates multiple Uint8Arrays into a single Uint8Array.\n */\nexport function concatUint8Arrays(\n arrays: Uint8Array[]\n): Uint8Array<ArrayBuffer> {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n\nexport function makeRoomStateMsg(\n actor: number,\n nonce: string,\n scopes: string[],\n users: Record<number, BaseUserMeta & { scopes: string[] }>,\n publicMeta?: JsonObject\n): RoomStateServerMsg<BaseUserMeta> {\n return {\n type: ServerMsgCode.ROOM_STATE,\n actor,\n nonce,\n scopes,\n users,\n meta: publicMeta ?? {},\n };\n}\n\n/**\n * Check if a leased session is expired.\n * Returns true if the current time is greater than or equal to updatedAt + ttl.\n */\nexport function isLeasedSessionExpired(leasedSession: LeasedSession): boolean {\n const now = Date.now();\n return now >= leasedSession.updatedAt + leasedSession.ttl;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/liveblocks-backend/liveblocks-backend/shared/liveblocks-server/dist/index.cjs","../src/decoders/ClientMsg.ts","../src/protocol/feedMessages.ts","../src/protocol/feedErrors.ts","../src/protocol/ProtocolVersion.ts","../src/decoders/feedMetadata.ts","../src/decoders/jsonYolo.ts","../src/decoders/Op.ts","../src/decoders/y-types.ts","../src/formats/LossyJson.ts","../src/formats/NodeStream.ts","../src/formats/PlainLson.ts","../src/makeInMemorySnapshot.ts","../src/lib/DefaultMap.ts","../src/lib/NestedMap.ts","../src/MetadataDB.ts","../src/Room.ts","../src/lib/Logger.ts","../src/plugins/InMemoryDriver.ts","../src/lib/text.ts","../src/Storage.ts","../src/YjsStorage.ts","../src/lib/tryCatch.ts","../src/lib/UniqueMap.ts","../src/utils.ts"],"names":["ProtocolVersion","string","optional","object","constant","array","taggedUnion","snapshot","CrdtType","buildObject","buildNode","buildList","buildMap","emitObject","emit","emitList","emitMap","raise","LogLevel","target","nn","isRootStorageNode","asPos","op","OpCode","intent","applyUpdate","ServerMsgCode","ClientMsgCode"],"mappings":"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CX,YAAA;AACC,YAAA;AAEhB,UAAA;AACH,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAEK,MAAA;AACH,QAAA;AACA,QAAA;AAA6B,UAAA;AAE7B,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACE,UAAA;AAAA,YAAA;AACE,cAAA;AACY,cAAA;AACW,YAAA;AAEzB,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAEK,MAAA;AACH,QAAA;AACA,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAEK,MAAA;AACH,QAAA;AACA,QAAA;AAAgC,UAAA;AACK,YAAA;AACpB,YAAA;AAEd,UAAA;AACH,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAEK,MAAA;AACH,QAAA;AACA,QAAA;AAAgC,UAAA;AACzB,YAAA;AACO,YAAA;AACA,YAAA;AAEZ,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAEK,MAAA;AACH,QAAA;AACA,QAAA;AAAyB,UAAA;AAEzB,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAEA,MAAA;AACE,QAAA;AACE,UAAA;AACF,QAAA;AAEA,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACF;AA36CE;AAsDS;AhByzFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/liveblocks-backend/liveblocks-backend/shared/liveblocks-server/dist/index.cjs","sourcesContent":[null,"/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { Json, JsonObject } from \"@liveblocks/core\";\nimport { ClientMsgCode } from \"@liveblocks/core\";\nimport type { Decoder } from \"decoders\";\nimport {\n array,\n boolean,\n constant,\n jsonObject,\n nonEmptyString,\n number,\n object,\n optional,\n string,\n taggedUnion,\n} from \"decoders\";\n\nimport type {\n AddFeedClientMsg,\n AddFeedMessageClientMsg,\n BroadcastEventClientMsg,\n ClientMsg,\n DeleteFeedClientMsg,\n DeleteFeedMessageClientMsg,\n FetchFeedMessagesClientMsg,\n FetchFeedsClientMsg,\n FetchStorageClientMsg,\n FetchYDocClientMsg,\n UpdateFeedClientMsg,\n UpdateFeedMessageClientMsg,\n UpdatePresenceClientMsg,\n UpdateStorageClientMsg,\n UpdateYDocClientMsg,\n} from \"~/protocol\";\nimport { FeedMsgCode } from \"~/protocol\";\n\nimport {\n feedMetadataUpdateDecoder,\n fetchFeedsMetadataFilterDecoder,\n optionalFeedMetadataDecoder,\n} from \"./feedMetadata\";\nimport { jsonObjectYolo, jsonYolo } from \"./jsonYolo\";\nimport { op } from \"./Op\";\nimport type { YUpdate, YVector } from \"./y-types\";\nimport { guidDecoder } from \"./y-types\";\n\nconst updatePresenceClientMsg: Decoder<UpdatePresenceClientMsg<JsonObject>> =\n object({\n type: constant(ClientMsgCode.UPDATE_PRESENCE),\n data: jsonObjectYolo,\n targetActor: optional(number),\n });\n\nconst broadcastEventClientMsg: Decoder<BroadcastEventClientMsg<Json>> = object({\n type: constant(ClientMsgCode.BROADCAST_EVENT),\n event: jsonYolo,\n});\n\nconst fetchStorageClientMsg: Decoder<FetchStorageClientMsg> = object({\n type: constant(ClientMsgCode.FETCH_STORAGE),\n});\n\nconst updateStorageClientMsg: Decoder<UpdateStorageClientMsg> = object({\n type: constant(ClientMsgCode.UPDATE_STORAGE),\n ops: array(op),\n});\n\nconst fetchYDocClientMsg: Decoder<FetchYDocClientMsg> = object({\n type: constant(ClientMsgCode.FETCH_YDOC),\n vector: string.refineType<YVector>(),\n guid: optional(guidDecoder), // Don't specify to update the root doc\n v2: optional(boolean),\n});\n\nconst updateYDocClientMsg: Decoder<UpdateYDocClientMsg> = object({\n type: constant(ClientMsgCode.UPDATE_YDOC),\n update: string.refineType<YUpdate>(),\n guid: optional(guidDecoder), // Don't specify to update the root doc\n v2: optional(boolean),\n});\n\n// Feed message decoders\nconst fetchFeedsClientMsg: Decoder<FetchFeedsClientMsg> = object({\n type: constant(FeedMsgCode.FETCH_FEEDS),\n requestId: string,\n cursor: optional(string),\n since: optional(number),\n limit: optional(number),\n metadata: fetchFeedsMetadataFilterDecoder,\n});\n\nconst fetchFeedMessagesClientMsg: Decoder<FetchFeedMessagesClientMsg> = object({\n type: constant(FeedMsgCode.FETCH_FEED_MESSAGES),\n requestId: string,\n feedId: nonEmptyString,\n cursor: optional(string),\n since: optional(number),\n limit: optional(number),\n});\n\nconst addFeedClientMsg: Decoder<AddFeedClientMsg> = object({\n type: constant(FeedMsgCode.ADD_FEED),\n feedId: string,\n metadata: optionalFeedMetadataDecoder,\n timestamp: optional(number),\n requestId: optional(string),\n});\n\nconst updateFeedClientMsg: Decoder<UpdateFeedClientMsg> = object({\n type: constant(FeedMsgCode.UPDATE_FEED),\n feedId: string,\n metadata: feedMetadataUpdateDecoder,\n requestId: optional(string),\n});\n\nconst deleteFeedClientMsg: Decoder<DeleteFeedClientMsg> = object({\n type: constant(FeedMsgCode.DELETE_FEED),\n feedId: string,\n requestId: optional(string),\n});\n\nconst addFeedMessageClientMsg: Decoder<AddFeedMessageClientMsg> = object({\n type: constant(FeedMsgCode.ADD_FEED_MESSAGE),\n feedId: string,\n data: jsonObject,\n id: optional(string),\n timestamp: optional(number),\n requestId: optional(string),\n});\n\nconst updateFeedMessageClientMsg: Decoder<UpdateFeedMessageClientMsg> = object({\n type: constant(FeedMsgCode.UPDATE_FEED_MESSAGE),\n feedId: string,\n messageId: string,\n data: jsonObject,\n timestamp: optional(number),\n requestId: optional(string),\n});\n\nconst deleteFeedMessageClientMsg: Decoder<DeleteFeedMessageClientMsg> = object({\n type: constant(FeedMsgCode.DELETE_FEED_MESSAGE),\n feedId: string,\n messageId: string,\n requestId: optional(string),\n});\n\nexport const clientMsgDecoder: Decoder<ClientMsg<JsonObject, Json>> =\n taggedUnion(\"type\", {\n [ClientMsgCode.UPDATE_PRESENCE]: updatePresenceClientMsg,\n [ClientMsgCode.BROADCAST_EVENT]: broadcastEventClientMsg,\n [ClientMsgCode.FETCH_STORAGE]: fetchStorageClientMsg,\n [ClientMsgCode.UPDATE_STORAGE]: updateStorageClientMsg,\n [ClientMsgCode.FETCH_YDOC]: fetchYDocClientMsg,\n [ClientMsgCode.UPDATE_YDOC]: updateYDocClientMsg,\n [FeedMsgCode.FETCH_FEEDS]: fetchFeedsClientMsg,\n [FeedMsgCode.FETCH_FEED_MESSAGES]: fetchFeedMessagesClientMsg,\n [FeedMsgCode.ADD_FEED]: addFeedClientMsg,\n [FeedMsgCode.UPDATE_FEED]: updateFeedClientMsg,\n [FeedMsgCode.DELETE_FEED]: deleteFeedClientMsg,\n [FeedMsgCode.ADD_FEED_MESSAGE]: addFeedMessageClientMsg,\n [FeedMsgCode.UPDATE_FEED_MESSAGE]: updateFeedMessageClientMsg,\n [FeedMsgCode.DELETE_FEED_MESSAGE]: deleteFeedMessageClientMsg,\n } as unknown as Record<\n string | number,\n Decoder<ClientMsg<JsonObject, Json>>\n >).describe(\"Must be a valid client message\");\n\nexport const transientClientMsgDecoder: Decoder<ClientMsg<JsonObject, Json>> =\n taggedUnion(\"type\", {\n // [ClientMsgCode.UPDATE_PRESENCE]: updatePresenceClientMsg,\n // [ClientMsgCode.BROADCAST_EVENT]: broadcastEventClientMsg,\n // [ClientMsgCode.FETCH_STORAGE]: fetchStorageClientMsg,\n [ClientMsgCode.UPDATE_STORAGE]: updateStorageClientMsg,\n // [ClientMsgCode.FETCH_YDOC]: fetchYDocClientMsg,\n // [ClientMsgCode.UPDATE_YDOC]: updateYDocClientMsg,\n }).describe(\"Must be a valid transient client message\");\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\n/**\n * NOTE: this will be moved to @liveblocks/core in the future.\n * Feed WebSocket message codes.\n * 50x = server → client, 51x = client → server.\n */\nexport const FeedMsgCode = {\n // Server → client (50x)\n FEEDS_LIST: 500,\n FEEDS_ADDED: 501,\n FEEDS_UPDATED: 502,\n FEED_DELETED: 503,\n FEED_MESSAGES_LIST: 504,\n FEED_MESSAGES_ADDED: 505,\n FEED_MESSAGES_UPDATED: 506,\n FEED_MESSAGES_DELETED: 507,\n FEED_REQUEST_FAILED: 508,\n // Client → server (51x)\n FETCH_FEEDS: 510,\n FETCH_FEED_MESSAGES: 511,\n ADD_FEED: 512,\n UPDATE_FEED: 513,\n DELETE_FEED: 514,\n ADD_FEED_MESSAGE: 515,\n UPDATE_FEED_MESSAGE: 516,\n DELETE_FEED_MESSAGE: 517,\n} as const;\n\n/** Error codes for {@link FeedRequestFailedServerMsg}. */\nexport const FeedRequestErrorCode = {\n INTERNAL: \"INTERNAL\",\n FEED_ALREADY_EXISTS: \"FEED_ALREADY_EXISTS\",\n FEED_NOT_FOUND: \"FEED_NOT_FOUND\",\n FEED_MESSAGE_NOT_FOUND: \"FEED_MESSAGE_NOT_FOUND\",\n} as const;\n\nimport type { JsonObject } from \"@liveblocks/core\";\n\nimport type { Feed, FeedMessage } from \"../types\";\n\n// ─── Server messages (50x) ───────────────────────────────────────────────────\n\nexport type FeedsListServerMsg = {\n type: 500;\n requestId: string;\n feeds: Feed[];\n nextCursor?: string;\n};\n\nexport type FeedsAddedServerMsg = {\n type: 501;\n feeds: Feed[];\n};\n\nexport type FeedsUpdatedServerMsg = {\n type: 502;\n feeds: Feed[];\n};\n\nexport type FeedDeletedServerMsg = {\n type: 503;\n feedId: string;\n};\n\nexport type FeedRequestFailedServerMsg = {\n type: 508;\n requestId: string;\n code: string;\n reason?: string;\n};\n\nexport type FeedsServerMsg =\n | FeedsListServerMsg\n | FeedsAddedServerMsg\n | FeedsUpdatedServerMsg\n | FeedDeletedServerMsg\n | FeedRequestFailedServerMsg;\n\nexport type FeedMessagesListServerMsg = {\n type: 504;\n requestId: string;\n feedId: string;\n messages: FeedMessage[];\n nextCursor?: string;\n};\n\nexport type FeedMessagesAddedServerMsg = {\n type: 505;\n feedId: string;\n messages: FeedMessage[];\n};\n\nexport type FeedMessagesUpdatedServerMsg = {\n type: 506;\n feedId: string;\n messages: FeedMessage[];\n};\n\nexport type FeedMessagesDeletedServerMsg = {\n type: 507;\n feedId: string;\n messageIds: string[];\n};\n\nexport type FeedMessagesServerMsg =\n | FeedMessagesListServerMsg\n | FeedMessagesAddedServerMsg\n | FeedMessagesUpdatedServerMsg\n | FeedMessagesDeletedServerMsg\n | FeedRequestFailedServerMsg;\n\n// ─── Client messages (51x) ───────────────────────────────────────────────────\n\nexport type FetchFeedsClientMsg = {\n type: 510;\n requestId: string;\n cursor?: string;\n since?: number;\n limit?: number;\n /** Match feeds whose metadata contains these keys (same value rules as room metadata updates). */\n metadata?: Record<string, string | string[] | null>;\n};\n\nexport type FetchFeedMessagesClientMsg = {\n type: 511;\n requestId: string;\n feedId: string;\n cursor?: string;\n since?: number;\n limit?: number;\n};\n\nexport type AddFeedClientMsg = {\n type: 512;\n feedId: string;\n metadata?: Record<string, string[] | string>;\n timestamp?: number;\n requestId?: string;\n};\n\nexport type UpdateFeedClientMsg = {\n type: 513;\n feedId: string;\n metadata: Record<string, string[] | string | null>;\n requestId?: string;\n};\n\nexport type DeleteFeedClientMsg = {\n type: 514;\n feedId: string;\n requestId?: string;\n};\n\nexport type AddFeedMessageClientMsg = {\n type: 515;\n feedId: string;\n data: JsonObject;\n id?: string;\n timestamp?: number;\n requestId?: string;\n};\n\nexport type UpdateFeedMessageClientMsg = {\n type: 516;\n feedId: string;\n messageId: string;\n data: JsonObject;\n timestamp?: number;\n requestId?: string;\n};\n\nexport type DeleteFeedMessageClientMsg = {\n type: 517;\n feedId: string;\n messageId: string;\n requestId?: string;\n};\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport {\n FeedMsgCode,\n FeedRequestErrorCode,\n type FeedRequestFailedServerMsg,\n} from \"./feedMessages\";\n\nexport function feedRequestFailed(\n requestId: string | undefined,\n code: string,\n reason?: string\n): FeedRequestFailedServerMsg {\n return {\n type: FeedMsgCode.FEED_REQUEST_FAILED,\n requestId: requestId ?? \"\",\n code,\n reason,\n };\n}\n\n/**\n * Maps driver / Room errors to stable {@link FeedRequestFailedServerMsg} codes.\n * Prefer matching known {@link Error#message} shapes storage drivers.\n */\nexport function mapFeedError(err: unknown): { code: string; reason?: string } {\n if (!(err instanceof Error)) {\n return { code: FeedRequestErrorCode.INTERNAL };\n }\n\n const m = err.message;\n\n if (m.includes(\"already exists\")) {\n return { code: FeedRequestErrorCode.FEED_ALREADY_EXISTS, reason: m };\n }\n\n if (m.includes(\"Feed message\") && m.includes(\"not found\")) {\n return { code: FeedRequestErrorCode.FEED_MESSAGE_NOT_FOUND, reason: m };\n }\n\n if (m.includes(\"not found\")) {\n return { code: FeedRequestErrorCode.FEED_NOT_FOUND, reason: m };\n }\n\n return { code: FeedRequestErrorCode.INTERNAL, reason: m };\n}\n\n/** Maps an arbitrary thrown value to a {@link FeedRequestFailedServerMsg}. */\nexport function feedFailureServerMsg(\n requestId: string | undefined,\n err: unknown\n): FeedRequestFailedServerMsg {\n const mapped = mapFeedError(err);\n return feedRequestFailed(requestId, mapped.code, mapped.reason);\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport { enum_ } from \"decoders\";\n\nexport enum ProtocolVersion {\n //\n // NOTE:\n // The following versions were once used, but there is no usage of it anymore\n // in the wild, so we've removed support for them:\n //\n // V1 - Initial version\n // V2 - ?\n // V3 - started to broadcast storage operations to the sender to fix some\n // conflicts\n // V4 - created a virtual root to fix an issue where multiple people\n // initialize the storage at the same time\n // V5 - started to broadcast messages in a batch (arrays) for clients\n // V6 - started to validate inputs with decoders.\n //\n\n /**\n * V7 changes the URL params used to authorize the user.\n *\n * In V6 and lower, the ?token= URL param is used, which will only ever\n * contain a `pub-legacy` or `sec-legacy` token.\n *\n * URL PARAM CHANGES:\n * Starting with V7, the ?token= is no longer a legal URL param. Instead,\n * either of the following params is used:\n *\n * - ?tok=... for ID tokens\n * - ?tok=... for Access tokens\n * - ?tok=... for Secret Legacy tokens\n * - ?pubkey=... for public keys (no token, public key can be directly used here)\n *\n * Note that `pub-legacy` tokens are no longer accepted in V7, and are\n * replaced by the direct use of the public key.\n *\n * BEHAVIORAL CHANGES:\n * Starting with V7, the RoomState server message that gets sent when\n * a client initially connects will now include new fields:\n *\n * - `actor`\n * - `scopes`\n *\n * Since v1.2.0 (Jul 31, 2023)\n */\n V7 = 7,\n\n /**\n * V8 changes storage response format and allows streaming.\n *\n * MESSAGE FORMAT CHANGES:\n * - V8: sends 1+ STORAGE_CHUNK messages, followed by 1 final\n * STORAGE_STREAM_END message (with compact nodes)\n * - V7: sends 1 STORAGE_STATE_V7 message (with full nodes)\n *\n * STREAMING BEHAVIOR in V8:\n * - For SQLite-backed rooms: nodes are split into multiple STORAGE_CHUNK\n * messages, followed by STORAGE_STREAM_END\n * - For KV-backed rooms: all nodes are sent in a single STORAGE_CHUNK\n * message that will contain all nodes, followed by STORAGE_STREAM_END\n *\n * Since 3.14.0\n */\n V8 = 8,\n}\n\nexport const protocolVersionDecoder = enum_(ProtocolVersion).describe(\n \"Unsupported protocol version\"\n);\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\n/**\n * Feed metadata decoders — same rules as room/thread metadata (see @shared/common\n * createMetadataDecoder / updateRoomMetadataDecoder).\n */\n\nimport type { Decoder } from \"decoders\";\nimport {\n array,\n either,\n identifier,\n nullable,\n optional,\n poja,\n record,\n select,\n sized,\n string,\n} from \"decoders\";\n\n/** Same cap as createMetadataDecoder in @shared/common */\nconst MAX_METADATA_COUNT = 50;\nconst MAX_METADATA_VALUE_LIST_LENGTH = 50;\n\nexport const feedMetadataIdDecoder = sized(identifier, { min: 1, max: 40 });\n\nconst metadataStringValue = sized(string, { max: 256 });\n\nfunction createRoomMetadataValueDecoder(): Decoder<string | string[]> {\n return select(\n either(string, poja).describe(\"Must be string or string[]\"),\n (x) =>\n typeof x === \"string\"\n ? metadataStringValue\n : array(metadataStringValue).refine(\n (value) => value.length <= MAX_METADATA_VALUE_LIST_LENGTH,\n `Must be at most ${MAX_METADATA_VALUE_LIST_LENGTH} items`\n )\n );\n}\n\nconst roomMetadataValueDecoder = createRoomMetadataValueDecoder();\n\n/**\n * A single metadata value: string, string[], or null (null clears a key on update).\n */\nconst feedMetadataNullableValueDecoder = nullable(roomMetadataValueDecoder);\n\nconst feedMetadataRecordForCreate = record(\n feedMetadataIdDecoder,\n roomMetadataValueDecoder\n).refine(\n (value) => Object.keys(value).length <= MAX_METADATA_COUNT,\n `Must be at most ${MAX_METADATA_COUNT} items`\n);\n\n/**\n * Optional feed metadata on create (WebSocket ADD_FEED, HTTP POST …/feeds).\n * Same rules as createMetadataDecoder / room metadata on create (no null values).\n */\nexport const optionalFeedMetadataDecoder = optional(\n feedMetadataRecordForCreate\n);\n\n/**\n * Full metadata object for update (WebSocket UPDATE_FEED, HTTP PATCH …/feeds/:id).\n * Same shape as updateRoomMetadataDecoder.\n */\nexport const feedMetadataUpdateDecoder = record(\n feedMetadataIdDecoder,\n feedMetadataNullableValueDecoder\n);\n\nconst feedMetadataRecordForFilter = record(\n feedMetadataIdDecoder,\n feedMetadataNullableValueDecoder\n).refine(\n (value) => Object.keys(value).length <= MAX_METADATA_COUNT,\n `Must be at most ${MAX_METADATA_COUNT} items`\n);\n\n/**\n * Optional metadata filter for FETCH_FEEDS — same key/value rules as\n * {@link feedMetadataUpdateDecoder} / updateRoomMetadataDecoder in @shared/common.\n */\nexport const fetchFeedsMetadataFilterDecoder = optional(\n feedMetadataRecordForFilter\n);\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { Json, JsonObject } from \"@liveblocks/core\";\nimport type { Decoder } from \"decoders\";\nimport { unknown } from \"decoders\";\n\n/**\n * Drop-in replacement for the `json` decoder from the decoders standard\n * library, but implemented as a no-op. This is, of course, only safe to use in\n * contexts where you know that the input already is valid JSON.\n *\n * You know this for sure, for example, if you're decoding the result of\n * a `JSON.parse()` call.\n *\n * Done for performance reasons!\n */\nexport const jsonYolo: Decoder<Json> = unknown as Decoder<Json>;\n\n/**\n * Drop-in replacement for the `jsonObject` decoder from the decoders standard\n * library, but implemented as just a check for plain old JavaScript object.\n * This is, of course, only safe to use in contexts where you know that the\n * input already is valid JSON.\n *\n * You know this for sure, for example, if you're decoding the result of\n * a `JSON.parse()` call.\n *\n * Done for performance reasons!\n */\nexport const jsonObjectYolo: Decoder<JsonObject> = jsonYolo.refine(\n (value): value is JsonObject =>\n value !== null && typeof value === \"object\" && !Array.isArray(value),\n \"Must be JSON object\"\n);\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport { OpCode } from \"@liveblocks/core\";\nimport type { Decoder } from \"decoders\";\nimport {\n constant,\n object,\n oneOf,\n optional,\n string,\n taggedUnion,\n} from \"decoders\";\n\nimport type {\n ClientWireOp,\n CreateListOp,\n CreateMapOp,\n CreateObjectOp,\n CreateRegisterOp,\n DeleteCrdtOp,\n DeleteObjectKeyOp,\n SetParentKeyOp,\n UpdateObjectOp,\n} from \"~/protocol\";\n\nimport { jsonObjectYolo, jsonYolo } from \"./jsonYolo\";\n\ntype HasOpId = { opId: string };\n\nconst intent = oneOf([\"set\", \"push\"] as const);\n\nconst updateObjectOp: Decoder<UpdateObjectOp & HasOpId> = object({\n type: constant(OpCode.UPDATE_OBJECT),\n opId: string,\n id: string,\n data: jsonObjectYolo,\n});\n\nconst createObjectOp: Decoder<CreateObjectOp & HasOpId> = object({\n type: constant(OpCode.CREATE_OBJECT),\n opId: string,\n id: string,\n parentId: string,\n parentKey: string,\n data: jsonObjectYolo,\n intent: optional(intent),\n deletedId: optional(string),\n});\n\nconst createListOp: Decoder<CreateListOp & HasOpId> = object({\n type: constant(OpCode.CREATE_LIST),\n opId: string,\n id: string,\n parentId: string,\n parentKey: string,\n intent: optional(intent),\n deletedId: optional(string),\n});\n\nconst createMapOp: Decoder<CreateMapOp & HasOpId> = object({\n type: constant(OpCode.CREATE_MAP),\n opId: string,\n id: string,\n parentId: string,\n parentKey: string,\n intent: optional(intent),\n deletedId: optional(string),\n});\n\nconst createRegisterOp: Decoder<CreateRegisterOp & HasOpId> = object({\n type: constant(OpCode.CREATE_REGISTER),\n opId: string,\n id: string,\n parentId: string,\n parentKey: string,\n data: jsonYolo,\n intent: optional(intent),\n deletedId: optional(string),\n});\n\nconst deleteCrdtOp: Decoder<DeleteCrdtOp & HasOpId> = object({\n type: constant(OpCode.DELETE_CRDT),\n opId: string,\n id: string,\n});\n\nconst setParentKeyOp: Decoder<SetParentKeyOp & HasOpId> = object({\n type: constant(OpCode.SET_PARENT_KEY),\n opId: string,\n id: string,\n parentKey: string,\n});\n\nconst deleteObjectKeyOp: Decoder<DeleteObjectKeyOp & HasOpId> = object({\n type: constant(OpCode.DELETE_OBJECT_KEY),\n opId: string,\n id: string,\n key: string,\n});\n\nexport const op: Decoder<ClientWireOp> = taggedUnion(\"type\", {\n [OpCode.UPDATE_OBJECT]: updateObjectOp,\n [OpCode.CREATE_OBJECT]: createObjectOp,\n [OpCode.CREATE_LIST]: createListOp,\n [OpCode.CREATE_MAP]: createMapOp,\n [OpCode.CREATE_REGISTER]: createRegisterOp,\n [OpCode.DELETE_CRDT]: deleteCrdtOp,\n [OpCode.SET_PARENT_KEY]: setParentKeyOp,\n [OpCode.DELETE_OBJECT_KEY]: deleteObjectKeyOp,\n});\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { Brand } from \"@liveblocks/core\";\nimport { uuid } from \"decoders\";\n\n/**\n * A guid, a unique identifier for a Yjs sub document.\n */\nexport type Guid = Brand<string, \"Guid\">;\n\nexport const guidDecoder = uuid.refineType<Guid>();\n\nexport const ROOT_YDOC_ID = \"root\";\nexport type YDocId = typeof ROOT_YDOC_ID | Guid /* unique ID for subdoc */;\n\n/**\n * Any string that is a valid base64 encoded YJS update.\n */\nexport type YUpdate = Brand<string, \"YUpdate\">;\n\n/**\n * Any string that is a valid base64 encoded YJS state vector.\n */\nexport type YVector = Brand<string, \"YVector\">;\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { Json, JsonObject } from \"@liveblocks/core\";\nimport { CrdtType } from \"@liveblocks/core\";\n\nimport type { IReadableSnapshot } from \"~/interfaces\";\n\n// ---------------------------------------------------------------------------\n// Non-streaming version\n// ---------------------------------------------------------------------------\n\n/**\n * Serialize a storage snapshot to a simple JSON representation, returning a\n * full in-memory JsonObject. Faster than snapshotToLossyJson_lazy for\n * small/medium documents because the result can be passed straight to\n * JSON.stringify(). This format is lossy — the original storage structure\n * cannot be reconstructed from it, so it's output-only.\n */\nexport function snapshotToLossyJson_eager(\n snapshot: IReadableSnapshot\n): JsonObject {\n try {\n return buildObject(snapshot, \"root\", snapshot.get_root().data);\n } finally {\n snapshot.destroy();\n }\n}\n\nfunction buildNode(snapshot: IReadableSnapshot, id: string): Json {\n const node = snapshot.get_node(id);\n if (node.type === CrdtType.OBJECT) {\n return buildObject(snapshot, id, node.data);\n } else if (node.type === CrdtType.LIST) {\n return buildList(snapshot, id);\n } else if (node.type === CrdtType.MAP) {\n return buildMap(snapshot, id);\n } else {\n return node.data;\n }\n}\n\nfunction buildObject(\n snapshot: IReadableSnapshot,\n id: string,\n staticData: JsonObject\n): JsonObject {\n const data = Object.assign(Object.create(null), staticData) as JsonObject;\n for (const [key, childId] of snapshot.iter_children(id)) {\n data[key] = buildNode(snapshot, childId);\n }\n return data;\n}\n\nfunction buildList(snapshot: IReadableSnapshot, id: string): Json[] {\n const data: Json[] = [];\n for (const [_, childId] of snapshot.iter_children(id)) {\n data.push(buildNode(snapshot, childId));\n }\n return data;\n}\n\nfunction buildMap(snapshot: IReadableSnapshot, id: string): JsonObject {\n const data = Object.create(null) as JsonObject;\n for (const [key, childId] of snapshot.iter_children(id)) {\n data[key] = buildNode(snapshot, childId);\n }\n return data;\n}\n\n// ---------------------------------------------------------------------------\n// Streaming version\n// ---------------------------------------------------------------------------\n\n// Generator-of-strings type alias for brevity of signatures\ntype StringGen = Generator<string, void, never>;\n\n/**\n * Serialize a storage snapshot to a simple JSON representation. This format is\n * easy to consume but lossy — the original storage structure cannot be\n * reconstructed from it, so it's an output-only format. Slower than\n * snapshotToLossyJson_eager but can stream documents that don't fit entirely\n * in memory.\n *\n * This generator yields text chunks that together, when concatenated, form the\n * output JSON document.\n */\nexport function* snapshotToLossyJson_lazy(\n snapshot: IReadableSnapshot\n): StringGen {\n try {\n const staticJson = JSON.stringify(snapshot.get_root().data).slice(1, -1);\n yield* emitObject(snapshot, \"root\", staticJson);\n } finally {\n snapshot.destroy();\n }\n}\n\nfunction* emit(snapshot: IReadableSnapshot, id: string): StringGen {\n const node = snapshot.get_node(id);\n if (node.type === CrdtType.OBJECT) {\n yield* emitObject(snapshot, id, JSON.stringify(node.data).slice(1, -1));\n } else if (node.type === CrdtType.LIST) {\n yield* emitList(snapshot, id);\n } else if (node.type === CrdtType.MAP) {\n yield* emitMap(snapshot, id);\n } else if (node.type === CrdtType.REGISTER) {\n yield JSON.stringify(node.data);\n }\n}\n\n/**\n * @param staticJson - The object's static (non-CRDT) properties as a raw JSON\n * string without the surrounding braces, e.g. `\"foo\":1,\"bar\":\"hi\"`.\n *\n * Children are emitted _after_ the static properties. If a child key\n * collides with a static key (which shouldn't normally happen, but\n * defensively), the child wins because JSON.parse keeps the last value\n * for duplicate keys.\n */\nfunction* emitObject(\n snapshot: IReadableSnapshot,\n id: string,\n staticJson: string\n): StringGen {\n let comma = staticJson.length > 0;\n\n yield \"{\";\n yield staticJson;\n\n for (const [key, childId] of snapshot.iter_children(id)) {\n if (comma) yield \",\";\n else comma = true;\n\n yield `${JSON.stringify(key)}:`;\n yield* emit(snapshot, childId);\n }\n yield \"}\";\n}\n\nfunction* emitList(snapshot: IReadableSnapshot, id: string): StringGen {\n let comma = false;\n\n yield \"[\";\n for (const [_, childId] of snapshot.iter_children(id)) {\n if (comma) yield \",\";\n else comma = true;\n yield* emit(snapshot, childId);\n }\n yield \"]\";\n}\n\nfunction* emitMap(snapshot: IReadableSnapshot, id: string): StringGen {\n let comma = false;\n\n yield \"{\";\n for (const [key, childId] of snapshot.iter_children(id)) {\n if (comma) yield \",\";\n else comma = true;\n\n yield `${JSON.stringify(key)}:`;\n yield* emit(snapshot, childId);\n }\n yield \"}\";\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { StorageNode } from \"@liveblocks/core\";\n\nimport type { IReadableSnapshot } from \"~/interfaces\";\n\n/**\n * Yield all nodes from a snapshot as [id, crdt] tuples.\n * Destroys the snapshot when done (or aborted).\n */\nexport function* snapshotToNodeStream(\n snapshot: IReadableSnapshot\n): Generator<StorageNode, void, never> {\n try {\n yield* snapshot.iter_all();\n } finally {\n snapshot.destroy();\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type {\n JsonObject,\n ObjectStorageNode,\n PlainLson,\n PlainLsonFields,\n PlainLsonList,\n PlainLsonMap,\n PlainLsonObject,\n RootStorageNode,\n SerializedList,\n StorageNode,\n} from \"@liveblocks/core\";\nimport {\n assertNever,\n CrdtType,\n isJsonObject,\n makePosition,\n} from \"@liveblocks/core\";\n\nimport type { IReadableSnapshot } from \"~/interfaces\";\n\nconst SERVER_INIT_OP_PREFIX = \"si\";\n\nfunction generateId(state: { clock: number }) {\n return `${SERVER_INIT_OP_PREFIX}:${state.clock++}`;\n}\n\nfunction isSpecialPlainLsonValue(\n value: PlainLson\n): value is PlainLsonObject | PlainLsonMap | PlainLsonList {\n return isJsonObject(value) && value.liveblocksType !== undefined;\n}\n\n/**\n * Generator that yields NodeTuples for a JSON value.\n * Always yields parent nodes before their children.\n */\nfunction* iterJson(\n key: string,\n data: PlainLson,\n parent: StorageNode,\n state: { clock: number }\n): Generator<StorageNode, void, undefined> {\n if (isSpecialPlainLsonValue(data)) {\n switch (data.liveblocksType) {\n case \"LiveObject\":\n yield* iterObjectInner(key, data.data, parent, state);\n return;\n\n case \"LiveList\":\n yield* iterList(key, data.data, parent, state);\n return;\n\n case \"LiveMap\":\n yield* iterMap(key, data.data, parent, state);\n return;\n\n // istanbul ignore next\n default:\n assertNever(data, \"Unknown `liveblocksType` field\");\n }\n } else {\n yield [\n generateId(state),\n {\n type: CrdtType.REGISTER,\n data,\n parentId: parent[0],\n parentKey: key,\n },\n ];\n }\n}\n\n/**\n * Generator that yields NodeTuples for a LiveMap.\n * Yields the map node first, then its children.\n */\nfunction* iterMap(\n key: string,\n map: PlainLsonFields,\n parent: StorageNode,\n state: { clock: number }\n): Generator<StorageNode, void, undefined> {\n const mapTuple: StorageNode = [\n generateId(state),\n { type: CrdtType.MAP, parentId: parent[0], parentKey: key },\n ];\n\n // Yield the map node first (parent before children)\n yield mapTuple;\n\n // Then yield all children\n for (const [subKey, subValue] of Object.entries(map)) {\n yield* iterJson(subKey, subValue, mapTuple, state);\n }\n}\n\n/**\n * Generator that yields NodeTuples for a LiveList.\n * Yields the list node first, then its children.\n */\nfunction* iterList(\n key: string,\n list: PlainLson[],\n parent: StorageNode,\n state: { clock: number }\n): Generator<StorageNode, void, undefined> {\n const id = generateId(state);\n const crdt: SerializedList = {\n type: CrdtType.LIST,\n parentId: parent[0],\n parentKey: key,\n };\n const listTuple: StorageNode = [id, crdt];\n\n // Yield the list node first (parent before children)\n yield listTuple;\n\n // Then yield all children\n let position = makePosition();\n for (const subValue of list) {\n yield* iterJson(position, subValue, listTuple, state);\n position = makePosition(position);\n }\n}\n\n/**\n * Generator that yields NodeTuples for a LiveObject.\n * Yields the object node first, then its children.\n *\n * Note: The object's data field is populated with non-special values\n * (primitives, arrays, plain objects), while special values (LiveObject,\n * LiveList, LiveMap) are yielded as separate nodes.\n */\nfunction* iterObjectInner(\n key: string,\n value: PlainLsonFields,\n parent: StorageNode | null,\n state: { clock: number }\n): Generator<StorageNode, void, undefined> {\n // First pass: collect non-special data and identify special children\n const data: JsonObject = {};\n const specialChildren: Array<[string, PlainLson]> = [];\n\n for (const [subKey, subValue] of Object.entries(value)) {\n if (isSpecialPlainLsonValue(subValue)) {\n specialChildren.push([subKey, subValue]);\n } else {\n data[subKey] = subValue;\n }\n }\n\n // Create the object tuple with collected data\n const objectTuple: RootStorageNode | ObjectStorageNode =\n parent !== null\n ? [\n generateId(state),\n {\n type: CrdtType.OBJECT,\n data,\n parentId: parent[0],\n parentKey: key,\n },\n ]\n : [\"root\", { type: CrdtType.OBJECT, data }];\n\n // Yield the object node first (parent before children)\n yield objectTuple;\n\n // Then yield all special children\n for (const [subKey, subValue] of specialChildren) {\n yield* iterJson(subKey, subValue, objectTuple, state);\n }\n}\n\n/**\n * Transform a \"Plain LSON\" document to a lazy NodeStream. Used to initialize\n * the storage with a predefined state.\n * Always emits parent nodes before their children.\n */\nexport function* plainLsonToNodeStream(\n root: PlainLsonObject\n): Generator<StorageNode, void, undefined> {\n const state = { clock: 1 };\n yield* iterObjectInner(\"root\", root.data, null, state);\n}\n\n// ---------------------------------------------------------------------------\n// Non-streaming serialization: builds a full PlainLsonObject in memory.\n// ---------------------------------------------------------------------------\n\n/**\n * Serialize a storage snapshot to \"Plain LSON\" format, returning a full\n * in-memory PlainLsonObject. Faster than snapshotToPlainLson_lazy for\n * small/medium documents because the result can be passed straight to\n * JSON.stringify().\n */\nexport function snapshotToPlainLson_eager(\n snapshot: IReadableSnapshot\n): PlainLsonObject {\n try {\n return buildObject(snapshot, \"root\", snapshot.get_root().data);\n } finally {\n snapshot.destroy();\n }\n}\n\nfunction buildNode(snapshot: IReadableSnapshot, id: string): PlainLson {\n const node = snapshot.get_node(id);\n if (node.type === CrdtType.OBJECT) {\n return buildObject(snapshot, id, node.data);\n } else if (node.type === CrdtType.LIST) {\n return buildList(snapshot, id);\n } else if (node.type === CrdtType.MAP) {\n return buildMap(snapshot, id);\n } else {\n // TEMPORARY: `?? null` is only here to project legacy KV rooms that\n // contain data-less registers (under a LiveMap, representing `null`\n // entries), so the KV projection matches how the SQLite migrator\n // normalizes them. Remove once all rooms have been migrated to SQLite.\n return node.data ?? null;\n }\n}\n\nfunction buildObject(\n snapshot: IReadableSnapshot,\n id: string,\n staticData: JsonObject\n): PlainLsonObject {\n // Static data values are Json, which is a subset of PlainLson\n const data: PlainLsonFields = Object.assign(\n Object.create(null),\n staticData\n ) as PlainLsonFields;\n for (const [key, childId] of snapshot.iter_children(id)) {\n data[key] = buildNode(snapshot, childId);\n }\n return { liveblocksType: \"LiveObject\", data };\n}\n\nfunction buildList(snapshot: IReadableSnapshot, id: string): PlainLsonList {\n const data: PlainLson[] = [];\n for (const [_, childId] of snapshot.iter_children(id)) {\n data.push(buildNode(snapshot, childId));\n }\n return { liveblocksType: \"LiveList\", data };\n}\n\nfunction buildMap(snapshot: IReadableSnapshot, id: string): PlainLsonMap {\n const data = Object.create(null) as PlainLsonFields;\n for (const [key, childId] of snapshot.iter_children(id)) {\n data[key] = buildNode(snapshot, childId);\n }\n return { liveblocksType: \"LiveMap\", data };\n}\n\n// ---------------------------------------------------------------------------\n// Streaming serialization: yields string chunks that concatenate to JSON.\n// ---------------------------------------------------------------------------\n\n// Generator-of-strings type alias for brevity of signatures\ntype StringGen = Generator<string, void, never>;\n\n/**\n * Serialize a storage snapshot to \"Plain LSON\" format. Yields string chunks\n * that, when concatenated, form a valid JSON string representing the storage\n * document. Slower than snapshotToPlainLson_eager but can stream documents\n * that don't fit entirely in memory.\n */\nexport function* snapshotToPlainLson_lazy(\n snapshot: IReadableSnapshot\n): StringGen {\n try {\n const staticJson = JSON.stringify(snapshot.get_root().data).slice(1, -1);\n yield* emitObject(snapshot, \"root\", staticJson);\n } finally {\n snapshot.destroy();\n }\n}\n\nfunction* emit(snapshot: IReadableSnapshot, id: string): StringGen {\n const node = snapshot.get_node(id);\n if (node.type === CrdtType.OBJECT) {\n yield* emitObject(snapshot, id, JSON.stringify(node.data).slice(1, -1));\n } else if (node.type === CrdtType.LIST) {\n yield* emitList(snapshot, id);\n } else if (node.type === CrdtType.MAP) {\n yield* emitMap(snapshot, id);\n } else if (node.type === CrdtType.REGISTER) {\n // TEMPORARY: see buildNode — remove `?? null` once all rooms are\n // migrated to SQLite.\n yield JSON.stringify(node.data ?? null);\n }\n}\n\n/**\n * @param staticJson - The object's static (non-CRDT) properties as a raw JSON\n * string without the surrounding braces, e.g. `\"foo\":1,\"bar\":\"hi\"`.\n *\n * Children are emitted _after_ the static properties. If a child key\n * collides with a static key (which shouldn't normally happen, but\n * defensively), the child wins because JSON.parse keeps the last value\n * for duplicate keys.\n */\nfunction* emitObject(\n snapshot: IReadableSnapshot,\n id: string,\n staticJson: string\n): StringGen {\n let comma = staticJson.length > 0;\n\n yield '{\"liveblocksType\":\"LiveObject\",\"data\":{';\n yield staticJson;\n\n for (const [key, childId] of snapshot.iter_children(id)) {\n if (comma) yield \",\";\n else comma = true;\n\n yield `${JSON.stringify(key)}:`;\n yield* emit(snapshot, childId);\n }\n yield \"}}\";\n}\n\nfunction* emitList(snapshot: IReadableSnapshot, id: string): StringGen {\n let comma = false;\n\n yield '{\"liveblocksType\":\"LiveList\",\"data\":[';\n for (const [_, childId] of snapshot.iter_children(id)) {\n if (comma) yield \",\";\n else comma = true;\n yield* emit(snapshot, childId);\n }\n yield \"]}\";\n}\n\nfunction* emitMap(snapshot: IReadableSnapshot, id: string): StringGen {\n let comma = false;\n\n yield '{\"liveblocksType\":\"LiveMap\",\"data\":{';\n for (const [key, childId] of snapshot.iter_children(id)) {\n if (comma) yield \",\";\n else comma = true;\n\n yield `${JSON.stringify(key)}:`;\n yield* emit(snapshot, childId);\n }\n yield \"}}\";\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type {\n NodeMap,\n NodeStream,\n SerializedChild,\n SerializedCrdt,\n SerializedRootObject,\n StorageNode,\n} from \"@liveblocks/core\";\nimport { CrdtType, isRootStorageNode, nn } from \"@liveblocks/core\";\n\nimport type { IReadableSnapshot } from \"~/interfaces\";\nimport { NestedMap } from \"~/lib/NestedMap\";\n\n/**\n * Create a basic in-memory snapshot from a set of storage nodes.\n *\n * Takes a copy of the provided nodes, so the snapshot is isolated from\n * subsequent mutations to the source.\n */\nexport function makeInMemorySnapshot(\n values: NodeMap | NodeStream\n): IReadableSnapshot {\n const map: NodeMap = new Map<string, SerializedCrdt>(values as NodeStream);\n\n if (!map.has(\"root\")) {\n map.set(\"root\", { type: CrdtType.OBJECT, data: {} });\n }\n\n // Collect child entries, sort by (parentId, parentKey), then insert into\n // the revMap so that entriesAt() returns children in parent_key order\n // without needing to re-sort on every iter_children call.\n const entries: Array<[parentId: string, parentKey: string, id: string]> = [];\n const nodeStream = map as NodeStream;\n for (const node of nodeStream) {\n if (isRootStorageNode(node)) continue;\n const [id, crdt] = node;\n entries.push([crdt.parentId, crdt.parentKey, id]);\n }\n entries.sort((a, b) =>\n a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0\n );\n\n const revMap = new NestedMap<string, string, string>();\n for (const [parentId, parentKey, id] of entries) {\n revMap.set(parentId, parentKey, id);\n }\n\n function get_node(id: string): SerializedChild {\n return nn(map.get(id), `Node not found: ${id}`) as SerializedChild;\n }\n\n return {\n get_root: () =>\n nn(\n map.get(\"root\"),\n \"Root not found\"\n ) as SerializedCrdt as SerializedRootObject,\n get_node,\n iter_children: (nodeId) => revMap.entriesAt(nodeId),\n iter_all: () => map as Iterable<StorageNode>,\n destroy() {\n map.clear();\n revMap.clear();\n },\n };\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport { raise } from \"@liveblocks/core\";\n\n/**\n * Like ES6 map, but takes a default (factory) function which will be used\n * to create entries for missing keys on the fly.\n *\n * Useful for code like:\n *\n * const map = new DefaultMap(() => []);\n * map.getOrCreate('foo').push('hello');\n * map.getOrCreate('foo').push('world');\n * map.getOrCreate('foo')\n * // ['hello', 'world']\n *\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n #defaultFn?: (key: K) => V;\n\n /**\n * If the default function is not provided to the constructor, it has to be\n * provided in each .getOrCreate() call individually.\n */\n constructor(\n defaultFn?: (key: K) => V,\n entries?: readonly (readonly [K, V])[] | null\n ) {\n super(entries);\n this.#defaultFn = defaultFn;\n }\n\n /**\n * Gets the value at the given key, or creates it.\n *\n * Difference from normal Map: if the key does not exist, it will be created\n * on the fly using the factory function, and that value will get returned\n * instead of `undefined`.\n */\n getOrCreate(key: K, defaultFn?: (key: K) => V): V {\n if (super.has(key)) {\n return super.get(key)!;\n } else {\n const fn =\n defaultFn ??\n this.#defaultFn ??\n raise(\"DefaultMap used without a factory function\");\n\n const value = fn(key);\n this.set(key, value);\n return value;\n }\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport { DefaultMap } from \"./DefaultMap\";\n\nfunction emptyIterator(): IterableIterator<never> {\n return [][Symbol.iterator]();\n}\n\n/**\n * Like an ES6 Map, but two levels deep. Useful for building reverse lookup\n * tables. Will automatically delete second-level maps when they are empty.\n */\nexport class NestedMap<K1, K2, V> {\n #map: DefaultMap<K1, Map<K2, V>>;\n\n constructor() {\n this.#map = new DefaultMap(() => new Map<K2, V>());\n }\n\n get size(): number {\n let total = 0;\n for (const value of this.#map.values()) {\n total += value.size;\n }\n return total;\n }\n\n count(key1: K1): number {\n return this.#map.get(key1)?.size ?? 0;\n }\n\n *keys(): IterableIterator<[K1, K2]> {\n for (const [key1, nested] of this.#map) {\n for (const key2 of nested.keys()) {\n yield [key1, key2];\n }\n }\n }\n\n has(key1: K1, key2: K2): boolean {\n return this.#map.get(key1)?.has(key2) ?? false;\n }\n\n get(key1: K1, key2: K2): V | undefined {\n return this.#map.get(key1)?.get(key2);\n }\n\n set(key1: K1, key2: K2, value: V): this {\n this.#map.getOrCreate(key1).set(key2, value);\n return this;\n }\n\n delete(key1: K1, key2: K2): void {\n if (!this.#map.has(key1)) {\n return;\n }\n\n const nested = this.#map.get(key1)!;\n nested.delete(key2);\n if (nested.size === 0) {\n this.#map.delete(key1);\n }\n }\n\n clear(): void {\n this.#map.clear();\n }\n\n *[Symbol.iterator](): IterableIterator<[K1, K2, V]> {\n for (const [key1, nested] of this.#map) {\n for (const [key2, value] of nested) {\n yield [key1, key2, value];\n }\n }\n }\n\n entriesAt(key1: K1): IterableIterator<[K2, V]> {\n return this.#map.get(key1)?.entries() ?? emptyIterator();\n }\n\n *filterAt(key1: K1, keys: Iterable<K2>): Iterable<[K2, V]> {\n const nested = this.#map.get(key1);\n if (nested === undefined) {\n return;\n }\n\n for (const k2 of keys) {\n const value = nested.get(k2);\n if (value !== undefined) {\n yield [k2, value];\n }\n }\n }\n\n keysAt(key1: K1): IterableIterator<K2> {\n return this.#map.get(key1)?.keys() ?? emptyIterator();\n }\n\n valuesAt(key1: K1): IterableIterator<V> {\n return this.#map.get(key1)?.values() ?? emptyIterator();\n }\n\n deleteAll(key1: K1): void {\n this.#map.delete(key1);\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { Json } from \"@liveblocks/core\";\nimport type { Decoder } from \"decoders\";\n\nimport type { IStorageDriver } from \"~/interfaces\";\n\nexport interface MetadataDB {\n // Getter supports optional decoder\n get(key: string): Json | undefined;\n get<T>(decoder: Decoder<T>, key: string): T | undefined;\n\n put(key: string, value: Json): void;\n delete(key: string): void;\n}\n\n/**\n * Returns a thin wrapper around an IStorageDriver to provide MetadataDB\n * functionality, including type-safe reads.\n */\nexport function makeMetadataDB(driver: IStorageDriver): MetadataDB {\n function get(key: string): Json | undefined;\n function get<T>(decoder: Decoder<T>, key: string): T | undefined;\n function get<T>(a1: string | Decoder<T>, a2?: string): T | Json | undefined {\n if (a2 === undefined) {\n return driver.get_meta(a1 as string);\n } else {\n return (a1 as Decoder<T>).value(driver.get_meta(a2));\n }\n }\n\n return {\n get,\n put: driver.put_meta.bind(driver),\n delete: driver.delete_meta.bind(driver),\n };\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type {\n BaseUserMeta,\n Brand,\n CompactNode,\n IUserInfo,\n Json,\n JsonObject,\n} from \"@liveblocks/core\";\nimport {\n assertNever,\n ClientMsgCode,\n OpCode,\n raise,\n ServerMsgCode as CoreServerMsgCode,\n tryParseJson,\n WebsocketCloseCodes as CloseCode,\n} from \"@liveblocks/core\";\nimport { Mutex, tryAcquire } from \"async-mutex\";\nimport { array, formatInline } from \"decoders\";\nimport { chunkedByCost } from \"itertools\";\nimport { nanoid } from \"nanoid\";\n\nimport type { Guid } from \"~/decoders\";\nimport { clientMsgDecoder } from \"~/decoders\";\nimport type {\n IServerWebSocket,\n IStorageDriver,\n ListFeedMessagesOptions,\n ListFeedMessagesResult,\n ListFeedsOptions,\n ListFeedsResult,\n} from \"~/interfaces\";\nimport { Logger } from \"~/lib/Logger\";\nimport { makeNewInMemoryDriver } from \"~/plugins/InMemoryDriver\";\nimport type {\n AddFeedClientMsg,\n AddFeedMessageClientMsg,\n ClientMsg as GenericClientMsg,\n DeleteFeedClientMsg,\n DeleteFeedMessageClientMsg,\n FeedMessagesServerMsg,\n FeedsServerMsg,\n FetchFeedMessagesClientMsg,\n FetchFeedsClientMsg,\n IgnoredOp,\n Op,\n ServerMsg as GenericServerMsg,\n ServerWireOp,\n UpdateFeedClientMsg,\n UpdateFeedMessageClientMsg,\n} from \"~/protocol\";\nimport { Storage } from \"~/Storage\";\nimport { YjsStorage } from \"~/YjsStorage\";\n\nimport { tryCatch } from \"./lib/tryCatch\";\nimport { UniqueMap } from \"./lib/UniqueMap\";\nimport { feedFailureServerMsg, feedRequestFailed } from \"./protocol/feedErrors\";\nimport { FeedMsgCode, FeedRequestErrorCode } from \"./protocol/feedMessages\";\nimport { ProtocolVersion } from \"./protocol/ProtocolVersion\";\nimport type { Feed, FeedMessage, jstring, LeasedSession } from \"./types\";\nimport { isLeasedSessionExpired, makeRoomStateMsg } from \"./utils\";\n\nconst MB = 1024 * 1024;\n\nconst messagesDecoder = array(clientMsgDecoder);\n\n// Temporary patch\nconst ServerMsgCode = { ...CoreServerMsgCode, ...FeedMsgCode };\nconst HIGHEST_PROTOCOL_VERSION = Math.max(\n ...Object.values(ProtocolVersion).filter(\n (v): v is number => typeof v === \"number\"\n )\n) as ProtocolVersion;\n\n// Reverse lookup for ServerMsgCodes\nconst SERVER_MSG_CODE_NAMES = Object.fromEntries(\n Object.entries(ServerMsgCode).map(([k, v]) => [v, k])\n) as Record<(typeof ServerMsgCode)[keyof typeof ServerMsgCode], string>;\n\nconst BLACK_HOLE = new Logger([\n /* No targets, i.e. black hole logger */\n]);\n\n// Stream pre-built CompactNode tuple strings into raw STORAGE_CHUNK frames\n// (no parse/re-stringify of jdata). Each node is its own atomic unit for\n// chunkedByCost, so a single oversized row (≤ 2 MB CF limit) is the worst\n// case any output frame can exceed MAX_SIZE by.\nfunction groupNodesForWebSocketMessages(\n input: Iterable<jstring<CompactNode>>\n): Iterable<jstring<CompactNode>[]> {\n // Cap each outgoing WebSocket message at 16 MB. CF's 32 MB message size\n // limit only applies inbound, so the real constraint here is memory\n // pressure: the worker has 128 MB total, and stringifying a 16 MB message\n // costs roughly that much again in transient allocations.\n const MAX_SIZE = 16 * MB;\n\n // Chunk to form ideal message sizes, per chunk:\n // chunks 0..2 → 1 MB each (fast start)\n // chunks 3..9 → 2, 3, 4, 5, 6, 7, 8 MB (slowly ramp up)\n // chunks 10+ → 8 MB (steady state)\n const idealSize = (chunkIndex: number): number =>\n chunkIndex < 3 ? 1 * MB : Math.min(8, chunkIndex - 1) * MB;\n\n return chunkedByCost(input, (tup) => tup.length, MAX_SIZE, idealSize);\n}\n\nexport type ActorID = Brand<number, \"ActorID\">;\n/** Number of milliseconds since Unix epoch. */\nexport type Millis = Brand<number, \"Millis\">;\n\n/**\n * Session keys are also known as the \"nonce\" in the protocol. It's a random,\n * unique, but PRIVATE, identifier for the session, and it's important that\n * this ID is never shared to anyone except the connected client, which\n * receives it as part of its ROOM_STATE message.\n */\nexport type SessionKey = Brand<string, \"SessionKey\">;\n\ntype ClientMsg =\n | GenericClientMsg<JsonObject, Json>\n | FetchFeedsClientMsg\n | FetchFeedMessagesClientMsg\n | AddFeedClientMsg\n | UpdateFeedClientMsg\n | DeleteFeedClientMsg\n | AddFeedMessageClientMsg\n | UpdateFeedMessageClientMsg\n | DeleteFeedMessageClientMsg;\ntype ServerMsg =\n | GenericServerMsg<JsonObject, BaseUserMeta, Json>\n | FeedMessagesServerMsg\n | FeedsServerMsg; // Temp patched server msgs\n\n/**\n * Creates a collector for deferred promises (side effects that should run\n * outside a mutex). Call `defer` to collect promises, then `waitAll` to\n * await them all.\n */\nfunction collectSideEffects() {\n const deferred: Promise<void>[] = [];\n return {\n defer: (p: Promise<void>) => void deferred.push(p),\n waitAll: () => Promise.allSettled(deferred),\n };\n}\n\nfunction serialize(msgs: ServerMsg | readonly ServerMsg[]): jstring<ServerMsg> {\n return JSON.stringify(msgs) as jstring<ServerMsg>;\n}\n\nexport function ackIgnoredOp(opId: string): IgnoredOp {\n return { type: OpCode.DELETE_CRDT, id: \"ACK\", opId }; // (H)Ack Op\n}\n\nfunction stripOpId(op: Op): ServerWireOp {\n // TODO: Optimize later! Instead of duplicating every op and\n // stripping the opId explicitly, it would be generally more\n // efficient if we treated the opIds as \"envelopes\" around Ops (or\n // send them in a separate array altogether at the protocol level\n // in V8 soon--even better, as it would not even require any stripping!)\n const { opId: _, ...rest } = op; // Strip opIds from all outgoing messages!\n return rest;\n}\n\n/**\n * A known or anonymous user.\n *\n * BY DEFINITION:\n * A User with an assigned `id` property is a non-anonymous user.\n * A User with an assigned `anonymousId` property is an anonymous user.\n * A User with neither of those properties is also an anonymous user.\n *\n * WHAT'S THE DIFFERENCE?\n * When creating a non-anonymous user, other users in the room will be able to\n * observe the assigned `id` property in Presence (e.g. via the `other.user.id`\n * in the Liveblocks client).\n *\n * When creating an anonymous user, you can _optionally_ provide an anonymous\n * ID to (re)use. While not authorized, this still allows you to correlate\n * unique users.\n */\nexport type IUserData = AuthorizedUser | AnonymousUser; // YYY Remove this export before launch. It's a private API, but only needed temporarily, while refactoring our CF server\n\ntype AuthorizedUser = {\n readonly id: string;\n readonly anonymousId?: never;\n readonly info?: IUserInfo;\n};\n\n// Anonymous users, by definition, have no ID, or have an explicitly-assigned\n// anonymous ID (in case you need to control anonymous ID generation, e.g. by\n// tracking a cookie). The anonymous ID will not show up in other clients. To\n// those clients, it will appear as a user without an ID.\ntype AnonymousUser = {\n readonly anonymousId: string;\n readonly id?: never;\n readonly info?: IUserInfo;\n};\n\n/*\n\nSession Types: \n| | Browser Session | Backend Session | Leased Session |\n|-----------------------------------|-----------------|-----------------|-----------------|\n| Sends enter/leave/presence events | ✓ | | ✓ |\n| Visible to other users in room | ✓ | | ✓ |\n| Has WebSocket connection | ✓ | | |\n| Updated from | Browser | REST API | REST API |\n\n*/\n\n/**\n * Each BrowserSession is an abstraction around a socket instance, and maintains\n * metadata about the connection.\n */\nexport class BrowserSession<SM, CM extends JsonObject> {\n // ^^ User-defined Session Metadata\n // ^^ User-defined Client Metadata (sent to client in ROOM_STATE)\n\n public readonly version: ProtocolVersion; // Liveblocks protocol version this client will speak\n public readonly actor: ActorID; // Must be unique within the room\n public readonly createdAt: Millis;\n\n // Externally provided (public!) user metadata. This information will get shared with other clients\n public readonly user: IUserData;\n public readonly scopes: string[]; // Permissions for this session, sent to connected clients (so consider public info)\n public readonly meta: SM; // Arbitrary *private* meta data to attach to this session (will NOT be shared)\n public readonly publicMeta?: CM; // Metadata sent to client in ROOM_STATE message's \"meta\" field\n\n readonly #_socket: IServerWebSocket;\n readonly #_debug: boolean;\n /** Updated on every incoming message (including pings) via handleData(). Used for idle timeout detection. */\n #_lastActiveAt: Millis;\n\n // We keep a status in-memory in the session of whether we already sent a rejected ops message to the client.\n #_hasNotifiedClientStorageUpdateError: boolean;\n\n /** @internal - Never create a BrowserSession instance manually. Use the room.startBrowserSession() API instead. */\n constructor(\n ticket: Ticket<SM, CM>,\n socket: IServerWebSocket,\n debug: boolean,\n /** When restoring from hibernation, pass the original socket creation time so session duration is correct. */\n createdAt?: Date\n ) {\n this.version = ticket.version;\n this.actor = ticket.actor;\n this.user = ticket.user;\n this.scopes = ticket.scopes;\n this.meta = ticket.meta ?? (undefined as unknown as SM);\n this.publicMeta = ticket.publicMeta;\n this.#_socket = socket;\n this.#_debug = debug;\n\n const now = (createdAt?.getTime() ?? Date.now()) as Millis;\n this.createdAt = now;\n this.#_lastActiveAt = now;\n this.#_hasNotifiedClientStorageUpdateError = false;\n }\n\n get lastActiveAt(): Millis {\n const lastPing = this.#_socket.getLastPongTimestamp?.();\n if (lastPing && lastPing.getTime() > this.#_lastActiveAt) {\n return lastPing.getTime() as Millis;\n } else {\n return this.#_lastActiveAt;\n }\n }\n\n get hasNotifiedClientStorageUpdateError(): boolean {\n return this.#_hasNotifiedClientStorageUpdateError;\n }\n\n /** @internal - This should have to be called only from Room, never externally */\n markActive(now?: Date): void {\n this.#_lastActiveAt = (now ? now.getTime() : Date.now()) as Millis;\n }\n\n setHasNotifiedClientStorageUpdateError(): void {\n this.#_hasNotifiedClientStorageUpdateError = true;\n }\n\n sendPong(): number {\n this.markActive();\n\n const sent = this.#_socket.send(\"pong\");\n if (this.#_debug) {\n if (sent < 0) {\n console.error(\n `failed to send \"pong\" to actor=${this.actor} (back pressure)`\n );\n } else if (sent === 0) {\n console.error(\n `failed to send \"pong\" to actor=${this.actor} (connection issue)`\n );\n } else {\n // Success\n console.log(`sent to actor=${this.actor}: \"pong\"`);\n }\n }\n return sent;\n }\n\n send(serverMsg: ServerMsg | ServerMsg[] | jstring<ServerMsg>): number {\n const data =\n typeof serverMsg === \"string\" ? serverMsg : serialize(serverMsg);\n const sent = this.#_socket.send(data);\n if (this.#_debug) {\n if (sent < 0) {\n console.error(\n `failed to send message to actor=${this.actor} (back pressure)`\n );\n } else if (sent === 0) {\n console.error(\n `failed to send message to actor=${this.actor} (connection issue)`\n );\n }\n\n const msgs = JSON.parse(data) as ServerMsg | ServerMsg[];\n for (const msg of Array.isArray(msgs) ? msgs : [msgs]) {\n console.log(\n `sent to actor=${this.actor}: [${\n SERVER_MSG_CODE_NAMES[msg.type] ?? msg.type\n }] ${JSON.stringify(msg)}`\n );\n }\n }\n return sent;\n }\n\n /**\n * @internal\n * Closes the socket associated to this BrowserSession.\n *\n * NOTE: Never call this API directly! Call .endBrowserSession() instead.\n */\n closeSocket(code: number, reason?: string): void {\n this.#_socket.close(code, reason);\n }\n}\n\nexport class BackendSession extends BrowserSession<never, never> {\n /** @internal Never call this constructor directly */\n constructor(\n ticket: Ticket<never, never>,\n socket: IServerWebSocket,\n debug: boolean\n ) {\n super(ticket, socket, debug);\n }\n}\n\nexport type Ticket<SM, CM extends JsonObject> = {\n readonly sessionKey: SessionKey; // Should stay private\n readonly version: ProtocolVersion;\n readonly actor: ActorID;\n readonly meta?: SM; // Private Session metadata\n readonly publicMeta?: CM; // Client metadata is *public* metadata sent to client in ROOM_STATE message\n readonly user: IUserData; // User-provided, public, metadata\n readonly scopes: string[];\n};\n\nexport type CreateTicketOptions<SM, CM extends JsonObject> = {\n /** The Liveblocks protocol version this client will speak */\n version?: ProtocolVersion;\n meta?: SM;\n publicMeta?: CM;\n /** A user-provided ID to externally recognize the user by */\n id?: string;\n /**\n * A user-provided anonymous ID to use. When `id` is provided, this field is\n * ignored. When both fields are missing, a new anonymous ID will be\n * generated.\n */\n anonymousId?: string;\n /** Static user metadata to assign this session, will get broadcasted to other clients */\n info?: IUserInfo;\n /** Permissions to assign this session */\n scopes?: string[];\n\n /** An explicit actor ID to use. Supported for legacy use cases only. It's best to not set this and let it get assigned dynamically, as it's important for this identifier to be unique. */\n actor?: ActorID;\n};\n\ntype RoomOptions<SM, CM extends JsonObject, C> = {\n /**\n * Bring your own persistence backend\n */\n storage?: IStorageDriver;\n logger?: Logger;\n\n // YYY Restructure these hooks to all take a single `event` param\n hooks?: {\n /** Customize which incoming messages from a client are allowed or disallowed. */\n isClientMsgAllowed?: (\n msg: ClientMsg,\n session: BrowserSession<SM, CM>\n ) => { allowed: true } | { allowed: false; reason: string };\n\n /** Called whenever the server acknowledged a ping with a pong */\n onDidPong?: (ctx?: C) => void | Promise<void>;\n\n /** Called right before the room is attempted to be unloaded. Synchronous. May throw to abort the unloading. */\n onRoomWillUnload?: (ctx?: C) => void;\n\n /** Called when a new user entered the room. */\n onSessionDidStart?: (\n session: BrowserSession<SM, CM>,\n ctx?: C\n ) => void | Promise<void>;\n /** Called when a user left the room. */\n onSessionDidEnd?: (\n session: BrowserSession<SM, CM>,\n ctx?: C\n ) => void | Promise<void>;\n\n /**\n * Called when Liveblocks Storage for the room was updated.\n *\n * IMPORTANT! If you implement these as async functions, it's important to\n * note that these run outside of the storage mutex that guarantees\n * a consistent view of storage.\n * Therefore, only ever use this hook to implement a side effect (like\n * trigger a notification), don't read storage in this hook directly.\n */\n postClientMsgStorageDidUpdate?: (ctx?: C) => void | Promise<void>;\n /**\n * Called when Yjs Storage for the room was updated.\n *\n * IMPORTANT! If you implement these as async functions, it's important to\n * note that these run outside of the storage mutex that guarantees\n * a consistent view of storage.\n * Therefore, only ever use this hook to implement a side effect (like\n * trigger a notification), don't read storage in this hook directly.\n */\n postClientMsgYdocDidUpdate?: (\n ctx?: C,\n sess?: BrowserSession<SM, CM>\n ) => void | Promise<void>;\n };\n\n /** Enable debug logging */\n enableDebugLogging?: boolean;\n};\n\n/**\n * A Liveblocks Room server.\n */\nexport class Room<RM, SM, CM extends JsonObject, C = undefined> {\n // ^^^^^^^^^^ User-defined Room Metadata, Session Metadata, and Client Metadata\n\n public meta: RM;\n public readonly driver: IStorageDriver;\n #logger: Logger;\n\n /**\n * While a room is in \"maintenance mode\", all WebSocket connections to the\n * room should be rejected until it's pulled out of maintenance mode again.\n * Maintenance mode should only last a couple of milliseconds, typically.\n *\n * This mutex ensures that concurrent destructive operations (like\n * init-storage, storage-reset, etc.) cannot interfere with one another.\n */\n private readonly _maintenanceMode = new Mutex();\n\n private _storage: Storage;\n private _yjsStorage: YjsStorage;\n public readonly mutex: Mutex = new Mutex();\n\n public get storage(): Storage { return this._storage; } // prettier-ignore\n public get yjsStorage(): YjsStorage { return this._yjsStorage; } // prettier-ignore\n\n private _qsize = 0;\n\n private readonly sessions = new UniqueMap<\n SessionKey,\n BrowserSession<SM, CM>,\n ActorID\n >((s) => s.actor);\n\n private readonly hooks: {\n isClientMsgAllowed: (\n msg: ClientMsg,\n session: BrowserSession<SM, CM>\n ) => { allowed: true } | { allowed: false; reason: string };\n\n onDidPong?: (ctx?: C) => void | Promise<void>;\n\n onRoomWillUnload?: (ctx?: C) => void;\n\n onSessionDidStart?: (\n session: BrowserSession<SM, CM>,\n ctx: C | undefined\n ) => void | Promise<void>;\n onSessionDidEnd?: (\n session: BrowserSession<SM, CM>,\n ctx: C | undefined\n ) => void | Promise<void>;\n\n // Don't like these callback names yet. Think about how to better abstract it later.\n postClientMsgStorageDidUpdate?: (ctx?: C) => void | Promise<void>;\n postClientMsgYdocDidUpdate?: (\n ctx?: C,\n sess?: BrowserSession<SM, CM>\n ) => void | Promise<void>;\n };\n\n readonly #_debug: boolean;\n\n constructor(meta: RM, options?: RoomOptions<SM, CM, C>) {\n const driver = options?.storage ?? makeNewInMemoryDriver();\n this.meta = meta;\n this.driver = driver;\n this._storage = new Storage(this.driver);\n this._yjsStorage = new YjsStorage(this.driver);\n\n this.#logger = options?.logger ?? BLACK_HOLE;\n this.hooks = {\n isClientMsgAllowed:\n options?.hooks?.isClientMsgAllowed ??\n (() => {\n return {\n allowed: true,\n };\n }),\n\n onRoomWillUnload: options?.hooks?.onRoomWillUnload,\n\n onSessionDidStart: options?.hooks?.onSessionDidStart,\n onSessionDidEnd: options?.hooks?.onSessionDidEnd,\n\n postClientMsgStorageDidUpdate:\n options?.hooks?.postClientMsgStorageDidUpdate,\n postClientMsgYdocDidUpdate: options?.hooks?.postClientMsgYdocDidUpdate,\n };\n this.#_debug = options?.enableDebugLogging ?? false;\n }\n\n public get logger(): Logger {\n return this.#logger;\n }\n\n public addLoggerContext(attrs: JsonObject): void {\n this.#logger = this.#logger.withContext(attrs);\n }\n\n public get numSessions(): number { return this.sessions.size; } // prettier-ignore\n\n // ------------------------------------------------------------------------------------\n // Maintenance mode\n // ------------------------------------------------------------------------------------\n\n /**\n * Returns true if the room is currently in maintenance mode.\n * When in maintenance mode, callers should refuse new WebSocket connections.\n */\n public get isInMaintenance(): boolean {\n return this._maintenanceMode.isLocked();\n }\n\n /**\n * Tries to enter maintenance mode and run the given callback exclusively.\n * If the room is already in maintenance mode, throws E_ALREADY_LOCKED\n * immediately instead of queuing the request.\n */\n public async runInMaintenanceMode<T>(\n callback: () => T | Promise<T>\n ): Promise<T> {\n return tryAcquire(this._maintenanceMode).runExclusive(callback);\n }\n\n // ------------------------------------------------------------------------------------\n // Public API\n // ------------------------------------------------------------------------------------\n\n /**\n * Releases the currently-loaded storage tree and Yjs documents from worker\n * memory, freeing them up to be garbage collected. The next time the room\n * is used, fresh instances will lazily be created.\n */\n public unload(ctx?: C): void {\n this.hooks.onRoomWillUnload?.(ctx); // May throw to cancel unloading\n\n // NOTE: this.mutex is deliberately left untouched here. It guards\n // in-flight message processing for the lifetime of this Room instance,\n // and force-releasing a held lock would allow concurrent writers.\n this.driver.reinitialize();\n this._storage = new Storage(this.driver);\n this._yjsStorage = new YjsStorage(this.driver);\n }\n\n /**\n * Issues a Ticket with a new/unique actor ID\n *\n * IMPORTANT! As the caller of this function, you are responsible for\n * ensuring you trust the values passed in here. Never pass unauthorized\n * values in here.\n *\n * The returned Ticket can be turned into a active Session once the socket\n * connection is established. If the socket is never established, this\n * unused Ticket will simply get garbage collected.\n */\n public createTicket(options?: CreateTicketOptions<SM, CM>): Ticket<SM, CM> {\n const actor = options?.actor ?? this.getNextActor();\n const sessionKey = nanoid() as SessionKey;\n const info = options?.info;\n const ticket: Ticket<SM, CM> = {\n version: options?.version ?? HIGHEST_PROTOCOL_VERSION,\n actor,\n sessionKey,\n meta: options?.meta,\n publicMeta: options?.publicMeta,\n user: options?.id\n ? { id: options.id, info }\n : { anonymousId: options?.anonymousId ?? nanoid(), info },\n scopes: options?.scopes ?? [\"room:write\"],\n };\n if (this.#_debug) {\n console.log(`new ticket created: ${JSON.stringify(ticket)}`);\n }\n return ticket;\n }\n\n public createBackendSession_experimental(): [\n session: BackendSession,\n outgoingMessages: jstring<ServerMsg>[],\n ] {\n const ticket = this.createTicket() as Ticket<never, never>;\n const capturedServerMsgs: jstring<ServerMsg>[] = [];\n const stub = {\n send: (data) => {\n if (typeof data === \"string\") {\n capturedServerMsgs.push(data as jstring<ServerMsg>);\n }\n return 0;\n },\n close: () => {}, // noop\n } satisfies IServerWebSocket;\n const session = new BackendSession(ticket, stub, false);\n return [session, capturedServerMsgs];\n }\n\n /**\n * Restores the given sessions as the Room server's session list. Can only be\n * called as long as there are no existing sessions.\n *\n * The key difference with the .startBrowserSession() API is that restoreSessions is\n * used in cases where a session was hibernated and needs to be restored,\n * without _conceptually_ starting a new session.\n *\n * Because there are no side effects to restoreSession, it's synchronous.\n */\n public restoreSessions(\n sessions: {\n ticket: Ticket<SM, CM>;\n socket: IServerWebSocket;\n lastActivity: Date;\n /** Original session creation time (e.g. from persisted attachment). Required for correct session duration after hibernation. */\n createdAt?: Date;\n }[]\n ): void {\n if (this.sessions.size > 0) {\n throw new Error(\"This API can only be called before any sessions exist\");\n }\n\n for (const { ticket, socket, lastActivity, createdAt } of sessions) {\n const newSession = new BrowserSession(\n ticket,\n socket,\n this.#_debug,\n createdAt\n );\n this.sessions.set(ticket.sessionKey, newSession);\n newSession.markActive(lastActivity);\n }\n }\n\n private sendSessionStartMessages(\n newSession: BrowserSession<SM, CM>,\n ticket: Ticket<SM, CM>,\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to sendSessionStartMessages() to collect async side effects.\"\n );\n }\n ): void {\n const users: Record<ActorID, BaseUserMeta & { scopes: string[] }> = {};\n // Add regular sessions\n for (const session of this.otherSessions(ticket.sessionKey)) {\n users[session.actor] = {\n id: session.user.id,\n info: session.user.info,\n scopes: session.scopes,\n };\n }\n\n // List all active server sessions\n const leasedSessions: LeasedSession[] = this.listLeasedSessions(ctx, defer);\n // Add server sessions\n for (const leasedSession of leasedSessions) {\n users[leasedSession.actorId as ActorID] = {\n id: leasedSession.sessionId,\n info: leasedSession.info,\n scopes: [],\n };\n }\n\n // this must happen before presence messages are sent\n newSession.send(\n makeRoomStateMsg(\n newSession.actor,\n ticket.sessionKey, // called \"nonce\" in the protocol\n newSession.scopes,\n users,\n ticket.publicMeta\n )\n );\n\n // Send each server session's full presence to the new user\n // NOTE: this doesn't exist for other browser sessions because those other sessions send from their frontend clients in response to room state messages.\n for (const leasedSession of leasedSessions) {\n newSession.send({\n type: ServerMsgCode.UPDATE_PRESENCE,\n actor: leasedSession.actorId as ActorID,\n targetActor: newSession.actor, // full presence to new user\n data: leasedSession.presence as JsonObject,\n });\n }\n }\n\n /**\n * Registers a new BrowserSession into the Room server's session list, along with\n * the socket connection to use for that BrowserSession, now that it is known.\n *\n * This kicks off a few side effects:\n * - Sends a ROOM_STATE message to the socket.\n * - Broadcasts a USER_JOINED message to all other sessions in the room.\n */\n public startBrowserSession(\n ticket: Ticket<SM, CM>,\n socket: IServerWebSocket,\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to startBrowserSession() to collect async side effects.\"\n );\n }\n ): void {\n let existing: SessionKey | undefined;\n while (\n (existing = this.sessions.lookupPrimaryKey(ticket.actor)) !== undefined\n ) {\n // If this happens, it means a new connection attempt is happening for an\n // existing actor ID. It's most likely from a reconnection attempt using\n // a legacy token (which has the actor ID hardcoded in it), where the old\n // session hasn't been closed explicitly. We'll actively kill it now.\n\n // Terminate old session\n this.endBrowserSession(\n existing,\n CloseCode.KICKED,\n \"Closed stale connection\",\n ctx,\n defer\n );\n\n this.#logger.warn(\n `Previous session for actor ${ticket.actor} killed in favor of new session`\n );\n }\n\n const newSession = new BrowserSession(ticket, socket, this.#_debug);\n this.sessions.set(ticket.sessionKey, newSession);\n\n // send sessions start messages\n this.sendSessionStartMessages(newSession, ticket, ctx, defer);\n\n this.sendToOthers(\n ticket.sessionKey,\n {\n type: ServerMsgCode.USER_JOINED,\n actor: newSession.actor,\n id: newSession.user.id,\n info: newSession.user.info,\n scopes: newSession.scopes,\n },\n ctx,\n defer\n );\n\n // Call the hook, but don't await the results here\n const p$ = this.hooks.onSessionDidStart?.(newSession, ctx);\n if (p$) defer(p$);\n }\n\n /**\n * Unregisters the BrowserSession for the given actor. Call this when the socket has\n * been closed from the client's end.\n *\n * This kicks off a few side effects:\n * - Broadcasts a USER_LEFT message to all other sessions in the room.\n */\n public endBrowserSession(\n key: SessionKey,\n code: number,\n reason: string,\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"Your onSessionDidEnd handler returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to endBrowserSession() to collect async side effects.\"\n );\n }\n ): void {\n const sessions = this.sessions;\n\n const session = sessions.get(key);\n if (session === undefined) return;\n\n session.closeSocket(code, reason);\n\n const deleted = sessions.delete(key);\n if (deleted) {\n for (const other of this.otherSessions(key)) {\n other.send({ type: ServerMsgCode.USER_LEFT, actor: session.actor });\n }\n\n // Call the hook\n const p$ = this.hooks.onSessionDidEnd?.(session, ctx);\n if (p$) defer(p$);\n }\n }\n\n /**\n * Force-closes all sessions matching the given predicate.\n */\n public endSessionBy(\n predicate: (session: BrowserSession<SM, CM>) => boolean,\n code: number,\n reason: string,\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"Your onSessionDidEnd handler returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to endSessionBy() to collect async side effects.\"\n );\n }\n ): number {\n let count = 0;\n for (const [key, session] of this.sessions) {\n if (predicate(session)) {\n count++;\n this.endBrowserSession(key, code, reason, ctx, defer);\n }\n }\n return count;\n }\n\n /**\n * Handles a raw incoming socket message, which can be a ping, or an\n * JSON-encoded message batch.\n */\n public async handleData(\n key: SessionKey,\n data: unknown,\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to handleData() to collect async side effects.\"\n );\n }\n ): Promise<void> {\n const text =\n typeof data === \"string\" ? data : raise(\"Unsupported message format\");\n\n this.sessions.get(key)?.markActive();\n if (text === \"ping\") {\n await this.handlePing(key, ctx);\n } else {\n const json = tryParseJson(text);\n const messages = messagesDecoder.decode(json);\n\n if (!messages.ok) {\n const reason =\n process.env.NODE_ENV !== \"production\"\n ? formatInline(messages.error)\n : \"Invalid message format\";\n\n this.endBrowserSession(\n key,\n CloseCode.INVALID_MESSAGE_FORMAT,\n reason,\n ctx,\n defer\n );\n return;\n }\n\n // TODO: Decide on these limits later.\n // If qsize is > 0, then it means there is a traffic jam. This shouldn't\n // be a problem for a while, but it grows beyond a certain (soft or hard)\n // limit, we may want to take measures.\n if (this._qsize > 10_000) {\n // Over hard limit\n // TODO: Maybe disconnect this sockets with a 42xx close code? This\n // will make the client back off more aggressively. See\n // https://github.com/liveblocks/liveblocks/blob/223f7ce0d77380fecd3b08ed9454ca8c330bbe16/packages/liveblocks-core/src/types/IWebSocket.ts#L53\n } else if (this._qsize > 5_000) {\n // Over soft limit\n // TODO: Maybe instruct clients to increase their throttle values?\n }\n\n this._qsize++;\n\n // Run this.handleMsgs(), but guarded by a mutex lock, ensuring that no\n // two messages will get processed simultaneously. This provides similar\n // concurrency protection as Cloudflare's I/O gates\n try {\n await this.processClientMsg(key, messages.value, ctx);\n } finally {\n this._qsize--;\n }\n }\n }\n\n /**\n * Processes an incoming batch of 1 or more ClientMsgs on behalf of\n * a (regular user/browser) session.\n *\n * IMPORTANT: Only use this API on \"trusted\" data!\n * To handle untrusted input data, use `.handleData()` instead.\n *\n * Before calling this API, make sure:\n * 1. The call site is entitled to call this message on behalf of this session; and\n * 2. The ClientMsg payload has been validated to be correct.\n */\n public async processClientMsg(\n key: SessionKey,\n messages: ClientMsg[],\n ctx?: C\n ): Promise<void> {\n const { defer, waitAll } = collectSideEffects();\n await this.mutex.runExclusive(() =>\n this._processClientMsg_withExclusiveAccess(key, messages, ctx, defer)\n );\n\n // Run all deferred work (like queueing messages, sending notifications,\n // etc) outside of the mutex\n await waitAll();\n }\n\n /**\n * Processes an incoming batch of 1 or more ClientMsgs on behalf of\n * a BACKEND session.\n *\n * Difference 1: HTTP RESPONSE instead of WEB SOCKET RESPONSE\n * ----------------------------------------------------------\n * For \"normal\" sessions that have a socket attached, any \"responses\" (i.e.\n * server messages like acks or fixops) will be sent back through that\n * existing socket connection.\n *\n * The key difference when using this method is that there is no such socket,\n * so any \"response\" ServerMsgs will get sent back as an HTTP response.\n *\n * Difference 2: No auth check\n * ---------------------------\n * Another key difference is that when processing a backend session, no\n * \"isClientMsgAllowed()\" check is performed, because those checks assume\n * a session.\n */\n public async processClientMsgFromBackendSession(\n session: BackendSession,\n messages: ClientMsg[],\n ctx?: C\n ): Promise<void> {\n const { defer, waitAll } = collectSideEffects();\n await this.mutex.runExclusive(() =>\n this._processClientMsgFromBackendSession_withExclusiveAccess(\n session,\n messages,\n ctx,\n defer\n )\n );\n\n // Run all deferred work (like queueing messages, sending notifications,\n // etc) outside of the mutex\n await waitAll();\n }\n\n public getSession(\n sessionKey: SessionKey\n ): BrowserSession<SM, CM> | undefined {\n return this.sessions.get(sessionKey);\n }\n\n public listSessions(): BrowserSession<SM, CM>[] {\n return Array.from(this.sessions.values());\n }\n\n /**\n * Upsert a leased session. Creates a new session if it doesn't exist (or is expired),\n * or updates an existing session with merged presence.\n */\n public upsertLeasedSession(\n sessionId: string,\n presence: JsonObject,\n ttl: number,\n info: IUserInfo,\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to upsertLeasedSession() to collect async side effects.\"\n );\n }\n ): void {\n const existingSession = this.driver.get_leased_session(sessionId);\n const isExpired =\n existingSession !== undefined && isLeasedSessionExpired(existingSession);\n\n if (isExpired) {\n this.deleteLeasedSession(existingSession, ctx, defer);\n }\n\n if (existingSession === undefined || isExpired) {\n // Creating new session (or was expired)\n const actorId = this.getNextActor();\n const now = Date.now();\n const session: LeasedSession = {\n sessionId,\n presence,\n updatedAt: now,\n info,\n ttl,\n actorId,\n };\n\n this.driver.put_leased_session(session);\n\n // Broadcast USER_JOINED to all existing sessions\n this.sendToAll(\n {\n type: ServerMsgCode.USER_JOINED,\n actor: actorId,\n id: sessionId,\n info,\n scopes: [],\n },\n ctx,\n defer\n );\n // now send the presence to all sessions\n this.sendToAll(\n {\n type: ServerMsgCode.UPDATE_PRESENCE,\n actor: actorId,\n data: presence,\n targetActor: 1,\n },\n ctx,\n defer\n );\n } else {\n // Updating existing session (and not expired)\n // Merge/patch the presence\n const mergedPresence = {\n ...(existingSession.presence as JsonObject),\n ...presence,\n };\n const updatedSession: LeasedSession = {\n ...existingSession,\n //info, UserInfo is immutable after creation\n presence: mergedPresence,\n updatedAt: Date.now(),\n ttl,\n };\n\n this.driver.put_leased_session(updatedSession);\n\n // Broadcast UPDATE_PRESENCE WITHOUT targetActor to all sessions (patch)\n this.sendToAll(\n {\n type: ServerMsgCode.UPDATE_PRESENCE,\n actor: existingSession.actorId,\n data: presence, // Send only the patch, not the full merged presence\n // NO targetActor - this makes it a partial presence patch\n },\n ctx,\n defer\n );\n }\n }\n\n /**\n * List all server sessions. As a side effect, it will delete expired sessions.\n */\n public listLeasedSessions(\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to listLeasedSessions() to collect async side effects.\"\n );\n }\n ): LeasedSession[] {\n const sessions = this.driver.list_leased_sessions();\n const validSessions: LeasedSession[] = [];\n const toDelete: LeasedSession[] = [];\n for (const [_, session] of sessions) {\n if (isLeasedSessionExpired(session)) {\n toDelete.push(session);\n } else {\n validSessions.push(session);\n }\n }\n\n for (const session of toDelete) {\n this.deleteLeasedSession(session, ctx, defer);\n }\n\n return validSessions;\n }\n\n /**\n * Delete a server session and broadcast USER_LEFT to all sessions.\n */\n public deleteLeasedSession(\n session: LeasedSession,\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to deleteLeasedSession() to collect async side effects.\"\n );\n }\n ): void {\n // Broadcast USER_LEFT to all sessions\n this.sendToAll(\n {\n type: ServerMsgCode.USER_LEFT,\n actor: session.actorId,\n },\n ctx,\n defer\n );\n this.driver.delete_leased_session(session.sessionId);\n }\n\n /**\n * Delete all server sessions and broadcast USER_LEFT to all sessions.\n */\n public deleteAllLeasedSessions(\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to deleteAllLeasedSessions() to collect async side effects.\"\n );\n }\n ): void {\n const sessions = this.driver.list_leased_sessions();\n for (const [_, session] of sessions) {\n this.deleteLeasedSession(session, ctx, defer);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Feed APIs\n // ---------------------------------------------------------------------------\n\n /**\n * List feeds with pagination and filtering.\n */\n public listFeeds(options?: ListFeedsOptions): ListFeedsResult {\n return this.driver.list_feeds(options);\n }\n\n /**\n * Get a specific feed by feed ID.\n */\n public getFeed(feedId: string): Feed | undefined {\n return this.driver.get_feed(feedId);\n }\n\n /**\n * Create a new feed.\n * If timestamp is not provided, current server time is used.\n */\n public createFeed(\n feed: Omit<Feed, \"createdAt\" | \"updatedAt\"> & { timestamp?: number }\n ): Feed {\n const now = feed.timestamp ?? Date.now();\n const fullFeed: Feed = {\n ...feed,\n createdAt: now,\n updatedAt: now,\n };\n this.driver.create_feed(fullFeed);\n return fullFeed;\n }\n\n /**\n * Update a feed's metadata.\n */\n public updateFeedMetadata(feedId: string, metadata: Json): void {\n this.driver.update_feed_metadata(feedId, metadata);\n }\n\n /**\n * Delete a feed.\n */\n public deleteFeed(feedId: string): void {\n this.driver.delete_feed(feedId);\n }\n\n /**\n * List feed messages for a feed with pagination.\n */\n public listFeedMessages(\n feedId: string,\n options?: ListFeedMessagesOptions\n ): ListFeedMessagesResult {\n return this.driver.list_feed_messages(feedId, options);\n }\n\n /**\n * Add a message to a feed.\n * If message id is not provided, a unique ID is automatically generated.\n * If timestamp is not provided, current server time is used.\n */\n public addFeedMessage(\n feedId: string,\n message: Omit<FeedMessage, \"id\" | \"createdAt\" | \"updatedAt\"> &\n Partial<Pick<FeedMessage, \"id\">> & { timestamp?: number }\n ): FeedMessage {\n const now = message.timestamp ?? Date.now();\n const fullMessage: FeedMessage = {\n id: message.id ?? nanoid(),\n createdAt: now,\n updatedAt: now,\n data: message.data,\n };\n this.driver.add_feed_message(feedId, fullMessage);\n return fullMessage;\n }\n\n /**\n * Update a feed message's data.\n * Returns the updated message.\n */\n public updateFeedMessage(\n feedId: string,\n messageId: string,\n data: Json,\n timestamp?: number\n ): FeedMessage {\n return this.driver.update_feed_message(\n feedId,\n messageId,\n data,\n timestamp ?? Date.now()\n );\n }\n\n /**\n * Delete a feed message.\n */\n public deleteFeedMessage(feedId: string, messageId: string): void {\n this.driver.delete_feed_message(feedId, messageId);\n }\n\n /**\n * Will send the given ServerMsg through all Session, except the Session\n * where the message originates from.\n */\n public sendToOthers(\n sender: SessionKey,\n serverMsg: ServerMsg | readonly ServerMsg[],\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to sendToOthers() to collect async side effects.\"\n );\n }\n ): void {\n const msg = serialize(serverMsg);\n for (const [key, session] of this.otherSessionEntries(sender)) {\n const success = session.send(msg);\n if (success === 0) {\n // If there is a connection issue, terminate the session at once.\n // Note that in the case of -1 (= back pressure), we don't terminate\n // the connection.\n this.endBrowserSession(\n key,\n CloseCode.KICKED,\n \"Closed broken connection\",\n ctx,\n defer\n );\n }\n }\n }\n\n /**\n * Will broadcast the given ServerMsg to all Sessions in the Room.\n */\n public sendToAll(\n serverMsg: ServerMsg | readonly ServerMsg[],\n ctx?: C,\n defer: (promise: Promise<void>) => void = () => {\n throw new Error(\n \"One of your hook handlers returned a promise, but no side effect collector was provided. \" +\n \"Pass a `defer` callback to sendToAll() to collect async side effects.\"\n );\n }\n ): void {\n const msg = serialize(serverMsg);\n for (const [key, session] of this.sessions) {\n const success = session.send(msg);\n if (success === 0) {\n // If there is a connection issue, terminate the session at once.\n // Note that in the case of -1 (= back pressure), we don't terminate\n // the connection.\n this.endBrowserSession(\n key,\n CloseCode.KICKED,\n \"Closed broken connection\",\n ctx,\n defer\n );\n }\n }\n }\n\n /**\n * Returns a new, unique, actor ID.\n */\n public getNextActor(): ActorID {\n return this.driver.next_actor() as ActorID;\n }\n\n // ------------------------------------------------------------------------------------\n // Private APIs\n // ------------------------------------------------------------------------------------\n\n /**\n * Iterates over all *other* Sessions and their session keys.\n */\n private *otherSessionEntries(\n currentKey: SessionKey\n ): Generator<[SessionKey, BrowserSession<SM, CM>]> {\n for (const [key, session] of this.sessions) {\n if (key !== currentKey) {\n yield [key, session];\n }\n }\n }\n\n /**\n * Iterates over all *other* Sessions.\n */\n private *otherSessions(\n currentKey: SessionKey\n ): Generator<BrowserSession<SM, CM>> {\n for (const [key, session] of this.sessions) {\n if (key !== currentKey) {\n yield session;\n }\n }\n }\n\n /**\n * @internal\n * Handles an incoming ping, by sending a pong back.\n */\n private async handlePing(sessionKey: SessionKey, ctx?: C): Promise<void> {\n const session = this.sessions.get(sessionKey);\n if (session === undefined) {\n this.#logger\n .withContext({ sessionKey })\n .warn(\"[probe] in handlePing, no such session exists\");\n return;\n }\n\n const sent = session.sendPong();\n\n // 0 means there was a connection issue\n // -1 means there was back pressure, which is no issue (we'll just count the ping)\n if (sent !== 0) {\n await this.hooks.onDidPong?.(ctx);\n }\n }\n\n private _processClientMsg_withExclusiveAccess(\n sessionKey: SessionKey,\n messages: ClientMsg[],\n ctx: C | undefined,\n defer: (p: Promise<void>) => void\n ): void {\n const session = this.sessions.get(sessionKey);\n if (!session) {\n this.#logger\n .withContext({ sessionKey })\n .warn(\"[probe] in handleClientMsgs, no such session exists\");\n return;\n }\n\n // Keep two ServerMsg buffers to send at the end:\n // - Messages to fan-out to all *others* (current session not included)\n // - Messages to reply back to the current sender (i.e. acks and rejections)\n const toFanOut: ServerMsg[] = [];\n const toReply: ServerMsg[] = [];\n const replyImmediately = (\n msg: ServerMsg | ServerMsg[] | jstring<ServerMsg>\n ) => void session.send(msg);\n const scheduleFanOut = (msg: ServerMsg) => void toFanOut.push(msg);\n const scheduleReply = (msg: ServerMsg) => void toReply.push(msg);\n\n for (const msg of messages) {\n const isMsgAllowed = this.hooks.isClientMsgAllowed(msg, session);\n if (isMsgAllowed.allowed) {\n this.handleOne(\n session,\n msg,\n replyImmediately,\n scheduleFanOut,\n scheduleReply,\n ctx,\n defer\n );\n } else {\n if (!session.hasNotifiedClientStorageUpdateError) {\n toReply.push({\n type: ServerMsgCode.REJECT_STORAGE_OP,\n opIds:\n msg.type === ClientMsgCode.UPDATE_STORAGE\n ? msg.ops.map((op) => op.opId)\n : [],\n reason: isMsgAllowed.reason,\n });\n session.setHasNotifiedClientStorageUpdateError();\n }\n }\n }\n\n if (toFanOut.length > 0) {\n this.sendToOthers(sessionKey, toFanOut, ctx, defer);\n }\n\n if (toReply.length > 0) {\n session.send(toReply);\n }\n }\n\n // TODO It's a bit bothering how much duplication there is between this method\n // and the _processClientMsg_withExclusiveAccess version. A better\n // abstraction is needed.\n private _processClientMsgFromBackendSession_withExclusiveAccess(\n session: BackendSession,\n messages: ClientMsg[],\n ctx: C | undefined,\n defer: (p: Promise<void>) => void\n ): void {\n // Keep two ServerMsg buffers to send at the end:\n // - Messages to fan-out to all *others* (current session not included)\n // - Messages to reply back to the current sender (i.e. acks and rejections)\n const toFanOut: ServerMsg[] = [];\n const toReplyImmediately: ServerMsg[] = [];\n const toReplyAfter: ServerMsg[] = [];\n\n const replyImmediately = (\n msg: ServerMsg | ServerMsg[] | jstring<ServerMsg>\n ) => {\n if (typeof msg === \"string\") {\n // Pre-serialized payload: flush any pending typed messages first so\n // wire order matches call order, then send the raw string directly.\n if (toReplyImmediately.length > 0) {\n session.send(toReplyImmediately);\n toReplyImmediately.length = 0;\n }\n session.send(msg);\n } else if (Array.isArray(msg)) {\n for (const m of msg) {\n toReplyImmediately.push(m);\n }\n } else {\n toReplyImmediately.push(msg);\n }\n };\n const scheduleFanOut = (msg: ServerMsg) => void toFanOut.push(msg);\n const scheduleReply = (msg: ServerMsg) => void toReplyAfter.push(msg);\n\n for (const msg of messages) {\n this.handleOne(\n session,\n msg,\n replyImmediately,\n scheduleFanOut,\n scheduleReply,\n ctx,\n defer\n );\n }\n\n if (toReplyImmediately.length > 0) {\n session.send(toReplyImmediately);\n toReplyImmediately.length = 0;\n }\n\n if (toFanOut.length > 0) {\n this.sendToOthers(\"(transient)\" as SessionKey, toFanOut, ctx, defer);\n toFanOut.length = 0;\n }\n\n if (toReplyAfter.length > 0) {\n session.send(toReplyAfter);\n toReplyAfter.length = 0;\n }\n }\n\n private handleOne(\n session: BrowserSession<SM, CM>,\n msg: ClientMsg,\n replyImmediately: (\n msg: ServerMsg | ServerMsg[] | jstring<ServerMsg>\n ) => void,\n scheduleFanOut: (msg: ServerMsg) => void,\n scheduleReply: (msg: ServerMsg) => void,\n ctx: C | undefined,\n defer: (p: Promise<void>) => void\n ): void {\n if (!this.mutex.isLocked()) {\n throw new Error(\"Handling messages requires exclusive access\");\n }\n\n switch (msg.type) {\n case ClientMsgCode.UPDATE_PRESENCE: {\n // YYY Maybe consider calling session.sendToOthers() directly here instead of queueing for fan-out?\n scheduleFanOut({\n type: ServerMsgCode.UPDATE_PRESENCE,\n actor: session.actor,\n data: msg.data,\n targetActor: msg.targetActor,\n });\n break;\n }\n\n case ClientMsgCode.BROADCAST_EVENT: {\n // YYY Maybe consider calling session.sendToOthers() directly here instead of queueing for fan-out?\n scheduleFanOut({\n type: ServerMsgCode.BROADCASTED_EVENT,\n actor: session.actor,\n event: msg.event,\n });\n break;\n }\n\n case ClientMsgCode.FETCH_STORAGE: {\n if (session.version >= ProtocolVersion.V8) {\n //\n // Turn storage nodes into WebSocket messages. They get chunked so\n // that each individual WebSocket message won't be too big, but the\n // total number of WebSocket messages needed is kept in check.\n //\n // NOTE: We use iter_nodes_optimized() here to avoid the JS overhead\n // of parsing, mutating, serializing, and measuring each individual\n // node, which can add up in large documents.\n //\n const rawStream = this.storage.driver.iter_nodes_optimized();\n for (const chunk of groupNodesForWebSocketMessages(rawStream)) {\n const frame =\n `{\"type\":${ServerMsgCode.STORAGE_CHUNK},\"nodes\":[${chunk.join(\",\")}]}` as jstring<ServerMsg>;\n replyImmediately(frame);\n }\n replyImmediately({ type: ServerMsgCode.STORAGE_STREAM_END });\n } else {\n replyImmediately({\n type: ServerMsgCode.STORAGE_STATE_V7,\n items: Array.from(this.storage.driver.iter_nodes()),\n });\n }\n break;\n }\n\n case ClientMsgCode.UPDATE_STORAGE: {\n // Bump storage version to indicate data will get mutated\n // A driver can use this information to implement copy-on-write\n // semantics to provide snapshot isolation.\n this.driver.bump_storage_version?.();\n\n const result = this.storage.applyOps(msg.ops);\n\n const opsToForward: ServerWireOp[] = result.flatMap((r) =>\n r.action === \"accepted\" ? [r.op] : []\n );\n\n const opsToSendBack: ServerWireOp[] = result.flatMap(\n (r): ServerWireOp[] => {\n switch (r.action) {\n case \"ignored\":\n // HACK! We send a cleverly composed message, that will act\n // as an acknowledgement to all old clients out there in\n // the wild.\n return r.ignoredOpId !== undefined\n ? [ackIgnoredOp(r.ignoredOpId)]\n : [];\n\n case \"rectified\":\n // The op was already applied earlier; re-acknowledge it with\n // its stored, authoritative position.\n return [r.ackOp, r.fix];\n\n case \"accepted\":\n return r.fix !== undefined ? [r.fix] : [];\n\n // istanbul ignore next\n default:\n return assertNever(r, \"Unhandled case\");\n }\n }\n );\n\n if (opsToForward.length > 0) {\n scheduleFanOut({\n type: ServerMsgCode.UPDATE_STORAGE,\n ops: opsToForward.map(stripOpId),\n });\n scheduleReply({\n type: ServerMsgCode.UPDATE_STORAGE,\n ops: opsToForward,\n });\n }\n\n if (opsToSendBack.length > 0) {\n replyImmediately({\n type: ServerMsgCode.UPDATE_STORAGE,\n ops: opsToSendBack,\n });\n }\n\n if (opsToForward.length > 0) {\n // NOTE! These are being called after *every* handleOne() call\n // currently. Should we not just call these once at the end of\n // handleClientMsgs()?\n const p$ = this.hooks.postClientMsgStorageDidUpdate?.(ctx);\n if (p$) defer(p$);\n }\n break;\n }\n\n case ClientMsgCode.FETCH_YDOC: {\n const vector = msg.vector;\n const guid = msg.guid as Guid | undefined;\n const isV2 = msg.v2;\n const update = this.yjsStorage.getYDocUpdate(\n this.#logger,\n vector,\n guid,\n isV2\n );\n const stateVector = this.yjsStorage.getYStateVector(this.#logger, guid);\n const snapshotHash = this.yjsStorage.getSnapshotHash(this.#logger, {\n guid,\n isV2,\n });\n\n if (update !== null && snapshotHash !== null) {\n replyImmediately({\n type: ServerMsgCode.UPDATE_YDOC,\n update,\n isSync: true, // this is no longer used by the client, instead we use the presence of stateVector\n stateVector,\n guid,\n v2: isV2,\n remoteSnapshotHash: snapshotHash,\n });\n }\n break;\n }\n\n case ClientMsgCode.UPDATE_YDOC: {\n const update = msg.update;\n const guid = msg.guid as Guid | undefined;\n const isV2 = msg.v2;\n const [result, error] = tryCatch(() =>\n this.yjsStorage.addYDocUpdate(this.#logger, update, guid, isV2)\n );\n\n if (error)\n // Ignore any errors\n break;\n\n this.sendToAll(\n {\n type: ServerMsgCode.UPDATE_YDOC,\n update,\n guid,\n isSync: false,\n stateVector: null,\n v2: isV2,\n remoteSnapshotHash: result.snapshotHash,\n },\n ctx,\n defer\n );\n if (result.isUpdated) {\n const p$ = this.hooks.postClientMsgYdocDidUpdate?.(ctx, session);\n if (p$) defer(p$);\n }\n\n break;\n }\n\n // Feed messages\n case FeedMsgCode.FETCH_FEEDS: {\n const fetchMsg = msg;\n const [result, err] = tryCatch(() =>\n this.listFeeds({\n cursor: fetchMsg.cursor,\n since: fetchMsg.since,\n limit: fetchMsg.limit,\n metadata: fetchMsg.metadata,\n })\n );\n if (err) {\n replyImmediately(feedFailureServerMsg(fetchMsg.requestId, err));\n break;\n }\n replyImmediately({\n type: FeedMsgCode.FEEDS_LIST,\n requestId: fetchMsg.requestId,\n feeds: result.feeds,\n nextCursor: result.nextCursor,\n });\n break;\n }\n\n case FeedMsgCode.FETCH_FEED_MESSAGES: {\n const fetchMsg = msg;\n const [result, err] = tryCatch(() =>\n this.listFeedMessages(fetchMsg.feedId, {\n cursor: fetchMsg.cursor,\n since: fetchMsg.since,\n limit: fetchMsg.limit,\n })\n );\n if (err) {\n replyImmediately(feedFailureServerMsg(fetchMsg.requestId, err));\n break;\n }\n replyImmediately({\n type: FeedMsgCode.FEED_MESSAGES_LIST,\n requestId: fetchMsg.requestId,\n feedId: fetchMsg.feedId,\n messages: result.messages,\n nextCursor: result.nextCursor,\n });\n break;\n }\n\n case FeedMsgCode.ADD_FEED: {\n const addMsg = msg;\n const [feed, err] = tryCatch(() =>\n this.createFeed({\n feedId: addMsg.feedId,\n metadata: (addMsg.metadata as Json) ?? {},\n })\n );\n if (err) {\n replyImmediately(feedFailureServerMsg(addMsg.requestId, err));\n break;\n }\n const feedsMsg = {\n type: FeedMsgCode.FEEDS_ADDED,\n feeds: [feed],\n };\n replyImmediately(feedsMsg);\n scheduleFanOut(feedsMsg);\n break;\n }\n\n case FeedMsgCode.UPDATE_FEED: {\n const updateMsg = msg;\n const [, metaErr] = tryCatch(() =>\n this.updateFeedMetadata(updateMsg.feedId, updateMsg.metadata as Json)\n );\n if (metaErr) {\n replyImmediately(feedFailureServerMsg(updateMsg.requestId, metaErr));\n break;\n }\n const feed = this.getFeed(updateMsg.feedId);\n if (!feed) {\n replyImmediately(\n feedRequestFailed(\n updateMsg.requestId,\n FeedRequestErrorCode.FEED_NOT_FOUND\n )\n );\n break;\n }\n const feedsMsg = {\n type: FeedMsgCode.FEEDS_UPDATED,\n feeds: [feed],\n };\n replyImmediately(feedsMsg);\n scheduleFanOut(feedsMsg);\n break;\n }\n\n case FeedMsgCode.DELETE_FEED: {\n const deleteMsg = msg;\n const [, err] = tryCatch(() => this.deleteFeed(deleteMsg.feedId));\n if (err) {\n replyImmediately(feedFailureServerMsg(deleteMsg.requestId, err));\n break;\n }\n const feedDeletedMsg = {\n type: FeedMsgCode.FEED_DELETED,\n feedId: deleteMsg.feedId,\n };\n replyImmediately(feedDeletedMsg);\n scheduleFanOut(feedDeletedMsg);\n break;\n }\n\n case FeedMsgCode.ADD_FEED_MESSAGE: {\n const addMsg = msg;\n const [message, err] = tryCatch(() =>\n this.addFeedMessage(addMsg.feedId, {\n data: addMsg.data as Json,\n id: addMsg.id,\n })\n );\n if (err) {\n replyImmediately(feedFailureServerMsg(addMsg.requestId, err));\n break;\n }\n const feedMessagesMsg = {\n type: FeedMsgCode.FEED_MESSAGES_ADDED,\n feedId: addMsg.feedId,\n messages: [message],\n };\n replyImmediately(feedMessagesMsg);\n scheduleFanOut(feedMessagesMsg);\n break;\n }\n\n case FeedMsgCode.UPDATE_FEED_MESSAGE: {\n const updateMsg = msg;\n const [message, err] = tryCatch(() =>\n this.updateFeedMessage(\n updateMsg.feedId,\n updateMsg.messageId,\n updateMsg.data as Json\n )\n );\n if (err) {\n replyImmediately(feedFailureServerMsg(updateMsg.requestId, err));\n break;\n }\n const feedMessagesMsg = {\n type: FeedMsgCode.FEED_MESSAGES_UPDATED,\n feedId: updateMsg.feedId,\n messages: [message],\n };\n replyImmediately(feedMessagesMsg);\n scheduleFanOut(feedMessagesMsg);\n break;\n }\n\n case FeedMsgCode.DELETE_FEED_MESSAGE: {\n const deleteMsg = msg;\n const [, err] = tryCatch(() =>\n this.deleteFeedMessage(deleteMsg.feedId, deleteMsg.messageId)\n );\n if (err) {\n replyImmediately(feedFailureServerMsg(deleteMsg.requestId, err));\n break;\n }\n const feedMessagesMsg = {\n type: FeedMsgCode.FEED_MESSAGES_DELETED,\n feedId: deleteMsg.feedId,\n messageIds: [deleteMsg.messageId],\n };\n replyImmediately(feedMessagesMsg);\n scheduleFanOut(feedMessagesMsg);\n break;\n }\n\n default: {\n try {\n return assertNever(msg, \"Unrecognized client msg\");\n } catch {\n // Ignore\n }\n }\n }\n }\n}\n\nexport { serialize as serializeServerMsg };\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { JsonObject } from \"@liveblocks/core\";\nimport { raise } from \"@liveblocks/core\";\n\nexport enum LogLevel {\n DEBUG = 0,\n INFO = 1,\n WARNING = 2,\n ERROR = 3,\n}\n\nfunction formatError(err: Error) {\n const prefix = `${err.name}: ${err.message}`;\n return (\n err.stack?.startsWith(prefix) ? err.stack : `${prefix}\\n${err.stack ?? \"\"}`\n ).trimEnd();\n}\n\n/**\n * Inherit from this abstract log target to implement your own custom\n * LogTarget.\n */\nexport abstract class LogTarget {\n public readonly level: LogLevel;\n\n #cache = new WeakMap<JsonObject, string>();\n\n constructor(level: LogLevel | keyof typeof LogLevelNames = LogLevel.INFO) {\n this.level =\n typeof level === \"number\"\n ? level\n : (LogLevelNames[level] ?? LogLevel.INFO);\n }\n\n /** Helper for formatting a log level */\n protected formatLevel(level: LogLevel): string {\n switch (level) {\n case LogLevel.DEBUG:\n return \"debug\";\n case LogLevel.INFO:\n return \"info\";\n case LogLevel.WARNING:\n return \"warn\";\n case LogLevel.ERROR:\n return \"error\";\n default:\n return raise(\"Invalid log level\");\n }\n }\n\n /** Helper for formatting an Arg */\n protected formatArg(arg: string | Error): string {\n return typeof arg === \"object\"\n ? arg instanceof Error\n ? formatError(arg)\n : JSON.stringify(arg)\n : String(arg); // Coerce to string in case TypeScript is bypassed\n }\n\n /**\n * Helper for formatting a Context. Override this in a subclass to change the\n * formatting.\n */\n protected formatContextImpl(context: JsonObject): string {\n const parts = [];\n for (const [k, v] of Object.entries(context ?? {})) {\n if (v !== undefined) {\n // Object, or null, or array\n const sv = typeof v === \"object\" ? JSON.stringify(v) : v;\n parts.push(`${k}=${sv}`);\n }\n }\n return parts.length > 0 ? `[${parts.join(\" \")}]` : \"\";\n }\n\n /**\n * Helper for formatting a Context. Will only compute the string once for\n * every Context instance, and keep its computed string value cached for\n * performance.\n */\n protected formatContext(context: JsonObject): string {\n let formatted = this.#cache.get(context);\n if (formatted === undefined) {\n formatted = this.formatContextImpl(context);\n this.#cache.set(context, formatted);\n }\n return formatted;\n }\n\n /**\n * Implement this in a concrete subclass. The goal is to do whatever to log\n * the given log level, context, and log arg. You'll typically want to\n * utilize the pre-defined helper methods .formatContext() and .formatArg()\n * to implement this.\n */\n abstract log(level: LogLevel, context: JsonObject, arg: string | Error): void;\n}\n\n//\n// Console log target ----------------------------------------------------------\n//\n\nconst CONSOLE_METHOD = {\n [LogLevel.DEBUG]: \"info\",\n [LogLevel.INFO]: \"info\",\n [LogLevel.WARNING]: \"warn\",\n [LogLevel.ERROR]: \"error\",\n} as const;\n\nexport class ConsoleTarget extends LogTarget {\n log(level: LogLevel, context: JsonObject, arg: string | Error): void {\n console[CONSOLE_METHOD[level]](\n this.formatArg(arg),\n this.formatContext(context)\n );\n }\n}\n\n//\n// Logger implementation ------------------------------------------------------\n//\n\n// Friendly names to pass to the constructor\nconst LogLevelNames = {\n debug: LogLevel.DEBUG,\n info: LogLevel.INFO,\n warning: LogLevel.WARNING,\n error: LogLevel.ERROR,\n} as const;\n\ntype LogFn = (arg: string | Error) => void;\n\n/**\n * Structured logger with configurable log targets.\n */\nexport class Logger {\n public readonly debug: LogFn;\n public readonly info: LogFn;\n public readonly warn: LogFn;\n public readonly error: LogFn;\n\n public readonly o: {\n readonly debug?: LogFn;\n readonly info?: LogFn;\n readonly warn?: LogFn;\n readonly error?: LogFn;\n };\n\n private readonly _context: JsonObject;\n private readonly _targets: readonly LogTarget[];\n\n constructor(\n target: LogTarget | readonly LogTarget[] = new ConsoleTarget(),\n context: JsonObject = {}\n ) {\n this._context = context;\n this._targets = Array.isArray(target) ? target : [target];\n\n const minLevel: number = Math.min(...this._targets.map((t) => t.level));\n\n const noop = () => {};\n const makeLogFn = (lvl: LogLevel) => (arg: string | Error) =>\n this._targets.forEach((target) => {\n if (target.level <= lvl) {\n target.log(lvl, this._context, arg);\n }\n });\n\n this.o = {\n debug: minLevel <= LogLevel.DEBUG ? makeLogFn(LogLevel.DEBUG) : undefined,\n info: minLevel <= LogLevel.INFO ? makeLogFn(LogLevel.INFO) : undefined,\n warn:\n minLevel <= LogLevel.WARNING ? makeLogFn(LogLevel.WARNING) : undefined,\n error: minLevel <= LogLevel.ERROR ? makeLogFn(LogLevel.ERROR) : undefined,\n };\n\n this.debug = this.o.debug ?? noop;\n this.info = this.o.info ?? noop;\n this.warn = this.o.warn ?? noop;\n this.error = this.o.error ?? noop;\n }\n\n /**\n * Creates a new Logger instance with the given extra context applied. All\n * log calls made from that new Logger will carry all current _and_ the extra\n * context, with the extra context taking precedence. Assign an explicit\n * `undefined` value to a key to \"remove\" it from the context.\n */\n withContext(extra: JsonObject): Logger {\n const combined: JsonObject = { ...this._context, ...extra };\n return new Logger(this._targets, combined);\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\n/* eslint-disable @typescript-eslint/explicit-module-boundary-types */\nimport type {\n CompactNode,\n Json,\n JsonObject,\n NodeMap,\n NodeStream,\n PlainLsonObject,\n SerializedChild,\n SerializedCrdt,\n SerializedObject,\n SerializedRootObject,\n} from \"@liveblocks/core\";\nimport {\n asPos,\n CrdtType,\n isRootStorageNode,\n nn,\n nodeStreamToCompactNodes,\n} from \"@liveblocks/core\";\nimport { ifilter, imap } from \"itertools\";\n\nimport type { YDocId } from \"~/decoders/y-types\";\nimport { plainLsonToNodeStream } from \"~/formats/PlainLson\";\nimport type {\n IReadableSnapshot,\n IStorageDriver,\n LeasedSession,\n ListFeedMessagesOptions,\n ListFeedMessagesResult,\n ListFeedsOptions,\n ListFeedsResult,\n} from \"~/interfaces\";\nimport { NestedMap } from \"~/lib/NestedMap\";\nimport { quote } from \"~/lib/text\";\nimport { makeInMemorySnapshot } from \"~/makeInMemorySnapshot\";\nimport type { Feed, FeedMessage, jstring, Pos } from \"~/types\";\n\nfunction buildRevNodes(nodeStream: NodeStream) {\n const result = new NestedMap<string, string, string>();\n for (const node of nodeStream) {\n if (isRootStorageNode(node)) continue;\n\n // Highest node id wins in case of conflict (deterministic across backends)\n const [id, crdt] = node;\n const existing = result.get(crdt.parentId, crdt.parentKey);\n if (existing === undefined || id > existing) {\n result.set(crdt.parentId, crdt.parentKey, id);\n }\n }\n return result;\n}\n\n/**\n * Builds the reverse node index, and corrects any data corruption found\n * along the way.\n */\nfunction buildReverseLookup(nodes: NodeMap) {\n const revNodes = buildRevNodes(nodes as NodeStream);\n\n const queue: string[] = [\"root\"];\n const reachableNodes: Set<string> = new Set();\n\n while (queue.length > 0) {\n const nodeId = queue.pop()!;\n const node = nn(nodes.get(nodeId));\n\n if (node.type === CrdtType.OBJECT) {\n for (const key of revNodes.keysAt(nodeId)) {\n delete node.data[key]; // Remove static data that conflicts with child nodes\n }\n }\n\n if (node.type !== CrdtType.REGISTER) {\n queue.push(...revNodes.valuesAt(nodeId));\n } else {\n const parent = nodes.get(node.parentId);\n if (parent?.type === CrdtType.OBJECT) {\n continue;\n }\n }\n\n reachableNodes.add(nodeId);\n }\n\n // Delete unreachable nodes (safe to delete from Map during iteration)\n let deletedCount = 0;\n for (const [id] of nodes) {\n if (!reachableNodes.has(id)) {\n nodes.delete(id);\n deletedCount++;\n }\n }\n\n // If no nodes were dropped (99% happy path), revNodes is correct already.\n // Otherwise, recompute it.\n return deletedCount === 0 ? revNodes : buildRevNodes(nodes as NodeStream);\n}\n\nfunction hasStaticDataAt(\n node: SerializedCrdt,\n key: string\n): node is SerializedObject | SerializedRootObject {\n return (\n node.type === CrdtType.OBJECT &&\n Object.prototype.hasOwnProperty.call(node.data, key) &&\n node.data[key] !== undefined\n );\n}\n\n/**\n * The lazily-initialized part of the driver that implements all node-related\n * APIs.\n */\ntype NodesAPI = Pick<\n IStorageDriver,\n | \"get_node\"\n | \"iter_nodes\"\n | \"iter_nodes_optimized\"\n | \"has_node\"\n | \"get_child_at\"\n | \"has_child_at\"\n | \"get_next_sibling\"\n | \"get_last_sibling\"\n | \"set_child\"\n | \"move_sibling\"\n | \"delete_node\"\n | \"delete_child_key\"\n | \"set_object_data\"\n | \"get_snapshot\"\n>;\n\n/**\n * Implements the most basic in-memory store. Used if no explicit store is\n * provided.\n */\nexport class InMemoryDriver implements IStorageDriver {\n private _nextActor;\n private _nodes: NodeMap;\n private _nodesApi?: NodesAPI;\n private _metadb: Map<string, Json>;\n private _ydb: Map<string, Uint8Array>;\n private _leasedSessions: Map<string, LeasedSession>;\n private _feeds: Map<string, Feed>;\n private _feedMessages: Map<string, FeedMessage>; // Key: `${feedId}:${messageId}`\n\n constructor(options?: {\n initialActor?: number;\n initialNodes?: Iterable<[string, SerializedCrdt]>;\n }) {\n this._nodes = new Map();\n this._metadb = new Map();\n this._ydb = new Map();\n this._leasedSessions = new Map();\n this._feeds = new Map();\n this._feedMessages = new Map();\n\n this._nextActor = options?.initialActor ?? -1;\n\n for (const [key, value] of options?.initialNodes ?? []) {\n this._nodes.set(key, value);\n }\n }\n\n raw_iter_nodes() {\n return this._nodes[Symbol.iterator]();\n }\n\n reinitialize(): void {\n this._nodesApi = undefined;\n }\n\n /** Deletes all nodes and replaces them with the given document. */\n DANGEROUSLY_reset_nodes(doc: PlainLsonObject) {\n // Invalidate the cached node API: its reverse-lookup index is built from\n // the node map we're about to replace. The next access rebuilds it.\n this.reinitialize();\n\n this._nodes.clear();\n for (const [id, node] of plainLsonToNodeStream(doc)) {\n this._nodes.set(id, node);\n }\n }\n\n get_meta(key: string) {\n return this._metadb.get(key);\n }\n put_meta(key: string, value: Json) {\n this._metadb.set(key, value);\n }\n delete_meta(key: string) {\n this._metadb.delete(key);\n }\n\n list_leased_sessions() {\n return this._leasedSessions.entries();\n }\n\n get_leased_session(sessionId: string) {\n return this._leasedSessions.get(sessionId);\n }\n\n put_leased_session(session: LeasedSession) {\n this._leasedSessions.set(session.sessionId, session);\n }\n\n delete_leased_session(sessionId: string) {\n this._leasedSessions.delete(sessionId);\n }\n\n takeRowsWritten(): number {\n return 0;\n }\n\n // ---------------------------------------------------------------------------\n // Feed APIs\n // ---------------------------------------------------------------------------\n\n list_feeds(options?: ListFeedsOptions): ListFeedsResult {\n const limit = Math.min(options?.limit ?? 20, 100);\n const since = options?.since;\n const cursor = options?.cursor;\n const metadata = options?.metadata;\n\n let feeds: Feed[] = [];\n\n // Collect all feeds\n for (const [_, feed] of this._feeds.entries()) {\n // Apply metadata filtering\n if (metadata !== undefined) {\n const feedMetadata = feed.metadata as JsonObject;\n let matches = true;\n for (const [key, value] of Object.entries(metadata)) {\n if (feedMetadata[key] !== value) {\n matches = false;\n break;\n }\n }\n if (!matches) continue;\n }\n\n // Apply since filter (uses createdAt for stable ordering)\n if (since !== undefined && feed.createdAt < since) {\n continue;\n }\n\n feeds.push(feed);\n }\n\n // Sort by createdAt descending, then by feedId descending\n feeds.sort((a, b) => {\n if (b.createdAt !== a.createdAt) {\n return b.createdAt - a.createdAt;\n }\n return b.feedId.localeCompare(a.feedId);\n });\n\n // Apply cursor-based pagination\n if (cursor !== undefined) {\n try {\n // Simple base64url decode (using btoa/atob for browser compatibility)\n const decoded = JSON.parse(\n decodeURIComponent(\n escape(atob(cursor.replace(/-/g, \"+\").replace(/_/g, \"/\")))\n )\n ) as [string, number];\n const [cursorFeedId, cursorCreatedAt] = decoded;\n feeds = feeds.filter((s) => {\n return (\n s.createdAt < cursorCreatedAt ||\n (s.createdAt === cursorCreatedAt && s.feedId < cursorFeedId)\n );\n });\n } catch {\n // Invalid cursor, ignore it\n }\n }\n\n // Apply limit (get one extra to determine if there's a next page)\n let nextCursor: string | undefined;\n if (feeds.length > limit) {\n feeds = feeds.slice(0, limit);\n // Create cursor from last returned item\n const last = feeds[feeds.length - 1];\n if (last) {\n const cursorData: [string, number] = [last.feedId, last.createdAt];\n // Simple base64url encode\n nextCursor = btoa(\n unescape(encodeURIComponent(JSON.stringify(cursorData)))\n )\n .replace(\"/\", \"_\")\n .replace(\"+\", \"-\")\n .replace(/=+$/, \"\");\n }\n }\n\n return { feeds, nextCursor };\n }\n\n get_feed(feedId: string): Feed | undefined {\n const feed = this._feeds.get(feedId);\n if (feed === undefined) {\n return undefined;\n }\n\n return feed;\n }\n\n create_feed(feed: Feed): void {\n // Check if feed already exists\n if (this._feeds.has(feed.feedId)) {\n throw new Error(`Feed ${feed.feedId} already exists`);\n }\n\n // Store feed metadata\n this._feeds.set(feed.feedId, {\n feedId: feed.feedId,\n metadata: feed.metadata,\n createdAt: feed.createdAt,\n updatedAt: feed.updatedAt,\n });\n }\n\n update_feed_metadata(feedId: string, metadata: Json): void {\n const existing = this._feeds.get(feedId);\n if (existing === undefined) {\n throw new Error(`Feed ${feedId} not found`);\n }\n\n this._feeds.set(feedId, {\n ...existing,\n metadata,\n });\n }\n\n delete_feed(feedId: string): void {\n // Delete all messages for this feed\n const messageKeys: string[] = [];\n for (const [key] of this._feedMessages.entries()) {\n if (key.startsWith(`${feedId}:`)) {\n messageKeys.push(key);\n }\n }\n for (const key of messageKeys) {\n this._feedMessages.delete(key);\n }\n\n // Delete feed\n this._feeds.delete(feedId);\n }\n\n list_feed_messages(\n feedId: string,\n options?: ListFeedMessagesOptions\n ): ListFeedMessagesResult {\n const limit = Math.min(options?.limit ?? 20, 100);\n const since = options?.since;\n const cursor = options?.cursor;\n\n let messages: FeedMessage[] = [];\n\n // Collect all messages for this feed\n const prefix = `${feedId}:`;\n for (const [key, message] of this._feedMessages.entries()) {\n if (key.startsWith(prefix)) {\n // Apply since filter (uses createdAt for stable ordering)\n if (since !== undefined && message.createdAt < since) {\n continue;\n }\n\n messages.push(message);\n }\n }\n\n // Sort by createdAt descending, then by id descending\n messages.sort((a, b) => {\n if (b.createdAt !== a.createdAt) {\n return b.createdAt - a.createdAt;\n }\n return b.id.localeCompare(a.id);\n });\n\n // Apply cursor-based pagination\n if (cursor !== undefined) {\n try {\n const decoded = JSON.parse(\n decodeURIComponent(\n escape(atob(cursor.replace(/-/g, \"+\").replace(/_/g, \"/\")))\n )\n ) as [string, number];\n const [cursorMessageId, cursorCreatedAt] = decoded;\n messages = messages.filter((m) => {\n return (\n m.createdAt < cursorCreatedAt ||\n (m.createdAt === cursorCreatedAt && m.id < cursorMessageId)\n );\n });\n } catch {\n // Invalid cursor, ignore it\n }\n }\n\n // Apply limit (get one extra to determine if there's a next page)\n let nextCursor: string | undefined;\n if (messages.length > limit) {\n messages = messages.slice(0, limit);\n // Create cursor from last returned item\n const last = messages[messages.length - 1];\n if (last) {\n const cursorData: [string, number] = [last.id, last.createdAt];\n nextCursor = btoa(\n unescape(encodeURIComponent(JSON.stringify(cursorData)))\n )\n .replace(\"/\", \"_\")\n .replace(\"+\", \"-\")\n .replace(/=+$/, \"\");\n }\n }\n\n return { messages, nextCursor };\n }\n\n add_feed_message(feedId: string, message: FeedMessage): void {\n // Verify feed exists\n const feed = this._feeds.get(feedId);\n if (feed === undefined) {\n throw new Error(`Feed ${feedId} not found`);\n }\n\n this._feedMessages.set(`${feedId}:${message.id}`, message);\n }\n\n update_feed_message(\n feedId: string,\n messageId: string,\n data: Json,\n timestamp?: number\n ): FeedMessage {\n const key = `${feedId}:${messageId}`;\n const message = this._feedMessages.get(key);\n\n if (message === undefined) {\n throw new Error(`Feed message ${messageId} not found in feed ${feedId}`);\n }\n\n const effectiveTimestamp = timestamp ?? Date.now();\n if (effectiveTimestamp < message.updatedAt) {\n return message;\n }\n\n const updatedMessage: FeedMessage = {\n ...message,\n updatedAt: effectiveTimestamp,\n data,\n };\n\n this._feedMessages.set(key, updatedMessage);\n\n return updatedMessage;\n }\n\n delete_feed_message(feedId: string, messageId: string): void {\n this._feedMessages.delete(`${feedId}:${messageId}`);\n }\n\n next_actor() {\n return ++this._nextActor;\n }\n\n iter_y_updates(docId: YDocId) {\n const prefix = `${docId}@|@`;\n return imap(\n ifilter(this._ydb.entries(), ([k]) => k.startsWith(prefix)),\n ([k, v]) => [k.slice(prefix.length), v] as [string, Uint8Array]\n );\n }\n write_y_updates(docId: YDocId, key: string, data: Uint8Array) {\n this._ydb.set(`${docId}@|@${key}`, data);\n }\n delete_y_updates(docId: YDocId, keys: string[]) {\n for (const key of keys) {\n this._ydb.delete(`${docId}@|@${key}`);\n }\n }\n\n /** @private Only use this in unit tests, never in production. */\n DANGEROUSLY_wipe_all_y_updates() {\n this._ydb.clear();\n }\n\n // ---------------------------------------------------------------------------\n // Node APIs\n // ---------------------------------------------------------------------------\n\n /**\n * The lazily-built node API. Building it ensures the root node exists and\n * constructs the reverse-lookup index. Invalidated by\n * DANGEROUSLY_reset_nodes(), after which the next access rebuilds it.\n */\n private get loadedApi(): NodesAPI {\n return (this._nodesApi ??= this._loadNodesApi());\n }\n\n get_node(id: string): SerializedCrdt | undefined {\n return this.loadedApi.get_node(id);\n }\n\n iter_nodes(): NodeStream {\n return this.loadedApi.iter_nodes();\n }\n\n iter_nodes_optimized(): Iterable<jstring<CompactNode>> {\n return this.loadedApi.iter_nodes_optimized();\n }\n\n has_node(id: string): boolean {\n return this.loadedApi.has_node(id);\n }\n\n get_child_at(parentId: string, parentKey: string): string | undefined {\n return this.loadedApi.get_child_at(parentId, parentKey);\n }\n\n has_child_at(parentId: string, parentKey: string): boolean {\n return this.loadedApi.has_child_at(parentId, parentKey);\n }\n\n get_next_sibling(parentId: string, pos: Pos): Pos | undefined {\n return this.loadedApi.get_next_sibling(parentId, pos);\n }\n\n get_last_sibling(parentId: string): Pos | undefined {\n return this.loadedApi.get_last_sibling(parentId);\n }\n\n set_child(id: string, node: SerializedChild, allowOverwrite?: boolean): void {\n this.loadedApi.set_child(id, node, allowOverwrite);\n }\n\n move_sibling(id: string, newPos: Pos): void {\n this.loadedApi.move_sibling(id, newPos);\n }\n\n delete_node(id: string): void {\n this.loadedApi.delete_node(id);\n }\n\n delete_child_key(id: string, key: string): void {\n this.loadedApi.delete_child_key(id, key);\n }\n\n set_object_data(\n id: string,\n data: JsonObject,\n allowOverwrite?: boolean\n ): void {\n this.loadedApi.set_object_data(id, data, allowOverwrite);\n }\n\n get_snapshot(lowMemory?: boolean): IReadableSnapshot {\n return this.loadedApi.get_snapshot(lowMemory);\n }\n\n private _loadNodesApi(): NodesAPI {\n // For the in-memory backend, this._nodes IS the \"on-disk\" storage,\n // so we operate on it directly (no separate cache needed).\n const nodes = this._nodes;\n if (!nodes.has(\"root\")) {\n nodes.set(\"root\", { type: CrdtType.OBJECT, data: {} });\n }\n\n const revNodes = buildReverseLookup(nodes);\n\n function get_next_sibling(parentId: string, pos: Pos): Pos | undefined {\n let nextPos: Pos | undefined;\n // Find the smallest position greater than current\n for (const siblingKey of revNodes.keysAt(parentId)) {\n const siblingPos = asPos(siblingKey);\n if (\n siblingPos > pos &&\n (nextPos === undefined || siblingPos < nextPos)\n ) {\n nextPos = siblingPos;\n }\n }\n return nextPos;\n }\n\n function get_last_sibling(parentId: string): Pos | undefined {\n let lastPos: Pos | undefined;\n // Find the largest position under this parent\n for (const siblingKey of revNodes.keysAt(parentId)) {\n const siblingPos = asPos(siblingKey);\n if (lastPos === undefined || siblingPos > lastPos) {\n lastPos = siblingPos;\n }\n }\n return lastPos;\n }\n\n /**\n * Inserts a node in the storage tree, deleting any nodes that already exist\n * under this key (including all of its children), if any.\n */\n function set_child(\n id: string,\n node: SerializedChild,\n allowOverwrite = false\n ): void {\n const parentNode = nodes.get(node.parentId);\n // Reject orphans - parent must exist\n if (parentNode === undefined) {\n throw new Error(`No such parent ${quote(node.parentId)}`);\n }\n\n if (\n node.type === CrdtType.REGISTER &&\n parentNode.type === CrdtType.OBJECT\n ) {\n throw new Error(\"Cannot add register under object\");\n }\n\n const conflictingSiblingId = revNodes.get(node.parentId, node.parentKey);\n if (conflictingSiblingId !== id) {\n // Conflict!\n const parentNode = nodes.get(node.parentId);\n const hasConflictingData =\n parentNode !== undefined &&\n hasStaticDataAt(parentNode, node.parentKey);\n if (conflictingSiblingId !== undefined || hasConflictingData) {\n if (allowOverwrite) {\n delete_child_key(node.parentId, node.parentKey);\n } else {\n throw new Error(`Key ${quote(node.parentKey)} already exists`); // prettier-ignore\n }\n }\n\n // Finally, modify revNodes\n revNodes.set(node.parentId, node.parentKey, id);\n }\n\n nodes.set(id, node);\n }\n\n /**\n * Conceptually this is like \"detaching\" the node from its parent, and\n * \"reattaching\" it at the new position.\n *\n * However, this is a native operation, because doing a naive\n * delete-then-insert would would immediately destroy all (grand)children\n * when it's deleted.\n */\n function move_sibling(id: string, newPos: Pos): void {\n const node = nodes.get(id);\n if (node?.parentId === undefined) {\n return;\n }\n\n // If there is a conflicting sibling at the new position, disallow the move\n if (revNodes.has(node.parentId, newPos))\n throw new Error(`Pos ${quote(newPos)} already taken`); // prettier-ignore\n\n revNodes.delete(node.parentId, node.parentKey);\n const newNode = { ...node, parentKey: newPos };\n nodes.set(id, newNode);\n revNodes.set(node.parentId, newPos, id);\n }\n\n /**\n * Sets some static data on a node. The node must be an OBJECT node, or this\n * method will be a no-op.\n *\n * If any keys exist that also conflict with a child node, then the conflict\n * mode will determine what will happen. By default, an error will be thrown.\n * But if `allowOverwrite` is set to true, the conflicting child node (and\n * its entire subtree) will be deleted to make room for the new static data.\n */\n function set_object_data(\n id: string,\n data: JsonObject,\n allowOverwrite = false\n ): void {\n const node = nodes.get(id);\n if (node?.type !== CrdtType.OBJECT) {\n // Nothing to do\n return;\n }\n\n for (const key of Object.keys(data)) {\n // Handle if conflict!\n const childId = revNodes.get(id, key);\n if (childId !== undefined) {\n if (allowOverwrite) {\n delete_node(childId);\n } else {\n throw new Error(`Child node already exists under ${quote(key)}`); // prettier-ignore\n }\n }\n }\n\n nodes.set(id, { ...node, data: { ...node.data, ...data } });\n }\n\n /**\n * Delete a node from the tree, including all of its children.\n */\n function delete_node(id: string): void {\n const node = nodes.get(id);\n if (node?.parentId === undefined) {\n return;\n }\n\n // Delete the entry in the parent's children administration for this node\n revNodes.delete(node.parentId, node.parentKey);\n\n // Now proceed to deleting the node tree recursively\n const queue = [id];\n while (queue.length > 0) {\n const currid = queue.pop()!;\n queue.push(...revNodes.valuesAt(currid));\n nodes.delete(currid);\n revNodes.deleteAll(currid);\n }\n }\n\n /**\n * Deletes the child key under a given node, whether it's a static object\n * field, or a child node.\n */\n function delete_child_key(id: string, key: string): void {\n // At most one of these will do something, the other is a no-op\n const node = nodes.get(id);\n if (node !== undefined && hasStaticDataAt(node, key)) {\n const { [key]: _, ...rest } = node.data;\n nodes.set(id, { ...node, data: rest });\n }\n\n const childId = revNodes.get(id, key);\n if (childId !== undefined) {\n delete_node(childId);\n }\n }\n\n const api: NodesAPI = {\n /**\n * Return the node with the given id, or undefined if no such node exists.\n * Must always return a valid root node for id=\"root\", even if empty.\n */\n get_node: (id) => nodes.get(id),\n\n /**\n * Yield all nodes as [id, node] pairs. Must always include the root node.\n */\n iter_nodes: () => nodes as NodeStream,\n\n /**\n * Yield each node as a pre-built CompactNode JSON tuple string.\n *\n * This implementation IS the canonical reference for the invariant:\n * iter_nodes_optimized` ≡ `nodeStreamToCompactNodes(iter_nodes())\n */\n *iter_nodes_optimized() {\n for (const compact of nodeStreamToCompactNodes(nodes as NodeStream)) {\n yield JSON.stringify(compact) as jstring<CompactNode>;\n }\n },\n\n /**\n * Return true iff a node with the given id exists. Must return true for \"root\".\n */\n has_node: (id) => nodes.has(id),\n\n /**\n * Return the id of the child node at (parentId, parentKey), or undefined if\n * none. Only checks child nodes registered via set_child, NOT static data\n * keys on OBJECT nodes.\n */\n get_child_at: (id, key) => revNodes.get(id, key),\n\n /**\n * Return true iff a child node exists at (parentId, parentKey). Static data\n * keys on OBJECT nodes do not count—return false for those.\n */\n has_child_at: (id, key) => revNodes.has(id, key),\n\n /**\n * Return the position of the closest sibling \"to the right\" of `pos` under\n * parentId, or undefined if no such sibling exists. The given `pos` may, but\n * does not have to exist already. Positions compare lexicographically.\n */\n get_next_sibling,\n\n /**\n * Return the position of the last (rightmost) child under parentId, or\n * undefined if the node has no children.\n */\n get_last_sibling,\n\n /**\n * Insert a child node with the given id.\n *\n * If allowOverwrite=false (default): throw if a node with this id exists.\n * If allowOverwrite=true: replace any existing node at this id, deleting its\n * entire subtree if it has children.\n */\n set_child,\n\n /**\n * Change a node's parentKey, effectively repositioning the node within its\n * parent. The new position must be free.\n * Throw if another node already occupies (parentId, newPos).\n */\n move_sibling,\n\n /**\n * Delete a node and its entire subtree recursively.\n * Ignore if id=\"root\" (root is immortal).\n */\n delete_node,\n\n /**\n * Delete a key from node `id`. Handle two cases:\n *\n * 1. If id is an OBJECT with `key` in its data: remove that data field.\n * 2. If a child exists at (id, key): delete that child and all its\n * descendants recursively.\n *\n * No-op if neither applies or if the node doesn't exist.\n */\n delete_child_key,\n\n /**\n * Replace the data object of an OBJECT node.\n *\n * If allowOverwrite=false (default): throw if any key in `data` conflicts\n * with an existing child's parentKey.\n * If allowOverwrite=true: first delete any conflicting children (and their\n * entire subtrees), then set the data.\n */\n set_object_data,\n\n /**\n * Return a readable snapshot of the storage tree.\n *\n * @param lowMemory When true, the call site hints that the snapshot should\n * be optimized for lower memory consumption, even if that means slower\n * access.\n */\n get_snapshot(_lowMemory?: boolean): IReadableSnapshot {\n return makeInMemorySnapshot(nodes);\n },\n };\n return api;\n }\n}\n\nexport function makeNewInMemoryDriver(options?: {\n initialActor?: number;\n initialNodes?: Iterable<[string, SerializedCrdt]>;\n}): IStorageDriver {\n return new InMemoryDriver(options);\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\n/**\n * Wraps single-quotes around any string value. Useful for displaying field\n * names or other identifiers in error messages or logs.\n *\n * Examples:\n * quote(\"hi\") // \"'hi'\"\n * quote(\"i'm\") // \"'i'm'\"\n *\n * Note: no \"escaping\" happens here to the string value. This is because this\n * is intended to be used for human consumption, not machine consumption.\n */\nexport function quote(value: string | undefined): string {\n return value !== undefined ? `'${value}'` : \"???\";\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { SerializedChild, SerializedCrdt } from \"@liveblocks/core\";\nimport {\n asPos,\n assertNever,\n CrdtType,\n makePosition,\n OpCode,\n} from \"@liveblocks/core\";\n\nimport type { IStorageDriver } from \"~/interfaces\";\nimport type {\n ClientWireOp,\n CreateOp,\n DeleteCrdtOp,\n DeleteObjectKeyOp,\n FixOp,\n HasOpId,\n SetParentKeyOp,\n UpdateObjectOp,\n} from \"~/protocol\";\nimport type { Pos } from \"~/types\";\n\n/**\n * The three possible outcomes of applying a client op. They differ along\n * when the op (first) changed storage state, who hears about it, and what\n * gets sent back to the originating client:\n *\n * | | state change | fan out to others | reply to sender |\n * |-------------|--------------|-------------------|------------------|\n * | OpAccepted | now | yes | ack echo (+ fix) |\n * | OpRectified | in the past | no | ack echo + fix |\n * | OpIgnored | never | no | bare (H)Ack |\n */\ntype ApplyOpResult = OpAccepted | OpIgnored | OpRectified;\n\nexport type OpAccepted = {\n action: \"accepted\";\n op: ClientWireOp;\n fix?: FixOp;\n};\n\nexport type OpIgnored = {\n action: \"ignored\";\n ignoredOpId?: string;\n};\n\nexport type OpRectified = {\n action: \"rectified\";\n /**\n * Echo of the client's op, with the stored, authoritative parentKey. Sent\n * back to the originating client as the acknowledgement, instead of the\n * bare (H)Ack. Used for re-sent CREATE ops whose node the server already\n * stored: the echo carries the authoritative position, so the client can\n * correct any optimistic local position it may have predicted while the op\n * was pending. Never fanned out to others: they already received the op\n * when it was originally accepted.\n */\n ackOp: CreateOp & HasOpId;\n /**\n * A corrective op to send back to the originating client, stating that\n * same authoritative position (see ackOp).\n */\n fix: FixOp;\n};\n\nfunction accept(op: ClientWireOp, fix?: FixOp): OpAccepted {\n return { action: \"accepted\", op, fix };\n}\n\nfunction ignore(ignoredOp: ClientWireOp): OpIgnored {\n return { action: \"ignored\", ignoredOpId: ignoredOp.opId };\n}\n\nfunction rectify(op: CreateOp & HasOpId, parentKey: string): OpRectified {\n return {\n action: \"rectified\",\n ackOp: { ...op, parentKey },\n fix: { type: OpCode.SET_PARENT_KEY, id: op.id, parentKey },\n };\n}\n\nfunction nodeFromCreateChildOp(op: CreateOp): SerializedChild {\n switch (op.type) {\n case OpCode.CREATE_LIST:\n return {\n type: CrdtType.LIST,\n parentId: op.parentId,\n parentKey: op.parentKey,\n };\n\n case OpCode.CREATE_MAP:\n return {\n type: CrdtType.MAP,\n parentId: op.parentId,\n parentKey: op.parentKey,\n };\n\n case OpCode.CREATE_OBJECT:\n return {\n type: CrdtType.OBJECT,\n parentId: op.parentId,\n parentKey: op.parentKey,\n data: op.data,\n };\n\n case OpCode.CREATE_REGISTER:\n return {\n type: CrdtType.REGISTER,\n parentId: op.parentId,\n parentKey: op.parentKey,\n data: op.data,\n };\n\n // istanbul ignore next\n default:\n return assertNever(op, \"Unknown op code\");\n }\n}\n\nexport class Storage {\n // The actual underlying storage API (could be backed by in-memory store,\n // SQLite, Postgres, etc.)\n public readonly driver: IStorageDriver;\n\n constructor(driver: IStorageDriver) {\n this.driver = driver;\n }\n\n // -------------------------------------------------------------------------\n // Public API (for Storage)\n // -------------------------------------------------------------------------\n\n // REFACTOR NOTE: Eventually raw_iter_nodes has to be removed here\n raw_iter_nodes(): Iterable<[string, SerializedCrdt]> {\n return this.driver.raw_iter_nodes();\n }\n\n /**\n * Applies a batch of Ops.\n */\n applyOps(ops: ClientWireOp[]): ApplyOpResult[] {\n const results: ApplyOpResult[] = [];\n for (const op of ops) {\n results.push(this.applyOp(op));\n }\n return results;\n }\n\n // -------------------------------------------------------------------------\n // Private APIs (for Storage)\n // -------------------------------------------------------------------------\n\n /**\n * Applies a single Op.\n */\n private applyOp(op: ClientWireOp): ApplyOpResult {\n switch (op.type) {\n case OpCode.CREATE_LIST:\n case OpCode.CREATE_MAP:\n case OpCode.CREATE_REGISTER:\n case OpCode.CREATE_OBJECT:\n return this.applyCreateOp(op);\n\n case OpCode.UPDATE_OBJECT:\n return this.applyUpdateObjectOp(op);\n\n case OpCode.SET_PARENT_KEY:\n return this.applySetParentKeyOp(op);\n\n case OpCode.DELETE_OBJECT_KEY:\n return this.applyDeleteObjectKeyOp(op);\n\n case OpCode.DELETE_CRDT:\n return this.applyDeleteCrdtOp(op);\n\n // istanbul ignore next\n default:\n if (process.env.NODE_ENV === \"production\") {\n return ignore(op);\n } else {\n return assertNever(op, \"Invalid op\");\n }\n }\n }\n\n private applyCreateOp(op: CreateOp & HasOpId): ApplyOpResult {\n if (this.driver.has_node(op.id)) {\n // Node already exists, meaning this op was already applied earlier\n // (e.g. it was re-sent after a reconnect because its original ack\n // never arrived), so it won't get applied again. For pushed list\n // items, rectify: send the stored, authoritative position back to the\n // originating client, because a bare ack would leave any optimistic\n // local position prediction on that client uncorrected. Only pushes\n // need this: they're the only ops the client locally repositions while\n // pending. Note that unlike acceptAndFix, rectifying happens even when\n // the stored key equals the op's key: the client's *local* key may\n // have drifted from both, and the server cannot see that.\n if (op.intent === \"push\") {\n const stored = this.driver.get_node(op.id);\n if (\n stored?.parentId !== undefined &&\n this.driver.get_node(stored.parentId)?.type === CrdtType.LIST\n ) {\n return rectify(op, stored.parentKey);\n }\n }\n return ignore(op);\n }\n\n const node = nodeFromCreateChildOp(op);\n\n const parent = this.driver.get_node(node.parentId);\n if (parent === undefined) {\n // Parent does not exist because the op is invalid or because it was deleted in race condition.\n return ignore(op);\n }\n\n // How to create this node in the node map depends on the parent node's type\n switch (parent.type) {\n case CrdtType.OBJECT:\n // Register children under object nodes are forbidden. We'll simply\n // ignore these Ops. This matches the eventual storage behavior: if\n // we'd persist them, they would get ignored when re-loading the\n // persisted room data into memory the next time the room loads.\n if (op.type === OpCode.CREATE_REGISTER) {\n return ignore(op);\n }\n // fall through\n\n case CrdtType.MAP:\n // Children of maps and objects require no special needs\n this.driver.set_child(op.id, node, true);\n return accept(op);\n\n case CrdtType.LIST:\n // List items need special handling around conflicting resolution,\n // which depends on the users intention\n return this.createChildAsListItem(op, node);\n\n case CrdtType.REGISTER:\n // It's illegal for registers to have children\n return ignore(op);\n\n // istanbul ignore next\n default:\n return assertNever(parent, \"Unhandled CRDT type\");\n }\n }\n\n private createChildAsListItem(\n op: CreateOp & HasOpId,\n node: SerializedChild\n ): ApplyOpResult {\n // The default intent, when not explicitly provided, is to insert, not set,\n // into the list.\n const intent: \"insert\" | \"set\" | \"push\" = op.intent ?? \"insert\";\n\n // istanbul ignore else\n if (intent === \"insert\") {\n // Insert at the client's preferred position, resolving any collision to a\n // nearby free slot.\n return this.acceptAndFix(op, node, this.insertIntoList(op.id, node));\n } else if (intent === \"push\") {\n // Server-authoritative append: place the node after the authoritative\n // end of the list (see `appendToList`), regardless of the client's preference.\n return this.acceptAndFix(op, node, this.appendToList(op.id, node));\n } else if (intent === \"set\") {\n let fix: FixOp | undefined;\n\n // The intent here is to \"set\", not insert, into the list, replacing the\n // existing item that\n\n // Special handling required here. They will include a \"deletedId\" that\n // points to the object they expect to be replacing. If in the mean time,\n // that object disappeared there (because it was moved, for example), be\n // sure to delete it anyway.\n // We should not just trust the given value, because we're about to\n // delete a node. It's only safe to delete the node if it indeed is\n // a sibling of the current node.\n const deletedId =\n op.deletedId !== undefined &&\n op.deletedId !== op.id &&\n this.driver.get_node(op.deletedId)?.parentId === node.parentId\n ? op.deletedId\n : undefined;\n\n if (deletedId !== undefined) {\n this.driver.delete_node(deletedId);\n }\n\n const prevItemId = this.driver.get_child_at(\n node.parentId,\n node.parentKey\n );\n if (prevItemId !== undefined && prevItemId !== deletedId) {\n // If this \"set\" operation indeed removed an item, but it wasn't the\n // expected `deletedId`, let the invoking client know that they'll\n // have to delete this object, too.\n fix = {\n type: OpCode.DELETE_CRDT,\n id: prevItemId,\n };\n }\n\n this.driver.set_child(op.id, node, true);\n\n return accept(op, fix);\n } else {\n return assertNever(intent, \"Invalid intent\");\n }\n }\n\n /**\n * Accept a freshly placed list item. If the server chose a different\n * position in the end (conflict resolution), broadcast only the corrected Op\n * to all clients and send a \"fix\" op back to the originating client.\n */\n private acceptAndFix(\n op: CreateOp & HasOpId,\n node: SerializedChild,\n finalKey: string\n ): ApplyOpResult {\n if (finalKey !== node.parentKey) {\n return accept(\n { ...op, parentKey: finalKey },\n { type: OpCode.SET_PARENT_KEY, id: op.id, parentKey: finalKey }\n );\n }\n return accept(op);\n }\n\n private applyDeleteObjectKeyOp(\n op: DeleteObjectKeyOp & HasOpId\n ): ApplyOpResult {\n this.driver.delete_child_key(op.id, op.key);\n return accept(op);\n }\n\n private applyUpdateObjectOp(op: UpdateObjectOp & HasOpId): ApplyOpResult {\n this.driver.set_object_data(op.id, op.data, true);\n return accept(op);\n }\n\n private applyDeleteCrdtOp(op: DeleteCrdtOp & HasOpId): ApplyOpResult {\n this.driver.delete_node(op.id);\n return accept(op);\n }\n\n private applySetParentKeyOp(op: SetParentKeyOp & HasOpId): ApplyOpResult {\n const newPosition = this.moveToPosInList(op.id, op.parentKey);\n if (newPosition === undefined) {\n // The operation got rejected because it didn't make sense, ignore it\n return ignore(op);\n }\n\n // If the inserted node is different from the input, it means there was\n // a conflict and the node has been inserted in a new, free, list position.\n // We should broadcast a modified Op to all clients that has the modified\n // position, and send a \"fix\" op back to the originating client.\n if (newPosition !== op.parentKey) {\n const modifiedOp = { ...op, parentKey: newPosition };\n const fix: FixOp = {\n type: OpCode.SET_PARENT_KEY,\n id: op.id,\n parentKey: newPosition,\n };\n return accept(modifiedOp, fix);\n } else {\n return accept(op);\n }\n }\n\n /**\n * Inserts a new node in the storage tree, under a list parent. If an\n * existing sibling node already exist under this key, however, it will look\n * for another free position under that parent and insert it under\n * a different parent key that is guaranteed to be available.\n *\n * Returns the key that was used for the insertion.\n */\n private insertIntoList(id: string, node: SerializedChild): string {\n // First, compute the key to use to insert this node\n const key = this.findFreeListPosition(node.parentId, asPos(node.parentKey));\n if (key !== node.parentKey) {\n node = { ...node, parentKey: key };\n }\n this.driver.set_child(id, node);\n return node.parentKey;\n }\n\n /**\n * Server-authoritative append: places the node strictly after every existing\n * sibling under its list parent. If the client's preferred key already sorts\n * after the current last sibling it's kept as-is (guaranteed free, since\n * it's beyond the max); otherwise the node is placed right after the last\n * sibling. Because Ops are processed serially, the chosen key is always\n * free, so concurrent pushes never collide.\n *\n * Returns the final key that was used for the insertion.\n */\n private appendToList(id: string, node: SerializedChild): string {\n const lastPos = this.driver.get_last_sibling(node.parentId);\n const preferredPos = asPos(node.parentKey);\n const finalKey =\n lastPos === undefined || preferredPos > lastPos\n ? preferredPos\n : makePosition(lastPos);\n this.driver.set_child(\n id,\n finalKey !== node.parentKey ? { ...node, parentKey: finalKey } : node\n );\n return finalKey;\n }\n\n /**\n * Tries to move a node to the given position under the same parent. If\n * a conflicting sibling node already exist at this position, it will use\n * another free position instead, to avoid the conflict.\n *\n * Returns the position (parentKey) that the node was eventually placed at.\n * If the node could be inserted without conflict, it will return the same\n * parentKey position.\n *\n * Will return `undefined` if this action could not be interpreted. Will be\n * a no-op for non-list items.\n */\n private moveToPosInList(id: string, targetKey: string): string | undefined {\n const node = this.driver.get_node(id);\n if (node?.parentId === undefined) {\n return; /* reject */\n }\n\n if (this.driver.get_node(node.parentId)?.type !== CrdtType.LIST) {\n // SetParentKeyOp is a no-op for all nodes, except list items\n return; /* reject */\n }\n\n if (node.parentKey === targetKey) {\n // Already there\n return targetKey; /* no-op */\n }\n\n // First, compute the key to use to insert this node\n const key = this.findFreeListPosition(node.parentId, asPos(targetKey));\n if (key !== node.parentKey) {\n this.driver.move_sibling(id, key);\n }\n return key;\n }\n\n /**\n * Checks whether the given parentKey is a \"free position\" under the\n * parentId, i.e. there are no siblings that have the same key. If a sibling\n * exists under that key, it tries to generate new positions until it finds\n * a free slot, and returns that. The returned value is therefore always safe\n * to use as parentKey.\n */\n private findFreeListPosition(parentId: string, parentPos: Pos): Pos {\n if (!this.driver.has_child_at(parentId, parentPos)) {\n return parentPos;\n }\n\n const currPos = parentPos;\n const nextPos = this.driver.get_next_sibling(parentId, currPos);\n if (nextPos !== undefined) {\n return makePosition(currPos, nextPos); // Between current and next\n } else {\n return makePosition(currPos); // After current (fallback)\n }\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport { createHash } from \"node:crypto\";\n\nimport { Base64 } from \"js-base64\";\nimport { nanoid } from \"nanoid\";\nimport * as Y from \"yjs\";\n\nimport type { Guid, YDocId } from \"~/decoders\";\nimport { ROOT_YDOC_ID } from \"~/decoders\";\nimport type { IStorageDriver } from \"~/interfaces\";\nimport type { Logger } from \"~/lib/Logger\";\n\n// How many updates to store before compacting\nconst UPDATE_COUNT_THRESHOLD = 1_000;\n\n// How much smaller the merged update can be than the sum of the updates before compacting\nconst MERGE_SHRINK_THRESHOLD = 0.8;\n\nexport class YjsStorage {\n private readonly driver: IStorageDriver;\n private readonly updateCountThreshold: number;\n\n /**\n * The root Y.Doc instance, which may not have been hydrated from storage\n * yet. Use getRootDoc() instead, which lazily hydrates it on first access —\n * only touch this field directly when the unhydrated instance is fine\n * (e.g. identity checks, subdoc traversal).\n */\n private readonly rawRootDoc: Y.Doc = new Y.Doc();\n private readonly lastSnapshotById = new Map<YDocId, Y.Snapshot>();\n private readonly docsById: Map<YDocId, Y.Doc> = new Map();\n private readonly storedKeysById: Map<YDocId, string[]> = new Map();\n\n constructor(\n driver: IStorageDriver,\n updateCountThreshold: number = UPDATE_COUNT_THRESHOLD\n ) {\n this.driver = driver;\n this.updateCountThreshold = updateCountThreshold;\n this.rawRootDoc.on(\"subdocs\", ({ removed }) => {\n removed.forEach((subdoc: Y.Doc) => {\n subdoc.destroy(); // will remove listeners\n });\n });\n }\n\n // ------------------------------------------------------------------------------------\n // Public API\n // ------------------------------------------------------------------------------------\n\n public getYDoc(logger: Logger, docId: YDocId): Y.Doc {\n if (docId !== ROOT_YDOC_ID) {\n // Subdocs hang off the root doc, so make sure that one is loaded first\n // (otherwise the subdoc lookup below would come up empty)\n this.getRootDoc(logger);\n }\n\n let loaded = this.docsById.get(docId);\n let doc =\n docId === ROOT_YDOC_ID ? this.rawRootDoc : this.findYSubdocByGuid(docId);\n if (!doc) {\n // An API call can load a subdoc without the root doc being loaded, we account for that by just instantiating a doc here.\n doc = new Y.Doc();\n }\n if (loaded === undefined) {\n loaded = this._loadYDocFromDurableStorage(doc, docId);\n this.docsById.set(docId, loaded);\n }\n return loaded;\n }\n\n /**\n * Returns the root Y.Doc, lazily loading it from storage first if it\n * hasn't been loaded yet during this instance's lifetime.\n */\n private getRootDoc(logger: Logger): Y.Doc {\n return this.getYDoc(logger, ROOT_YDOC_ID);\n }\n\n /**\n * If passed a state vector, an update with diff will be returned, if not the entire doc is returned.\n *\n * @param stateVector a base64 encoded target state vector created by running Y.encodeStateVector(Doc) on the client\n * @returns a base64 encoded array of YJS updates\n */\n public getYDocUpdate(\n logger: Logger,\n stateVector: string = \"\",\n guid?: Guid,\n isV2: boolean = false\n ): string | null {\n const update = this.getYDocUpdateBinary(logger, stateVector, guid, isV2);\n if (!update) return null;\n return Base64.fromUint8Array(update);\n }\n\n public getYDocUpdateBinary(\n logger: Logger,\n stateVector: string = \"\",\n guid?: Guid,\n isV2: boolean = false\n ): Uint8Array<ArrayBuffer> | null {\n const doc =\n guid !== undefined\n ? this.getYSubdoc(logger, guid)\n : this.getRootDoc(logger);\n if (!doc) {\n return null;\n }\n let encodedTargetVector;\n try {\n // if given a state vector, attempt to decode it a single diffed update\n encodedTargetVector =\n stateVector.length > 0 ? Base64.toUint8Array(stateVector) : undefined;\n } catch {\n logger.warn(\n \"Could not get update from passed vector, returning all updates\"\n );\n }\n if (isV2) {\n return Y.encodeStateAsUpdateV2(doc, encodedTargetVector);\n }\n return Y.encodeStateAsUpdate(doc, encodedTargetVector);\n }\n\n public getYStateVector(logger: Logger, guid?: Guid): string | null {\n const doc =\n guid !== undefined\n ? this.getYSubdoc(logger, guid)\n : this.getRootDoc(logger);\n if (!doc) {\n return null;\n }\n return Base64.fromUint8Array(Y.encodeStateVector(doc));\n }\n\n public getSnapshotHash(\n logger: Logger,\n options: { guid?: Guid; isV2?: boolean }\n ): string | null {\n const doc =\n options.guid !== undefined\n ? this.getYSubdoc(logger, options.guid)\n : this.getRootDoc(logger);\n if (!doc) {\n return null;\n }\n const snapshot = this._getOrPutLastSnapshot(doc);\n return this.calculateSnapshotHash(snapshot, { isV2: options.isV2 });\n }\n\n /**\n * @param update base64 encoded uint8array\n * @returns { isUpdated: boolean; snapshotHash: string }\n * isUpdated: true if the update had an effect on the YDoc\n * snapshotHash: the hash of the new snapshot\n */\n public addYDocUpdate(\n logger: Logger,\n update: string | Uint8Array,\n guid?: Guid,\n isV2?: boolean\n ): { isUpdated: boolean; snapshotHash: string } {\n const doc =\n guid !== undefined\n ? this.getYSubdoc(logger, guid)\n : this.getRootDoc(logger);\n if (!doc) {\n throw new Error(`YDoc with guid ${guid} not found`);\n }\n\n try {\n // takes a snapshot if none is stored in memory - NOTE: snapshots are a combination of statevector + deleteset, not a full doc\n const beforeSnapshot = this._getOrPutLastSnapshot(doc);\n const updateAsU8 =\n typeof update === \"string\" ? Base64.toUint8Array(update) : update;\n const applyUpdate = isV2 ? Y.applyUpdateV2 : Y.applyUpdate;\n applyUpdate(doc, updateAsU8, \"client\");\n // put the new \"after update\" snapshot\n const afterSnapshot = this._putLastSnapshot(doc);\n // Check the snapshot before/after to see if the update had an effect\n const updated = !Y.equalSnapshots(beforeSnapshot, afterSnapshot);\n if (updated) {\n this.handleYDocUpdate(doc, updateAsU8, isV2);\n }\n\n return {\n isUpdated: updated,\n snapshotHash: this.calculateSnapshotHash(afterSnapshot, { isV2 }),\n };\n } catch (e) {\n // The only reason this would happen is if a user would send bad data\n logger.warn(`Ignored bad YDoc update: ${String(e)}`);\n throw new Error(\n \"Bad YDoc update. Data is corrupted, or data does not match the encoding.\"\n );\n }\n }\n\n // ------------------------------------------------------------------------------------\n // Private APIs\n // ------------------------------------------------------------------------------------\n\n // NOTE: We could instead store the hash of snapshot instead of the whole snapshot to optimize memory usage.\n private _getOrPutLastSnapshot(doc: Y.Doc): Y.Snapshot {\n const docId: YDocId =\n doc.guid === this.rawRootDoc.guid ? ROOT_YDOC_ID : (doc.guid as Guid);\n const snapshot = this.lastSnapshotById.get(docId);\n if (snapshot) {\n return snapshot;\n }\n return this._putLastSnapshot(doc);\n }\n\n // NOTE: We could instead store the hash of snapshot instead of the whole snapshot to optimize memory usage.\n private _putLastSnapshot(doc: Y.Doc): Y.Snapshot {\n const docId: YDocId =\n doc.guid === this.rawRootDoc.guid ? ROOT_YDOC_ID : (doc.guid as Guid);\n const snapshot = Y.snapshot(doc);\n this.lastSnapshotById.set(docId, snapshot);\n return snapshot;\n }\n\n // compact the updates into a single update and write it to the durable storage\n private _compactYJSUpdates = (\n doc: Y.Doc,\n docId: YDocId,\n storedKeys: string[]\n ): void => {\n const compactedUpdate = Y.encodeStateAsUpdate(doc);\n const newKey = nanoid();\n this.driver.write_y_updates(docId, newKey, compactedUpdate);\n // Todo: after we kill the kv driver, we should have an overwrite method in the driverso we don't need to delete and write\n this.driver.delete_y_updates(docId, storedKeys);\n this.storedKeysById.set(docId, [newKey]);\n };\n\n private _loadYDocFromDurableStorage = (doc: Y.Doc, docId: YDocId): Y.Doc => {\n const docUpdates = Object.fromEntries(this.driver.iter_y_updates(docId));\n const updates = Object.values(docUpdates);\n const beforeSize = updates.reduce((acc, update) => acc + update.length, 0);\n const newupdate = Y.mergeUpdates(updates);\n const storedKeys = Object.keys(docUpdates);\n Y.applyUpdate(doc, newupdate);\n // after compaction, there will only be one unique key.\n if (\n this.shouldCompactByKeyCount(storedKeys) ||\n this.shouldCompactBySize(beforeSize, newupdate.length)\n ) {\n this._compactYJSUpdates(doc, docId, storedKeys);\n } else {\n this.storedKeysById.set(docId, storedKeys);\n }\n doc.emit(\"load\", [doc]); // sets the \"isLoaded\" to true on the doc\n\n return doc;\n };\n\n private findYSubdocByGuid(guid: Guid): Y.Doc | null {\n for (const subdoc of this.rawRootDoc.getSubdocs()) {\n if (subdoc.guid === guid) {\n return subdoc;\n }\n }\n return null;\n }\n\n private calculateSnapshotHash(\n snapshot: Y.Snapshot,\n { isV2 }: { isV2?: boolean }\n ): string {\n const encodedSnapshot = isV2\n ? Y.encodeSnapshotV2(snapshot)\n : Y.encodeSnapshot(snapshot);\n return createHash(\"sha256\").update(encodedSnapshot).digest(\"base64\");\n }\n\n // gets a subdoc, it will be loaded if not already loaded\n private getYSubdoc(logger: Logger, guid: Guid): Y.Doc | null {\n // Subdocs hang off the root doc, so make sure that one is loaded first\n this.getRootDoc(logger);\n\n const subdoc = this.findYSubdocByGuid(guid);\n if (!subdoc) {\n return null;\n }\n this.getYDoc(logger, guid);\n return subdoc;\n }\n\n // When the YJS doc changes, update it in durable storage\n private handleYDocUpdate(\n doc: Y.Doc,\n update: Uint8Array,\n isV2: boolean | undefined\n ): void {\n // Todo: in the future, we should pass this detail to the driver so it can store the version as metadata\n // this will be easy for sqlite drivers, but not for the KV driver\n const v1update = isV2 ? Y.convertUpdateFormatV2ToV1(update) : update;\n const docId: YDocId =\n doc.guid === this.rawRootDoc.guid ? ROOT_YDOC_ID : (doc.guid as Guid);\n\n const storedKeys = this.storedKeysById.get(docId);\n\n // Every UPDATE_COUNT_THRESHOLD updates, we compact the updates\n if (this.shouldCompactByKeyCount(storedKeys)) {\n this._compactYJSUpdates(doc, docId, storedKeys || []);\n return;\n }\n\n // the whole concept of storing keys is not needed when we kill the kv driver, all of this stuff is trivial in sqlite\n const newKey = nanoid();\n this.driver.write_y_updates(docId, newKey, v1update);\n\n // update the stored keys, which we'll need for compaction.\n if (!storedKeys) {\n this.storedKeysById.set(docId, [newKey]);\n } else {\n storedKeys.push(newKey);\n }\n }\n\n private shouldCompactBySize(beforeSize: number, afterSize: number): boolean {\n return beforeSize * MERGE_SHRINK_THRESHOLD > afterSize;\n }\n\n private shouldCompactByKeyCount(storedKeys: string[] | undefined): boolean {\n if (!storedKeys) {\n return false;\n }\n return storedKeys.length >= this.updateCountThreshold;\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\n/**\n * Given a promise or promise factory, returns a 2-tuple of success or failure.\n * This pattern avoids having to build deeply nested try / catch clauses, where\n * success variables need to be defined as a `let` outside of the `try` block.\n *\n * Turns:\n *\n * let result;\n * try {\n * result = await doSomething();\n * } catch (error) {\n * // do something with error\n * }\n *\n * doAnotherThing(result);\n *\n * Into:\n *\n * const [result, error] = await tryCatch(doSomething());\n * if (error) {\n * // do something with error\n * }\n * doAnotherThing(result);\n *\n * When given a sync callback, the tuple is returned synchronously (no await\n * needed):\n *\n * const [result, error] = tryCatch(() => doSomethingSync());\n *\n */\nfunction isThenable<T>(value: T | PromiseLike<T>): value is PromiseLike<T> {\n return (\n value !== null &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n typeof (value as { then?: unknown }).then === \"function\"\n );\n}\n\nexport function tryCatch<T, E = Error>(\n promise: PromiseLike<T> | (() => PromiseLike<T>)\n): Promise<[T, undefined] | [undefined, E]>;\nexport function tryCatch<T, E = Error>(\n fn: () => T\n): [T, undefined] | [undefined, E];\nexport function tryCatch<T, E = Error>(\n promise: PromiseLike<T> | (() => PromiseLike<T>) | (() => T)\n): Promise<[T, undefined] | [undefined, E]> | [T, undefined] | [undefined, E] {\n try {\n const result = typeof promise === \"function\" ? promise() : promise;\n if (isThenable(result)) {\n return Promise.resolve(result).then(\n (data: T): [T, undefined] => [data, undefined],\n (error: unknown): [undefined, E] => [undefined, error as E]\n );\n }\n return [result, undefined];\n } catch (error) {\n return [undefined, error as E];\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\n/**\n * Like ES6 map, but also provides a unique reverse lookup index for values\n * stored in the map.\n *\n * Useful for code like:\n *\n * // Store a list of persons by their IDs, but each person's email must also\n * // be unique\n * const map = new UniqueMap((person) => person.email);\n * map.set(1, { name: 'John Doe', email: 'john@example.org' });\n * map.set(2, { name: 'John Foo', email: 'john@example.org' }); // Will error!\n * map.delete(1);\n * map.set(3, { name: 'Johnny', email: 'john@example.org' }); // Now it's allowed\n *\n * map.getReverseKey('john@example.org') // 3\n * map.getReverse('john@example.org') // { name: 'Johnny', email: 'john@example.org' }\n *\n */\nexport class UniqueMap<K, V, UK> extends Map<K, V> {\n // / \\\n // Primary key Unique key\n #_revMap: Map<UK, K>;\n #_keyFn: (value: V) => UK;\n\n constructor(\n keyFn: (value: V) => UK\n // entries?: readonly (readonly [K, V])[] | null\n ) {\n super(); // super(entries)\n this.#_keyFn = keyFn;\n this.#_revMap = new Map();\n }\n\n lookupPrimaryKey(uniqKey: UK): K | undefined {\n return this.#_revMap.get(uniqKey);\n }\n\n lookup(uniqKey: UK): V | undefined {\n const key = this.#_revMap.get(uniqKey);\n return key !== undefined ? this.get(key) : undefined;\n }\n\n set(key: K, value: V): this {\n const uniqKey = this.#_keyFn(value);\n const primaryKey = this.#_revMap.get(uniqKey);\n if (primaryKey !== undefined && primaryKey !== key) {\n throw new Error(`Unique key ${String(uniqKey)} already exists`);\n }\n this.#_revMap.set(uniqKey, key);\n return super.set(key, value);\n }\n\n delete(primaryKey: K): boolean {\n const value = this.get(primaryKey);\n if (value !== undefined) {\n const indexedKey = this.#_keyFn(value);\n this.#_revMap.delete(indexedKey);\n }\n return super.delete(primaryKey);\n }\n}\n","/**\n * Copyright (c) Liveblocks Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/>.\n */\n\nimport type { BaseUserMeta, JsonObject } from \"@liveblocks/core\";\nimport { ServerMsgCode } from \"@liveblocks/core\";\n\nimport type { RoomStateServerMsg } from \"~/protocol\";\nimport type { LeasedSession } from \"~/types\";\n\n/**\n * Concatenates multiple Uint8Arrays into a single Uint8Array.\n */\nexport function concatUint8Arrays(\n arrays: Uint8Array[]\n): Uint8Array<ArrayBuffer> {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n\nexport function makeRoomStateMsg(\n actor: number,\n nonce: string,\n scopes: string[],\n users: Record<number, BaseUserMeta & { scopes: string[] }>,\n publicMeta?: JsonObject\n): RoomStateServerMsg<BaseUserMeta> {\n return {\n type: ServerMsgCode.ROOM_STATE,\n actor,\n nonce,\n scopes,\n users,\n meta: publicMeta ?? {},\n };\n}\n\n/**\n * Check if a leased session is expired.\n * Returns true if the current time is greater than or equal to updatedAt + ttl.\n */\nexport function isLeasedSessionExpired(leasedSession: LeasedSession): boolean {\n const now = Date.now();\n return now >= leasedSession.updatedAt + leasedSession.ttl;\n}\n"]}
|