@liveblocks/server 1.4.1 → 1.4.2-pre2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Json, asPos, IUserInfo, SerializedCrdt, JsonObject, DistributiveOmit, SetParentKeyOp, DeleteCrdtOp, ClientMsg as ClientMsg$1, Brand, SerializedRootObject, SerializedChild, StorageNode, Awaitable, PlainLsonObject, NodeMap as NodeMap$1, NodeStream as NodeStream$1, ClientWireOp, ServerMsg as ServerMsg$1, BaseUserMeta, IgnoredOp } from '@liveblocks/core';
1
+ import { Json, asPos, IUserInfo, SerializedCrdt, JsonObject, DistributiveOmit, SetParentKeyOp, DeleteCrdtOp, ClientMsg as ClientMsg$1, Brand, SerializedRootObject, SerializedChild, StorageNode, Awaitable, CompactNode, PlainLsonObject, NodeMap as NodeMap$1, NodeStream as NodeStream$1, ClientWireOp, ServerMsg as ServerMsg$1, BaseUserMeta, IgnoredOp } from '@liveblocks/core';
2
2
  export { BroadcastEventClientMsg, ClientMsg, ClientWireOp, CreateListOp, CreateMapOp, CreateObjectOp, CreateOp, CreateRegisterOp, DeleteCrdtOp, DeleteObjectKeyOp, FetchStorageClientMsg, FetchYDocClientMsg, HasOpId, IgnoredOp, Op, RoomStateServerMsg, ServerMsg, ServerWireOp, SetParentKeyOp, UpdateObjectOp, UpdatePresenceClientMsg, UpdateStorageClientMsg, UpdateYDocClientMsg } from '@liveblocks/core';
3
3
  import * as decoders from 'decoders';
4
4
  import { Decoder } from 'decoders';
@@ -23,6 +23,37 @@ import * as Y from 'yjs';
23
23
  */
24
24
 
25
25
  type Pos = ReturnType<typeof asPos>;
26
+ declare const fromType: unique symbol;
27
+ /**
28
+ * A string known to be a valid JSON-encoded value. Use this whenever a
29
+ * `string` already carries JSON, to keep that fact visible in the type system
30
+ * (instead of letting it look like a freeform `string`).
31
+ *
32
+ * Optionally parameterised by the *parsed* shape: `jstring<CompactNode>`
33
+ * means "a string whose `JSON.parse` result is a `CompactNode`". Without
34
+ * a type argument, defaults to `jstring<Json>` (any JSON value).
35
+ *
36
+ * At storage boundaries (e.g. reading `jdata` from SQLite), `as jstring`
37
+ * is acceptable — we trust the bytes are valid JSON because we wrote them as
38
+ * JSON ourselves.
39
+ *
40
+ * For example, these are valid JSON strings:
41
+ * - '0', '1', '2', '3', '3.14', etc.
42
+ * - 'null', 'true', 'false'
43
+ * - '{"foo":1}', '{ "foo": 1 }' (spaces are fine)
44
+ * - '[]', '[1,2, 3]', '[[]]', etc.
45
+ * - '["hi",{}]'
46
+ * - '"foo"'
47
+ *
48
+ * But these are not:
49
+ * - 'foo'
50
+ * - '1,2,3'
51
+ * - '{'
52
+ * - '[1,2,3,]' or '{"foo":1},' (trailing commas are not valid)
53
+ */
54
+ type jstring<J = Json> = string & {
55
+ readonly [fromType]: J;
56
+ };
26
57
  type NodeTuple<T extends SerializedCrdt = SerializedCrdt> = [
27
58
  id: string,
28
59
  value: T
@@ -683,6 +714,27 @@ interface IStorageDriverNodeAPI {
683
714
  * Yield all nodes as [id, node] pairs. Must always include the root node.
684
715
  */
685
716
  iter_nodes(): Iterable<StorageNode>;
717
+ /**
718
+ * Yield each node as a pre-built `CompactNode` JSON tuple string, ready to
719
+ * be emitted directly into a STORAGE_CHUNK wire frame without JSON.parse()
720
+ * or JSON.stringify()'ing overhead. Implementations MUST produce text whose
721
+ * parsed shape exactly matches the `CompactNode` union type from
722
+ * @liveblocks/core. The emitted shapes are:
723
+ *
724
+ * - Root node: '["root",<data>]'
725
+ * - OBJECT / REGISTER: '["0:1",0,"root","a",<data>]'
726
+ * - LIST / MAP: '["0:2",1,"0:1","b"]'
727
+ *
728
+ * Invariant (implementations MUST uphold; asserted by `_generateFullTestSuite`):
729
+ *
730
+ * iter_nodes_optimized().map(JSON.parse)
731
+ * ≡ nodeStreamToCompactNodes(iter_nodes())
732
+ *
733
+ * i.e. parsing each yielded string yields the same sequence of CompactNodes
734
+ * that the canonical `iter_nodes()` + `nodeStreamToCompactNodes()` path
735
+ * would produce.
736
+ */
737
+ iter_nodes_optimized(): Iterable<jstring<CompactNode>>;
686
738
  /**
687
739
  * Return true iff a node with the given id exists. Must return true for "root".
688
740
  */
@@ -704,6 +756,12 @@ interface IStorageDriverNodeAPI {
704
756
  * does not have to exist already. Positions compare lexicographically.
705
757
  */
706
758
  get_next_sibling(parentId: string, pos: Pos): Pos | undefined;
759
+ /**
760
+ * Return the position of the last (rightmost) child under parentId, or
761
+ * undefined if the node has no children. Positions compare
762
+ * lexicographically.
763
+ */
764
+ get_last_sibling(parentId: string): Pos | undefined;
707
765
  /**
708
766
  * Insert a child node with the given id.
709
767
  *
@@ -1228,10 +1286,9 @@ type Millis = Brand<number, "Millis">;
1228
1286
  * receives it as part of its ROOM_STATE message.
1229
1287
  */
1230
1288
  type SessionKey = Brand<string, "SessionKey">;
1231
- type PreSerializedServerMsg = Brand<string, "PreSerializedServerMsg">;
1232
1289
  type ClientMsg = ClientMsg$1<JsonObject, Json> | FetchFeedsClientMsg | FetchFeedMessagesClientMsg | AddFeedClientMsg | UpdateFeedClientMsg | DeleteFeedClientMsg | AddFeedMessageClientMsg | UpdateFeedMessageClientMsg | DeleteFeedMessageClientMsg;
1233
1290
  type ServerMsg = ServerMsg$1<JsonObject, BaseUserMeta, Json> | FeedMessagesServerMsg | FeedsServerMsg;
1234
- declare function serialize(msgs: ServerMsg | readonly ServerMsg[]): PreSerializedServerMsg;
1291
+ declare function serialize(msgs: ServerMsg | readonly ServerMsg[]): jstring<ServerMsg>;
1235
1292
  declare function ackIgnoredOp(opId: string): IgnoredOp;
1236
1293
  /**
1237
1294
  * A known or anonymous user.
@@ -1278,7 +1335,7 @@ declare class BrowserSession<SM, CM extends JsonObject> {
1278
1335
  get hasNotifiedClientStorageUpdateError(): boolean;
1279
1336
  setHasNotifiedClientStorageUpdateError(): void;
1280
1337
  sendPong(): number;
1281
- send(serverMsg: ServerMsg | ServerMsg[] | PreSerializedServerMsg): number;
1338
+ send(serverMsg: ServerMsg | ServerMsg[] | jstring<ServerMsg>): number;
1282
1339
  }
1283
1340
  declare class BackendSession extends BrowserSession<never, never> {
1284
1341
  }
@@ -1317,13 +1374,6 @@ type RoomOptions<SM, CM extends JsonObject, C> = {
1317
1374
  */
1318
1375
  storage?: IStorageDriver;
1319
1376
  logger?: Logger;
1320
- /**
1321
- * Whether to allow streaming storage responses. Only safe with drivers
1322
- * that can guarantee that no Ops from other clients can get interleaved
1323
- * between the chunk generation until the last chunk has been sent.
1324
- * Defaults to true, but is notably NOT safe to use from DOS-KV backends.
1325
- */
1326
- allowStreaming?: boolean;
1327
1377
  hooks?: {
1328
1378
  /** Customize which incoming messages from a client are allowed or disallowed. */
1329
1379
  isClientMsgAllowed?: (msg: ClientMsg, session: BrowserSession<SM, CM>) => {
@@ -1436,7 +1486,7 @@ declare class Room<RM, SM, CM extends JsonObject, C = undefined> {
1436
1486
  createTicket(options?: CreateTicketOptions<SM, CM>): Promise<Ticket<SM, CM>>;
1437
1487
  createBackendSession_experimental(): Promise<[
1438
1488
  session: BackendSession,
1439
- outgoingMessages: PreSerializedServerMsg[]
1489
+ outgoingMessages: jstring<ServerMsg>[]
1440
1490
  ]>;
1441
1491
  /**
1442
1492
  * Restores the given sessions as the Room server's session list. Can only be
@@ -1895,4 +1945,4 @@ declare class InMemoryDriver implements IStorageDriver {
1895
1945
  load_nodes_api(): IStorageDriverNodeAPI;
1896
1946
  }
1897
1947
 
1898
- export { type ActorID, type AddFeedClientMsg, type AddFeedMessageClientMsg, BackendSession, BrowserSession, ConsoleTarget, type CreateTicketOptions, DefaultMap, type DeleteFeedClientMsg, type DeleteFeedMessageClientMsg, type Feed, type FeedDeletedServerMsg, type FeedMessage, type FeedMessagesAddedServerMsg, type FeedMessagesDeletedServerMsg, type FeedMessagesListServerMsg, type FeedMessagesServerMsg, type FeedMessagesUpdatedServerMsg, FeedMsgCode, FeedRequestErrorCode, type FeedRequestFailedServerMsg, type FeedsAddedServerMsg, type FeedsListServerMsg, type FeedsServerMsg, type FeedsUpdatedServerMsg, type FetchFeedMessagesClientMsg, type FetchFeedsClientMsg, type FixOp, type Guid, type IReadableSnapshot, type IServerWebSocket, type IStorageDriver, type IStorageDriverNodeAPI, type IUserData, InMemoryDriver, type LeasedSession, type ListFeedMessagesOptions, type ListFeedMessagesResult, type ListFeedsOptions, type ListFeedsResult, type LoadingState, LogLevel, LogTarget, Logger, type MetadataDB, type Millis, NestedMap, type NodeMap, type NodeStream, type NodeTuple, type Pos, type PreSerializedServerMsg, ProtocolVersion, ROOT_YDOC_ID, Room, type SessionKey, type Ticket, UniqueMap, type UpdateFeedClientMsg, type UpdateFeedMessageClientMsg, type YDocId, type YUpdate, type YVector, ackIgnoredOp, clientMsgDecoder, concatUint8Arrays, feedFailureServerMsg, feedMetadataIdDecoder, feedMetadataUpdateDecoder, feedRequestFailed, fetchFeedsMetadataFilterDecoder, guidDecoder, isLeasedSessionExpired, jsonObjectYolo, jsonYolo, makeInMemorySnapshot, makeMetadataDB, mapFeedError, optionalFeedMetadataDecoder, plainLsonToNodeStream, protocolVersionDecoder, quote, serialize as serializeServerMsg, snapshotToLossyJson_eager, snapshotToLossyJson_lazy, snapshotToNodeStream, snapshotToPlainLson_eager, snapshotToPlainLson_lazy, Storage as test_only__Storage, YjsStorage as test_only__YjsStorage, transientClientMsgDecoder, tryCatch };
1948
+ export { type ActorID, type AddFeedClientMsg, type AddFeedMessageClientMsg, BackendSession, BrowserSession, ConsoleTarget, type CreateTicketOptions, DefaultMap, type DeleteFeedClientMsg, type DeleteFeedMessageClientMsg, type Feed, type FeedDeletedServerMsg, type FeedMessage, type FeedMessagesAddedServerMsg, type FeedMessagesDeletedServerMsg, type FeedMessagesListServerMsg, type FeedMessagesServerMsg, type FeedMessagesUpdatedServerMsg, FeedMsgCode, FeedRequestErrorCode, type FeedRequestFailedServerMsg, type FeedsAddedServerMsg, type FeedsListServerMsg, type FeedsServerMsg, type FeedsUpdatedServerMsg, type FetchFeedMessagesClientMsg, type FetchFeedsClientMsg, type FixOp, type Guid, type IReadableSnapshot, type IServerWebSocket, type IStorageDriver, type IStorageDriverNodeAPI, type IUserData, InMemoryDriver, type LeasedSession, type ListFeedMessagesOptions, type ListFeedMessagesResult, type ListFeedsOptions, type ListFeedsResult, type LoadingState, LogLevel, LogTarget, Logger, type MetadataDB, type Millis, NestedMap, type NodeMap, type NodeStream, type NodeTuple, type Pos, ProtocolVersion, ROOT_YDOC_ID, Room, type SessionKey, type Ticket, UniqueMap, type UpdateFeedClientMsg, type UpdateFeedMessageClientMsg, type YDocId, type YUpdate, type YVector, ackIgnoredOp, clientMsgDecoder, concatUint8Arrays, feedFailureServerMsg, feedMetadataIdDecoder, feedMetadataUpdateDecoder, feedRequestFailed, fetchFeedsMetadataFilterDecoder, guidDecoder, isLeasedSessionExpired, jsonObjectYolo, jsonYolo, type jstring, makeInMemorySnapshot, makeMetadataDB, mapFeedError, optionalFeedMetadataDecoder, plainLsonToNodeStream, protocolVersionDecoder, quote, serialize as serializeServerMsg, snapshotToLossyJson_eager, snapshotToLossyJson_lazy, snapshotToNodeStream, snapshotToPlainLson_eager, snapshotToPlainLson_lazy, Storage as test_only__Storage, YjsStorage as test_only__YjsStorage, transientClientMsgDecoder, tryCatch };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Json, asPos, IUserInfo, SerializedCrdt, JsonObject, DistributiveOmit, SetParentKeyOp, DeleteCrdtOp, ClientMsg as ClientMsg$1, Brand, SerializedRootObject, SerializedChild, StorageNode, Awaitable, PlainLsonObject, NodeMap as NodeMap$1, NodeStream as NodeStream$1, ClientWireOp, ServerMsg as ServerMsg$1, BaseUserMeta, IgnoredOp } from '@liveblocks/core';
1
+ import { Json, asPos, IUserInfo, SerializedCrdt, JsonObject, DistributiveOmit, SetParentKeyOp, DeleteCrdtOp, ClientMsg as ClientMsg$1, Brand, SerializedRootObject, SerializedChild, StorageNode, Awaitable, CompactNode, PlainLsonObject, NodeMap as NodeMap$1, NodeStream as NodeStream$1, ClientWireOp, ServerMsg as ServerMsg$1, BaseUserMeta, IgnoredOp } from '@liveblocks/core';
2
2
  export { BroadcastEventClientMsg, ClientMsg, ClientWireOp, CreateListOp, CreateMapOp, CreateObjectOp, CreateOp, CreateRegisterOp, DeleteCrdtOp, DeleteObjectKeyOp, FetchStorageClientMsg, FetchYDocClientMsg, HasOpId, IgnoredOp, Op, RoomStateServerMsg, ServerMsg, ServerWireOp, SetParentKeyOp, UpdateObjectOp, UpdatePresenceClientMsg, UpdateStorageClientMsg, UpdateYDocClientMsg } from '@liveblocks/core';
3
3
  import * as decoders from 'decoders';
4
4
  import { Decoder } from 'decoders';
@@ -23,6 +23,37 @@ import * as Y from 'yjs';
23
23
  */
24
24
 
25
25
  type Pos = ReturnType<typeof asPos>;
26
+ declare const fromType: unique symbol;
27
+ /**
28
+ * A string known to be a valid JSON-encoded value. Use this whenever a
29
+ * `string` already carries JSON, to keep that fact visible in the type system
30
+ * (instead of letting it look like a freeform `string`).
31
+ *
32
+ * Optionally parameterised by the *parsed* shape: `jstring<CompactNode>`
33
+ * means "a string whose `JSON.parse` result is a `CompactNode`". Without
34
+ * a type argument, defaults to `jstring<Json>` (any JSON value).
35
+ *
36
+ * At storage boundaries (e.g. reading `jdata` from SQLite), `as jstring`
37
+ * is acceptable — we trust the bytes are valid JSON because we wrote them as
38
+ * JSON ourselves.
39
+ *
40
+ * For example, these are valid JSON strings:
41
+ * - '0', '1', '2', '3', '3.14', etc.
42
+ * - 'null', 'true', 'false'
43
+ * - '{"foo":1}', '{ "foo": 1 }' (spaces are fine)
44
+ * - '[]', '[1,2, 3]', '[[]]', etc.
45
+ * - '["hi",{}]'
46
+ * - '"foo"'
47
+ *
48
+ * But these are not:
49
+ * - 'foo'
50
+ * - '1,2,3'
51
+ * - '{'
52
+ * - '[1,2,3,]' or '{"foo":1},' (trailing commas are not valid)
53
+ */
54
+ type jstring<J = Json> = string & {
55
+ readonly [fromType]: J;
56
+ };
26
57
  type NodeTuple<T extends SerializedCrdt = SerializedCrdt> = [
27
58
  id: string,
28
59
  value: T
@@ -683,6 +714,27 @@ interface IStorageDriverNodeAPI {
683
714
  * Yield all nodes as [id, node] pairs. Must always include the root node.
684
715
  */
685
716
  iter_nodes(): Iterable<StorageNode>;
717
+ /**
718
+ * Yield each node as a pre-built `CompactNode` JSON tuple string, ready to
719
+ * be emitted directly into a STORAGE_CHUNK wire frame without JSON.parse()
720
+ * or JSON.stringify()'ing overhead. Implementations MUST produce text whose
721
+ * parsed shape exactly matches the `CompactNode` union type from
722
+ * @liveblocks/core. The emitted shapes are:
723
+ *
724
+ * - Root node: '["root",<data>]'
725
+ * - OBJECT / REGISTER: '["0:1",0,"root","a",<data>]'
726
+ * - LIST / MAP: '["0:2",1,"0:1","b"]'
727
+ *
728
+ * Invariant (implementations MUST uphold; asserted by `_generateFullTestSuite`):
729
+ *
730
+ * iter_nodes_optimized().map(JSON.parse)
731
+ * ≡ nodeStreamToCompactNodes(iter_nodes())
732
+ *
733
+ * i.e. parsing each yielded string yields the same sequence of CompactNodes
734
+ * that the canonical `iter_nodes()` + `nodeStreamToCompactNodes()` path
735
+ * would produce.
736
+ */
737
+ iter_nodes_optimized(): Iterable<jstring<CompactNode>>;
686
738
  /**
687
739
  * Return true iff a node with the given id exists. Must return true for "root".
688
740
  */
@@ -704,6 +756,12 @@ interface IStorageDriverNodeAPI {
704
756
  * does not have to exist already. Positions compare lexicographically.
705
757
  */
706
758
  get_next_sibling(parentId: string, pos: Pos): Pos | undefined;
759
+ /**
760
+ * Return the position of the last (rightmost) child under parentId, or
761
+ * undefined if the node has no children. Positions compare
762
+ * lexicographically.
763
+ */
764
+ get_last_sibling(parentId: string): Pos | undefined;
707
765
  /**
708
766
  * Insert a child node with the given id.
709
767
  *
@@ -1228,10 +1286,9 @@ type Millis = Brand<number, "Millis">;
1228
1286
  * receives it as part of its ROOM_STATE message.
1229
1287
  */
1230
1288
  type SessionKey = Brand<string, "SessionKey">;
1231
- type PreSerializedServerMsg = Brand<string, "PreSerializedServerMsg">;
1232
1289
  type ClientMsg = ClientMsg$1<JsonObject, Json> | FetchFeedsClientMsg | FetchFeedMessagesClientMsg | AddFeedClientMsg | UpdateFeedClientMsg | DeleteFeedClientMsg | AddFeedMessageClientMsg | UpdateFeedMessageClientMsg | DeleteFeedMessageClientMsg;
1233
1290
  type ServerMsg = ServerMsg$1<JsonObject, BaseUserMeta, Json> | FeedMessagesServerMsg | FeedsServerMsg;
1234
- declare function serialize(msgs: ServerMsg | readonly ServerMsg[]): PreSerializedServerMsg;
1291
+ declare function serialize(msgs: ServerMsg | readonly ServerMsg[]): jstring<ServerMsg>;
1235
1292
  declare function ackIgnoredOp(opId: string): IgnoredOp;
1236
1293
  /**
1237
1294
  * A known or anonymous user.
@@ -1278,7 +1335,7 @@ declare class BrowserSession<SM, CM extends JsonObject> {
1278
1335
  get hasNotifiedClientStorageUpdateError(): boolean;
1279
1336
  setHasNotifiedClientStorageUpdateError(): void;
1280
1337
  sendPong(): number;
1281
- send(serverMsg: ServerMsg | ServerMsg[] | PreSerializedServerMsg): number;
1338
+ send(serverMsg: ServerMsg | ServerMsg[] | jstring<ServerMsg>): number;
1282
1339
  }
1283
1340
  declare class BackendSession extends BrowserSession<never, never> {
1284
1341
  }
@@ -1317,13 +1374,6 @@ type RoomOptions<SM, CM extends JsonObject, C> = {
1317
1374
  */
1318
1375
  storage?: IStorageDriver;
1319
1376
  logger?: Logger;
1320
- /**
1321
- * Whether to allow streaming storage responses. Only safe with drivers
1322
- * that can guarantee that no Ops from other clients can get interleaved
1323
- * between the chunk generation until the last chunk has been sent.
1324
- * Defaults to true, but is notably NOT safe to use from DOS-KV backends.
1325
- */
1326
- allowStreaming?: boolean;
1327
1377
  hooks?: {
1328
1378
  /** Customize which incoming messages from a client are allowed or disallowed. */
1329
1379
  isClientMsgAllowed?: (msg: ClientMsg, session: BrowserSession<SM, CM>) => {
@@ -1436,7 +1486,7 @@ declare class Room<RM, SM, CM extends JsonObject, C = undefined> {
1436
1486
  createTicket(options?: CreateTicketOptions<SM, CM>): Promise<Ticket<SM, CM>>;
1437
1487
  createBackendSession_experimental(): Promise<[
1438
1488
  session: BackendSession,
1439
- outgoingMessages: PreSerializedServerMsg[]
1489
+ outgoingMessages: jstring<ServerMsg>[]
1440
1490
  ]>;
1441
1491
  /**
1442
1492
  * Restores the given sessions as the Room server's session list. Can only be
@@ -1895,4 +1945,4 @@ declare class InMemoryDriver implements IStorageDriver {
1895
1945
  load_nodes_api(): IStorageDriverNodeAPI;
1896
1946
  }
1897
1947
 
1898
- export { type ActorID, type AddFeedClientMsg, type AddFeedMessageClientMsg, BackendSession, BrowserSession, ConsoleTarget, type CreateTicketOptions, DefaultMap, type DeleteFeedClientMsg, type DeleteFeedMessageClientMsg, type Feed, type FeedDeletedServerMsg, type FeedMessage, type FeedMessagesAddedServerMsg, type FeedMessagesDeletedServerMsg, type FeedMessagesListServerMsg, type FeedMessagesServerMsg, type FeedMessagesUpdatedServerMsg, FeedMsgCode, FeedRequestErrorCode, type FeedRequestFailedServerMsg, type FeedsAddedServerMsg, type FeedsListServerMsg, type FeedsServerMsg, type FeedsUpdatedServerMsg, type FetchFeedMessagesClientMsg, type FetchFeedsClientMsg, type FixOp, type Guid, type IReadableSnapshot, type IServerWebSocket, type IStorageDriver, type IStorageDriverNodeAPI, type IUserData, InMemoryDriver, type LeasedSession, type ListFeedMessagesOptions, type ListFeedMessagesResult, type ListFeedsOptions, type ListFeedsResult, type LoadingState, LogLevel, LogTarget, Logger, type MetadataDB, type Millis, NestedMap, type NodeMap, type NodeStream, type NodeTuple, type Pos, type PreSerializedServerMsg, ProtocolVersion, ROOT_YDOC_ID, Room, type SessionKey, type Ticket, UniqueMap, type UpdateFeedClientMsg, type UpdateFeedMessageClientMsg, type YDocId, type YUpdate, type YVector, ackIgnoredOp, clientMsgDecoder, concatUint8Arrays, feedFailureServerMsg, feedMetadataIdDecoder, feedMetadataUpdateDecoder, feedRequestFailed, fetchFeedsMetadataFilterDecoder, guidDecoder, isLeasedSessionExpired, jsonObjectYolo, jsonYolo, makeInMemorySnapshot, makeMetadataDB, mapFeedError, optionalFeedMetadataDecoder, plainLsonToNodeStream, protocolVersionDecoder, quote, serialize as serializeServerMsg, snapshotToLossyJson_eager, snapshotToLossyJson_lazy, snapshotToNodeStream, snapshotToPlainLson_eager, snapshotToPlainLson_lazy, Storage as test_only__Storage, YjsStorage as test_only__YjsStorage, transientClientMsgDecoder, tryCatch };
1948
+ export { type ActorID, type AddFeedClientMsg, type AddFeedMessageClientMsg, BackendSession, BrowserSession, ConsoleTarget, type CreateTicketOptions, DefaultMap, type DeleteFeedClientMsg, type DeleteFeedMessageClientMsg, type Feed, type FeedDeletedServerMsg, type FeedMessage, type FeedMessagesAddedServerMsg, type FeedMessagesDeletedServerMsg, type FeedMessagesListServerMsg, type FeedMessagesServerMsg, type FeedMessagesUpdatedServerMsg, FeedMsgCode, FeedRequestErrorCode, type FeedRequestFailedServerMsg, type FeedsAddedServerMsg, type FeedsListServerMsg, type FeedsServerMsg, type FeedsUpdatedServerMsg, type FetchFeedMessagesClientMsg, type FetchFeedsClientMsg, type FixOp, type Guid, type IReadableSnapshot, type IServerWebSocket, type IStorageDriver, type IStorageDriverNodeAPI, type IUserData, InMemoryDriver, type LeasedSession, type ListFeedMessagesOptions, type ListFeedMessagesResult, type ListFeedsOptions, type ListFeedsResult, type LoadingState, LogLevel, LogTarget, Logger, type MetadataDB, type Millis, NestedMap, type NodeMap, type NodeStream, type NodeTuple, type Pos, ProtocolVersion, ROOT_YDOC_ID, Room, type SessionKey, type Ticket, UniqueMap, type UpdateFeedClientMsg, type UpdateFeedMessageClientMsg, type YDocId, type YUpdate, type YVector, ackIgnoredOp, clientMsgDecoder, concatUint8Arrays, feedFailureServerMsg, feedMetadataIdDecoder, feedMetadataUpdateDecoder, feedRequestFailed, fetchFeedsMetadataFilterDecoder, guidDecoder, isLeasedSessionExpired, jsonObjectYolo, jsonYolo, type jstring, makeInMemorySnapshot, makeMetadataDB, mapFeedError, optionalFeedMetadataDecoder, plainLsonToNodeStream, protocolVersionDecoder, quote, serialize as serializeServerMsg, snapshotToLossyJson_eager, snapshotToLossyJson_lazy, snapshotToNodeStream, snapshotToPlainLson_eager, snapshotToPlainLson_lazy, Storage as test_only__Storage, YjsStorage as test_only__YjsStorage, transientClientMsgDecoder, tryCatch };
package/dist/index.js CHANGED
@@ -129,7 +129,9 @@ var feedMetadataRecordForCreate = record(
129
129
  (value) => Object.keys(value).length <= MAX_METADATA_COUNT,
130
130
  `Must be at most ${MAX_METADATA_COUNT} items`
131
131
  );
132
- var optionalFeedMetadataDecoder = optional(feedMetadataRecordForCreate);
132
+ var optionalFeedMetadataDecoder = optional(
133
+ feedMetadataRecordForCreate
134
+ );
133
135
  var feedMetadataUpdateDecoder = record(
134
136
  feedMetadataIdDecoder,
135
137
  feedMetadataNullableValueDecoder
@@ -155,7 +157,15 @@ var jsonObjectYolo = jsonYolo.refine(
155
157
 
156
158
  // src/decoders/Op.ts
157
159
  import { OpCode } from "@liveblocks/core";
158
- import { constant, object, optional as optional2, string as string2, taggedUnion } from "decoders";
160
+ import {
161
+ constant,
162
+ object,
163
+ oneOf,
164
+ optional as optional2,
165
+ string as string2,
166
+ taggedUnion
167
+ } from "decoders";
168
+ var intent = oneOf(["set", "push"]);
159
169
  var updateObjectOp = object({
160
170
  type: constant(OpCode.UPDATE_OBJECT),
161
171
  opId: string2,
@@ -169,7 +179,7 @@ var createObjectOp = object({
169
179
  parentId: string2,
170
180
  parentKey: string2,
171
181
  data: jsonObjectYolo,
172
- intent: optional2(constant("set")),
182
+ intent: optional2(intent),
173
183
  deletedId: optional2(string2)
174
184
  });
175
185
  var createListOp = object({
@@ -178,7 +188,7 @@ var createListOp = object({
178
188
  id: string2,
179
189
  parentId: string2,
180
190
  parentKey: string2,
181
- intent: optional2(constant("set")),
191
+ intent: optional2(intent),
182
192
  deletedId: optional2(string2)
183
193
  });
184
194
  var createMapOp = object({
@@ -187,7 +197,7 @@ var createMapOp = object({
187
197
  id: string2,
188
198
  parentId: string2,
189
199
  parentKey: string2,
190
- intent: optional2(constant("set")),
200
+ intent: optional2(intent),
191
201
  deletedId: optional2(string2)
192
202
  });
193
203
  var createRegisterOp = object({
@@ -197,7 +207,7 @@ var createRegisterOp = object({
197
207
  parentId: string2,
198
208
  parentKey: string2,
199
209
  data: jsonYolo,
200
- intent: optional2(constant("set")),
210
+ intent: optional2(intent),
201
211
  deletedId: optional2(string2)
202
212
  });
203
213
  var deleteCrdtOp = object({
@@ -820,7 +830,6 @@ function makeMetadataDB(driver) {
820
830
  import {
821
831
  assertNever as assertNever3,
822
832
  ClientMsgCode as ClientMsgCode2,
823
- nodeStreamToCompactNodes,
824
833
  OpCode as OpCode3,
825
834
  raise as raise3,
826
835
  ServerMsgCode as CoreServerMsgCode,
@@ -829,7 +838,7 @@ import {
829
838
  } from "@liveblocks/core";
830
839
  import { Mutex, tryAcquire } from "async-mutex";
831
840
  import { array as array3, formatInline } from "decoders";
832
- import { chunked } from "itertools";
841
+ import { chunkedByCost } from "itertools";
833
842
  import { nanoid as nanoid2 } from "nanoid";
834
843
 
835
844
  // src/lib/Logger.ts
@@ -964,7 +973,13 @@ var Logger = class _Logger {
964
973
  };
965
974
 
966
975
  // src/plugins/InMemoryDriver.ts
967
- import { asPos, CrdtType as CrdtType4, isRootStorageNode as isRootStorageNode2, nn as nn2 } from "@liveblocks/core";
976
+ import {
977
+ asPos,
978
+ CrdtType as CrdtType4,
979
+ isRootStorageNode as isRootStorageNode2,
980
+ nn as nn2,
981
+ nodeStreamToCompactNodes
982
+ } from "@liveblocks/core";
968
983
  import { ifilter, imap } from "itertools";
969
984
 
970
985
  // src/lib/text.ts
@@ -1287,6 +1302,16 @@ var InMemoryDriver = class {
1287
1302
  }
1288
1303
  return nextPos;
1289
1304
  }
1305
+ function get_last_sibling(parentId) {
1306
+ let lastPos;
1307
+ for (const siblingKey of revNodes.keysAt(parentId)) {
1308
+ const siblingPos = asPos(siblingKey);
1309
+ if (lastPos === void 0 || siblingPos > lastPos) {
1310
+ lastPos = siblingPos;
1311
+ }
1312
+ }
1313
+ return lastPos;
1314
+ }
1290
1315
  async function set_child(id, node, allowOverwrite = false) {
1291
1316
  const parentNode = nodes.get(node.parentId);
1292
1317
  if (parentNode === void 0) {
@@ -1374,6 +1399,17 @@ var InMemoryDriver = class {
1374
1399
  * Yield all nodes as [id, node] pairs. Must always include the root node.
1375
1400
  */
1376
1401
  iter_nodes: () => nodes,
1402
+ /**
1403
+ * Yield each node as a pre-built CompactNode JSON tuple string.
1404
+ *
1405
+ * This implementation IS the canonical reference for the invariant:
1406
+ * iter_nodes_optimized` ≡ `nodeStreamToCompactNodes(iter_nodes())
1407
+ */
1408
+ *iter_nodes_optimized() {
1409
+ for (const compact of nodeStreamToCompactNodes(nodes)) {
1410
+ yield JSON.stringify(compact);
1411
+ }
1412
+ },
1377
1413
  /**
1378
1414
  * Return true iff a node with the given id exists. Must return true for "root".
1379
1415
  */
@@ -1395,6 +1431,11 @@ var InMemoryDriver = class {
1395
1431
  * does not have to exist already. Positions compare lexicographically.
1396
1432
  */
1397
1433
  get_next_sibling,
1434
+ /**
1435
+ * Return the position of the last (rightmost) child under parentId, or
1436
+ * undefined if the node has no children.
1437
+ */
1438
+ get_last_sibling,
1398
1439
  /**
1399
1440
  * Insert a child node with the given id.
1400
1441
  *
@@ -1600,8 +1641,8 @@ var Storage = class {
1600
1641
  }
1601
1642
  async createChildAsListItem(op2, node) {
1602
1643
  let fix;
1603
- const intent = op2.intent ?? "insert";
1604
- if (intent === "insert") {
1644
+ const intent2 = op2.intent ?? "insert";
1645
+ if (intent2 === "insert") {
1605
1646
  const insertedParentKey = await this.insertIntoList(op2.id, node);
1606
1647
  if (insertedParentKey !== node.parentKey) {
1607
1648
  op2 = { ...op2, parentKey: insertedParentKey };
@@ -1613,7 +1654,7 @@ var Storage = class {
1613
1654
  return accept(op2, fix);
1614
1655
  }
1615
1656
  return accept(op2);
1616
- } else if (intent === "set") {
1657
+ } else if (intent2 === "set") {
1617
1658
  const deletedId = op2.deletedId !== void 0 && op2.deletedId !== op2.id && this.loadedDriver.get_node(op2.deletedId)?.parentId === node.parentId ? op2.deletedId : void 0;
1618
1659
  if (deletedId !== void 0) {
1619
1660
  await this.loadedDriver.delete_node(deletedId);
@@ -1630,8 +1671,26 @@ var Storage = class {
1630
1671
  }
1631
1672
  await this.loadedDriver.set_child(op2.id, node, true);
1632
1673
  return accept(op2, fix);
1674
+ } else if (intent2 === "push") {
1675
+ const lastPos = this.loadedDriver.get_last_sibling(node.parentId);
1676
+ const guessedKey = asPos2(node.parentKey);
1677
+ const pushedKey = lastPos === void 0 || guessedKey > lastPos ? guessedKey : makePosition2(lastPos);
1678
+ await this.loadedDriver.set_child(op2.id, {
1679
+ ...node,
1680
+ parentKey: pushedKey
1681
+ });
1682
+ if (pushedKey !== node.parentKey) {
1683
+ op2 = { ...op2, parentKey: pushedKey };
1684
+ fix = {
1685
+ type: OpCode2.SET_PARENT_KEY,
1686
+ id: op2.id,
1687
+ parentKey: pushedKey
1688
+ };
1689
+ return accept(op2, fix);
1690
+ }
1691
+ return accept(op2);
1633
1692
  } else {
1634
- return assertNever2(intent, "Invalid intent");
1693
+ return assertNever2(intent2, "Invalid intent");
1635
1694
  }
1636
1695
  }
1637
1696
  async applyDeleteObjectKeyOp(op2) {
@@ -2035,6 +2094,7 @@ function isLeasedSessionExpired(leasedSession) {
2035
2094
  }
2036
2095
 
2037
2096
  // src/Room.ts
2097
+ var MB = 1024 * 1024;
2038
2098
  var messagesDecoder = array3(clientMsgDecoder);
2039
2099
  var ServerMsgCode2 = { ...CoreServerMsgCode, ...FeedMsgCode };
2040
2100
  var HIGHEST_PROTOCOL_VERSION = Math.max(
@@ -2048,6 +2108,11 @@ var SERVER_MSG_CODE_NAMES = Object.fromEntries(
2048
2108
  var BLACK_HOLE = new Logger([
2049
2109
  /* No targets, i.e. black hole logger */
2050
2110
  ]);
2111
+ function groupNodesForWebSocketMessages(input) {
2112
+ const MAX_SIZE = 16 * MB;
2113
+ const idealSize = (chunkIndex) => chunkIndex < 3 ? 1 * MB : Math.min(8, chunkIndex - 1) * MB;
2114
+ return chunkedByCost(input, (tup) => tup.length, MAX_SIZE, idealSize);
2115
+ }
2051
2116
  function collectSideEffects() {
2052
2117
  const deferred = [];
2053
2118
  return {
@@ -2181,7 +2246,7 @@ var BackendSession = class extends BrowserSession {
2181
2246
  super(ticket, socket, debug);
2182
2247
  }
2183
2248
  };
2184
- var __debug2, __allowStreaming;
2249
+ var __debug2;
2185
2250
  var Room = class {
2186
2251
  constructor(meta, options) {
2187
2252
  // ^^^^^^^^^^ User-defined Room Metadata, Session Metadata, and Client Metadata
@@ -2203,12 +2268,10 @@ var Room = class {
2203
2268
  __publicField(this, "sessions", new UniqueMap((s) => s.actor));
2204
2269
  __publicField(this, "hooks");
2205
2270
  __privateAdd(this, __debug2);
2206
- __privateAdd(this, __allowStreaming);
2207
2271
  const driver = options?.storage ?? makeNewInMemoryDriver();
2208
2272
  this.meta = meta;
2209
2273
  this.driver = driver;
2210
2274
  this.logger = options?.logger ?? BLACK_HOLE;
2211
- __privateSet(this, __allowStreaming, options?.allowStreaming ?? true);
2212
2275
  this.hooks = {
2213
2276
  isClientMsgAllowed: options?.hooks?.isClientMsgAllowed ?? (() => {
2214
2277
  return {
@@ -2973,7 +3036,13 @@ var Room = class {
2973
3036
  const toReplyImmediately = [];
2974
3037
  const toReplyAfter = [];
2975
3038
  const replyImmediately = (msg) => {
2976
- if (Array.isArray(msg)) {
3039
+ if (typeof msg === "string") {
3040
+ if (toReplyImmediately.length > 0) {
3041
+ session.send(toReplyImmediately);
3042
+ toReplyImmediately.length = 0;
3043
+ }
3044
+ session.send(msg);
3045
+ } else if (Array.isArray(msg)) {
2977
3046
  for (const m of msg) {
2978
3047
  toReplyImmediately.push(m);
2979
3048
  }
@@ -3031,24 +3100,10 @@ var Room = class {
3031
3100
  }
3032
3101
  case ClientMsgCode2.FETCH_STORAGE: {
3033
3102
  if (session.version >= 8 /* V8 */) {
3034
- if (__privateGet(this, __allowStreaming)) {
3035
- const NODES_PER_CHUNK = 250;
3036
- for (const chunk of chunked(
3037
- nodeStreamToCompactNodes(this.storage.loadedDriver.iter_nodes()),
3038
- NODES_PER_CHUNK
3039
- )) {
3040
- replyImmediately({
3041
- type: ServerMsgCode2.STORAGE_CHUNK,
3042
- nodes: chunk
3043
- });
3044
- }
3045
- } else {
3046
- replyImmediately({
3047
- type: ServerMsgCode2.STORAGE_CHUNK,
3048
- nodes: Array.from(
3049
- nodeStreamToCompactNodes(this.storage.loadedDriver.iter_nodes())
3050
- )
3051
- });
3103
+ const rawStream = this.storage.loadedDriver.iter_nodes_optimized();
3104
+ for (const chunk of groupNodesForWebSocketMessages(rawStream)) {
3105
+ const frame = `{"type":${ServerMsgCode2.STORAGE_CHUNK},"nodes":[${chunk.join(",")}]}`;
3106
+ replyImmediately(frame);
3052
3107
  }
3053
3108
  replyImmediately({ type: ServerMsgCode2.STORAGE_STREAM_END });
3054
3109
  } else {
@@ -3327,7 +3382,6 @@ var Room = class {
3327
3382
  }
3328
3383
  };
3329
3384
  __debug2 = new WeakMap();
3330
- __allowStreaming = new WeakMap();
3331
3385
  export {
3332
3386
  BackendSession,
3333
3387
  BrowserSession,