@liveblocks/core 1.1.0-test1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +93 -33
  2. package/dist/index.js +299 -191
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -636,14 +636,34 @@ declare enum WebsocketCloseCodes {
636
636
  CLOSE_WITHOUT_RETRY = 4999
637
637
  }
638
638
 
639
+ /**
640
+ * Old connection statuses, here for backward-compatibility reasons only.
641
+ */
642
+ declare type LegacyConnectionStatus = "closed" | "authenticating" | "connecting" | "open" | "unavailable" | "failed";
643
+ /**
644
+ * Returns a human-readable status indicating the current connection status of
645
+ * a Room, as returned by `room.getStatus()`. Can be used to implement
646
+ * a connection status badge.
647
+ */
648
+ declare type Status = "initial" | "connecting" | "connected" | "reconnecting" | "disconnected";
649
+ /**
650
+ * Used to report about app-level reconnection issues.
651
+ *
652
+ * Normal (quick) reconnects won't be reported as a "lost connection". Instead,
653
+ * the application will only get an event if the reconnection attempts by the
654
+ * client are taking (much) longer than usual. Definitely a situation you want
655
+ * to inform your users about, for example, by throwing a toast message on
656
+ * screen, or show a "trying to reconnect" banner.
657
+ */
658
+ declare type LostConnectionEvent = "lost" | "restored" | "failed";
639
659
  /**
640
660
  * Arbitrary record that will be used as the authentication "token". It's the
641
661
  * value that is returned by calling the authentication delegate, and will get
642
662
  * passed to the connection factory delegate. This value will be remembered by
643
663
  * the connection manager, but its value will not be interpreted, so it can be
644
- * any object value.
664
+ * any value (except null).
645
665
  */
646
- declare type BaseAuthResult = Record<string, unknown>;
666
+ declare type BaseAuthResult = NonNullable<Json>;
647
667
  declare type Delegates<T extends BaseAuthResult> = {
648
668
  authenticate: () => Promise<T>;
649
669
  createSocket: (token: T) => IWebSocketInstance;
@@ -735,28 +755,6 @@ declare type CustomEvent<TRoomEvent extends Json> = {
735
755
  connectionId: number;
736
756
  event: TRoomEvent;
737
757
  };
738
- declare type Connection = {
739
- status: "closed";
740
- } | {
741
- status: "authenticating";
742
- } | {
743
- status: "connecting";
744
- id: number;
745
- userId?: string;
746
- userInfo?: Json;
747
- isReadOnly: boolean;
748
- } | {
749
- status: "open";
750
- id: number;
751
- userId?: string;
752
- userInfo?: Json;
753
- isReadOnly: boolean;
754
- } | {
755
- status: "unavailable";
756
- } | {
757
- status: "failed";
758
- };
759
- declare type ConnectionStatus = Connection["status"];
760
758
  declare type StorageStatus = "not-loaded" | "loading" | "synchronizing" | "synchronized";
761
759
  interface History {
762
760
  /**
@@ -893,12 +891,47 @@ declare type SubscribeFn<TPresence extends JsonObject, _TStorage extends LsonObj
893
891
  */
894
892
  (type: "error", listener: ErrorCallback): () => void;
895
893
  /**
896
- * Subscribe to connection state updates.
894
+ * @deprecated This API will be removed in a future version of Liveblocks.
895
+ * Prefer using the newer `.subscribe('status')` API.
896
+ *
897
+ * We recommend making the following changes if you use these APIs:
898
+ *
899
+ * OLD APIs NEW APIs
900
+ * .getConnectionState() --> .getStatus()
901
+ * .subscribe('connection') --> .subscribe('status')
902
+ *
903
+ * OLD STATUSES NEW STATUSES
904
+ * closed --> initial
905
+ * authenticating --> connecting
906
+ * connecting --> connecting
907
+ * open --> connected
908
+ * unavailable --> reconnecting
909
+ * failed --> disconnected
910
+ *
911
+ * Subscribe to legacy connection status updates.
912
+ *
913
+ * @returns Unsubscribe function.
914
+ *
915
+ */
916
+ (type: "connection", listener: Callback<LegacyConnectionStatus>): () => void;
917
+ /**
918
+ * Subscribe to connection status updates. The callback will be called any
919
+ * time the status changes.
897
920
  *
898
921
  * @returns Unsubscribe function.
899
922
  *
900
923
  */
901
- (type: "connection", listener: Callback<ConnectionStatus>): () => void;
924
+ (type: "status", listener: Callback<Status>): () => void;
925
+ /**
926
+ * Subscribe to the exceptional event where reconnecting to the Liveblocks
927
+ * servers is taking longer than usual. This typically is a sign of a client
928
+ * that has lost internet connectivity.
929
+ *
930
+ * This isn't problematic (because the Liveblocks client is still trying to
931
+ * reconnect), but it's typically a good idea to inform users about it if
932
+ * the connection takes too long to recover.
933
+ */
934
+ (type: "lost-connection", listener: Callback<LostConnectionEvent>): () => void;
902
935
  /**
903
936
  * Subscribes to changes made on a Live structure. Returns an unsubscribe function.
904
937
  * In a future version, we will also expose what exactly changed in the Live structure.
@@ -975,7 +1008,30 @@ declare type Room<TPresence extends JsonObject, TStorage extends LsonObject, TUs
975
1008
  * metadata and connection ID (from the auth server).
976
1009
  */
977
1010
  isSelfAware(): boolean;
978
- getConnectionState(): ConnectionStatus;
1011
+ /**
1012
+ * @deprecated This API will be removed in a future version of Liveblocks.
1013
+ * Prefer using `.getStatus()` instead.
1014
+ *
1015
+ * We recommend making the following changes if you use these APIs:
1016
+ *
1017
+ * OLD APIs NEW APIs
1018
+ * .getConnectionState() --> .getStatus()
1019
+ * .subscribe('connection') --> .subscribe('status')
1020
+ *
1021
+ * OLD STATUSES NEW STATUSES
1022
+ * closed --> initial
1023
+ * authenticating --> connecting
1024
+ * connecting --> connecting
1025
+ * open --> connected
1026
+ * unavailable --> reconnecting
1027
+ * failed --> disconnected
1028
+ */
1029
+ getConnectionState(): LegacyConnectionStatus;
1030
+ /**
1031
+ * Return the current connection status for this room. Can be used to display
1032
+ * a status badge for your Liveblocks connection.
1033
+ */
1034
+ getStatus(): Status;
979
1035
  readonly subscribe: SubscribeFn<TPresence, TStorage, TUserMeta, TRoomEvent>;
980
1036
  /**
981
1037
  * Room's history contains functions that let you undo and redo operation made on by the current client on the presence and storage.
@@ -1056,6 +1112,9 @@ declare type Room<TPresence extends JsonObject, TStorage extends LsonObject, TUs
1056
1112
  */
1057
1113
  getStorageSnapshot(): LiveObject<TStorage> | null;
1058
1114
  readonly events: {
1115
+ readonly connection: Observable<LegacyConnectionStatus>;
1116
+ readonly status: Observable<Status>;
1117
+ readonly lostConnection: Observable<LostConnectionEvent>;
1059
1118
  readonly customEvent: Observable<{
1060
1119
  connectionId: number;
1061
1120
  event: TRoomEvent;
@@ -1066,12 +1125,12 @@ declare type Room<TPresence extends JsonObject, TStorage extends LsonObject, TUs
1066
1125
  event: OthersEvent<TPresence, TUserMeta>;
1067
1126
  }>;
1068
1127
  readonly error: Observable<Error>;
1069
- readonly connection: Observable<ConnectionStatus>;
1070
1128
  readonly storage: Observable<StorageUpdate[]>;
1071
1129
  readonly history: Observable<HistoryEvent>;
1072
1130
  /**
1073
- * Subscribe to the storage loaded event. Will fire at most once during the
1074
- * lifetime of a Room.
1131
+ * Subscribe to the storage loaded event. Will fire any time a full Storage
1132
+ * copy is downloaded. (This happens after the initial connect, and on
1133
+ * every reconnect.)
1075
1134
  */
1076
1135
  readonly storageDidLoad: Observable<void>;
1077
1136
  readonly storageStatus: Observable<StorageStatus>;
@@ -1168,6 +1227,7 @@ declare type AuthEndpoint = string | ((room: string) => Promise<{
1168
1227
  */
1169
1228
  declare type ClientOptions = {
1170
1229
  throttle?: number;
1230
+ lostConnectionTimeout?: number;
1171
1231
  polyfills?: Polyfills;
1172
1232
  unstable_fallbackToHTTP?: boolean;
1173
1233
  /**
@@ -1762,7 +1822,7 @@ declare type ClientToPanelMessage =
1762
1822
  | {
1763
1823
  msg: "room::sync::full";
1764
1824
  roomId: string;
1765
- status: ConnectionStatus;
1825
+ status: Status;
1766
1826
  storage: readonly LsonTreeNode[] | null;
1767
1827
  me: UserTreeNode | null;
1768
1828
  others: readonly UserTreeNode[];
@@ -1773,7 +1833,7 @@ declare type ClientToPanelMessage =
1773
1833
  | {
1774
1834
  msg: "room::sync::partial";
1775
1835
  roomId: string;
1776
- status?: ConnectionStatus;
1836
+ status?: Status;
1777
1837
  storage?: readonly LsonTreeNode[];
1778
1838
  me?: UserTreeNode;
1779
1839
  others?: readonly UserTreeNode[];
@@ -1824,4 +1884,4 @@ declare type EnsureJson<T> = [
1824
1884
  [K in keyof T]: EnsureJson<T[K]>;
1825
1885
  };
1826
1886
 
1827
- export { AckOp, AppOnlyAuthToken, AuthToken, BaseAuthResult, BaseUserMeta, BroadcastEventClientMsg, BroadcastOptions, BroadcastedEventServerMsg, Client, ClientMsg, ClientMsgCode, ConnectionStatus, CrdtType, CreateChildOp, CreateListOp, CreateMapOp, CreateObjectOp, CreateOp, CreateRegisterOp, CreateRootObjectOp, Delegates, DeleteCrdtOp, DeleteObjectKeyOp, DevToolsTreeNode as DevTools, protocol as DevToolsMsg, EnsureJson, FetchStorageClientMsg, History, IWebSocket, IWebSocketCloseEvent, IWebSocketEvent, IWebSocketInstance, IWebSocketMessageEvent, IdTuple, Immutable, InitialDocumentStateServerMsg, Json, JsonArray, JsonObject, JsonScalar, LiveList, LiveListUpdate, LiveMap, LiveMapUpdate, LiveNode, LiveObject, LiveObjectUpdate, LiveStructure, Lson, LsonObject, NodeMap, Op, OpCode, Others, ParentToChildNodeMap, PlainLson, PlainLsonFields, PlainLsonList, PlainLsonMap, PlainLsonObject, RejectedStorageOpServerMsg, Resolve, Room, RoomAuthToken, RoomInitializers, RoomStateServerMsg, SerializedChild, SerializedCrdt, SerializedList, SerializedMap, SerializedObject, SerializedRegister, SerializedRootObject, ServerMsg, ServerMsgCode, SetParentKeyOp, StorageStatus, StorageUpdate, ToImmutable, ToJson, UpdateObjectOp, UpdatePresenceClientMsg, UpdatePresenceServerMsg, UpdateStorageClientMsg, UpdateStorageServerMsg, User, UserJoinServerMsg, UserLeftServerMsg, WebsocketCloseCodes, asArrayWithLegacyMethods, asPos, assert, assertNever, b64decode, createClient, deprecate, deprecateIf, errorIf, freeze, isAppOnlyAuthToken, isAuthToken, isChildCrdt, isJsonArray, isJsonObject, isJsonScalar, isPlainObject, isRoomAuthToken, isRootCrdt, legacy_patchImmutableObject, lsonToJson, makePosition, nn, patchLiveObjectKey, shallow, throwUsageError, toPlainLson, tryParseJson, withTimeout };
1887
+ export { AckOp, AppOnlyAuthToken, AuthToken, BaseAuthResult, BaseUserMeta, BroadcastEventClientMsg, BroadcastOptions, BroadcastedEventServerMsg, Client, ClientMsg, ClientMsgCode, CrdtType, CreateChildOp, CreateListOp, CreateMapOp, CreateObjectOp, CreateOp, CreateRegisterOp, CreateRootObjectOp, Delegates, DeleteCrdtOp, DeleteObjectKeyOp, DevToolsTreeNode as DevTools, protocol as DevToolsMsg, EnsureJson, FetchStorageClientMsg, History, IWebSocket, IWebSocketCloseEvent, IWebSocketEvent, IWebSocketInstance, IWebSocketMessageEvent, IdTuple, Immutable, InitialDocumentStateServerMsg, Json, JsonArray, JsonObject, JsonScalar, LegacyConnectionStatus, LiveList, LiveListUpdate, LiveMap, LiveMapUpdate, LiveNode, LiveObject, LiveObjectUpdate, LiveStructure, LostConnectionEvent, Lson, LsonObject, NodeMap, Op, OpCode, Others, ParentToChildNodeMap, PlainLson, PlainLsonFields, PlainLsonList, PlainLsonMap, PlainLsonObject, RejectedStorageOpServerMsg, Resolve, Room, RoomAuthToken, RoomInitializers, RoomStateServerMsg, SerializedChild, SerializedCrdt, SerializedList, SerializedMap, SerializedObject, SerializedRegister, SerializedRootObject, ServerMsg, ServerMsgCode, SetParentKeyOp, Status, StorageStatus, StorageUpdate, ToImmutable, ToJson, UpdateObjectOp, UpdatePresenceClientMsg, UpdatePresenceServerMsg, UpdateStorageClientMsg, UpdateStorageServerMsg, User, UserJoinServerMsg, UserLeftServerMsg, WebsocketCloseCodes, asArrayWithLegacyMethods, asPos, assert, assertNever, b64decode, createClient, deprecate, deprecateIf, errorIf, freeze, isAppOnlyAuthToken, isAuthToken, isChildCrdt, isJsonArray, isJsonObject, isJsonScalar, isPlainObject, isRoomAuthToken, isRootCrdt, legacy_patchImmutableObject, lsonToJson, makePosition, nn, patchLiveObjectKey, shallow, throwUsageError, toPlainLson, tryParseJson, withTimeout };
package/dist/index.js CHANGED
@@ -157,7 +157,7 @@ var onMessageFromPanel = eventSource.observable;
157
157
  // src/devtools/index.ts
158
158
  var VERSION = true ? (
159
159
  /* istanbul ignore next */
160
- "1.1.0-test1"
160
+ "1.1.0"
161
161
  ) : "dev";
162
162
  var _devtoolsSetupHasRun = false;
163
163
  function setupDevTools(getAllRooms) {
@@ -199,7 +199,7 @@ function startSyncStream(room) {
199
199
  fullSync(room);
200
200
  unsubsByRoomId.set(room.id, [
201
201
  // When the connection status changes
202
- room.events.connection.subscribe(() => partialSyncConnection(room)),
202
+ room.events.status.subscribe(() => partialSyncConnection(room)),
203
203
  // When storage initializes, send the update
204
204
  room.events.storageDidLoad.subscribeOnce(() => partialSyncStorage(room)),
205
205
  // Any time storage updates, send the new storage root
@@ -213,7 +213,7 @@ function partialSyncConnection(room) {
213
213
  sendToPanel({
214
214
  msg: "room::sync::partial",
215
215
  roomId: room.id,
216
- status: room.getConnectionState()
216
+ status: room.getStatus()
217
217
  });
218
218
  }
219
219
  function partialSyncStorage(room) {
@@ -254,7 +254,7 @@ function fullSync(room) {
254
254
  sendToPanel({
255
255
  msg: "room::sync::full",
256
256
  roomId: room.id,
257
- status: room.getConnectionState(),
257
+ status: room.getStatus(),
258
258
  storage: (_a = root == null ? void 0 : root.toTreeNode("root").payload) != null ? _a : null,
259
259
  me,
260
260
  others
@@ -823,22 +823,37 @@ function withTimeout(promise, millis, errmsg = "Timed out") {
823
823
  }
824
824
 
825
825
  // src/connection.ts
826
- function toPublicConnectionStatus(state) {
826
+ function newToLegacyStatus(status) {
827
+ switch (status) {
828
+ case "connecting":
829
+ return "connecting";
830
+ case "connected":
831
+ return "open";
832
+ case "reconnecting":
833
+ return "unavailable";
834
+ case "disconnected":
835
+ return "failed";
836
+ case "initial":
837
+ return "closed";
838
+ default:
839
+ return "closed";
840
+ }
841
+ }
842
+ function toNewConnectionStatus(machine) {
843
+ const state = machine.currentState;
827
844
  switch (state) {
828
845
  case "@ok.connected":
829
846
  case "@ok.awaiting-pong":
830
- return "open";
847
+ return "connected";
831
848
  case "@idle.initial":
832
- return "closed";
849
+ return "initial";
833
850
  case "@auth.busy":
834
851
  case "@auth.backoff":
835
- return "authenticating";
836
852
  case "@connecting.busy":
837
- return "connecting";
838
853
  case "@connecting.backoff":
839
- return "unavailable";
854
+ return machine.context.successCount > 0 ? "reconnecting" : "connecting";
840
855
  case "@idle.failed":
841
- return "failed";
856
+ return "disconnected";
842
857
  default:
843
858
  return assertNever(state, "Unknown state");
844
859
  }
@@ -873,6 +888,9 @@ function increaseBackoffDelayAggressively(context) {
873
888
  backoffDelay: nextBackoffDelay(context.backoffDelay, BACKOFF_DELAYS_SLOW)
874
889
  });
875
890
  }
891
+ function resetSuccessCount(context) {
892
+ context.patch({ successCount: 0 });
893
+ }
876
894
  function log(level, message) {
877
895
  const logger = level === 2 /* ERROR */ ? error : level === 1 /* WARN */ ? warn : (
878
896
  /* black hole */
@@ -883,9 +901,34 @@ function log(level, message) {
883
901
  logger(message);
884
902
  };
885
903
  }
886
- function sendHeartbeat(ctx) {
887
- var _a;
888
- (_a = ctx.socket) == null ? void 0 : _a.send("ping");
904
+ function logPrematureErrorOrCloseEvent(e) {
905
+ const conn = "Connection to Liveblocks websocket server";
906
+ return (ctx) => {
907
+ if (e instanceof Error) {
908
+ warn(`${conn} could not be established. ${String(e)}`);
909
+ } else {
910
+ warn(
911
+ isCloseEvent(e) ? `${conn} closed prematurely (code: ${e.code}). Retrying in ${ctx.backoffDelay}ms.` : `${conn} could not be established.`
912
+ );
913
+ }
914
+ };
915
+ }
916
+ function logCloseEvent(event) {
917
+ return (ctx) => {
918
+ warn(
919
+ `Connection to Liveblocks websocket server closed (code: ${event.code}). Retrying in ${ctx.backoffDelay}ms.`
920
+ );
921
+ };
922
+ }
923
+ var logPermanentClose = log(
924
+ 1 /* WARN */,
925
+ "Connection to WebSocket closed permanently. Won't retry."
926
+ );
927
+ function isCloseEvent(error2) {
928
+ return !(error2 instanceof Error) && error2.type === "close";
929
+ }
930
+ function isCustomCloseEvent(error2) {
931
+ return isCloseEvent(error2) && error2.code >= 4e3 && error2.code < 4100;
889
932
  }
890
933
  function enableTracing(machine) {
891
934
  const start = (/* @__PURE__ */ new Date()).getTime();
@@ -916,16 +959,18 @@ function defineConnectivityEvents(machine) {
916
959
  const statusDidChange = makeEventSource();
917
960
  const didConnect = makeEventSource();
918
961
  const didDisconnect = makeEventSource();
919
- let oldPublicStatus = null;
920
- const unsubscribe = machine.events.didEnterState.subscribe((newState) => {
921
- const newPublicStatus = toPublicConnectionStatus(newState);
922
- statusDidChange.notify(newPublicStatus);
923
- if (oldPublicStatus === "open" && newPublicStatus !== "open") {
962
+ let lastStatus = null;
963
+ const unsubscribe = machine.events.didEnterState.subscribe(() => {
964
+ const currStatus = toNewConnectionStatus(machine);
965
+ if (currStatus !== lastStatus) {
966
+ statusDidChange.notify(currStatus);
967
+ }
968
+ if (lastStatus === "connected" && currStatus !== "connected") {
924
969
  didDisconnect.notify();
925
- } else if (oldPublicStatus !== "open" && newPublicStatus === "open") {
970
+ } else if (lastStatus !== "connected" && currStatus === "connected") {
926
971
  didConnect.notify();
927
972
  }
928
- oldPublicStatus = newPublicStatus;
973
+ lastStatus = currStatus;
929
974
  });
930
975
  return {
931
976
  statusDidChange: statusDidChange.observable,
@@ -940,6 +985,7 @@ function createConnectionStateMachine(delegates, enableDebugLogging) {
940
985
  onMessage.pause();
941
986
  const onLiveblocksError = makeEventSource();
942
987
  const initialContext = {
988
+ successCount: 0,
943
989
  token: null,
944
990
  socket: null,
945
991
  backoffDelay: RESET_DELAY
@@ -948,11 +994,11 @@ function createConnectionStateMachine(delegates, enableDebugLogging) {
948
994
  machine.addTransitions("*", {
949
995
  RECONNECT: {
950
996
  target: "@auth.backoff",
951
- effect: increaseBackoffDelay
997
+ effect: [increaseBackoffDelay, resetSuccessCount]
952
998
  },
953
999
  DISCONNECT: "@idle.initial"
954
1000
  });
955
- machine.addTransitions("@idle.*", {
1001
+ machine.onEnter("@idle.*", resetSuccessCount).addTransitions("@idle.*", {
956
1002
  CONNECT: (_, ctx) => (
957
1003
  // If we still have a known token, try to reconnect to the socket directly,
958
1004
  // otherwise, try to obtain a new token
@@ -1097,41 +1143,41 @@ function createConnectionStateMachine(delegates, enableDebugLogging) {
1097
1143
  effect: log(2 /* ERROR */, err.message)
1098
1144
  };
1099
1145
  }
1146
+ if (isCloseEvent(err) && err.code === 4999) {
1147
+ return {
1148
+ target: "@idle.failed",
1149
+ effect: log(2 /* ERROR */, err.reason)
1150
+ };
1151
+ }
1152
+ if (isCustomCloseEvent(err) && err.code !== 4001) {
1153
+ return {
1154
+ target: "@connecting.backoff",
1155
+ effect: [
1156
+ increaseBackoffDelayAggressively,
1157
+ logPrematureErrorOrCloseEvent(err)
1158
+ ]
1159
+ };
1160
+ }
1100
1161
  return {
1101
1162
  target: "@auth.backoff",
1102
- effect: [
1103
- // Increase the backoff delay conditionally
1104
- // TODO: This is ugly. DRY this up with the other code 40xx checks elsewhere.
1105
- !(err instanceof Error) && err.type === "close" && err.code >= 4e3 && err.code <= 4100 ? increaseBackoffDelayAggressively : increaseBackoffDelay,
1106
- // Produce a useful log message
1107
- (ctx) => {
1108
- if (err instanceof Error) {
1109
- warn(String(err));
1110
- } else {
1111
- warn(
1112
- err.type === "close" ? `Connection to Liveblocks websocket server closed prematurely (code: ${err.code}). Retrying in ${ctx.backoffDelay}ms.` : "Connection to Liveblocks websocket server could not be established."
1113
- );
1114
- }
1115
- }
1116
- ]
1163
+ effect: [increaseBackoffDelay, logPrematureErrorOrCloseEvent(err)]
1117
1164
  };
1118
1165
  }
1119
1166
  );
1120
- machine.addTimedTransition("@ok.connected", HEARTBEAT_INTERVAL, {
1167
+ const sendHeartbeat = {
1121
1168
  target: "@ok.awaiting-pong",
1122
- effect: sendHeartbeat
1123
- }).addTransitions("@ok.connected", {
1124
- WINDOW_GOT_FOCUS: { target: "@ok.awaiting-pong", effect: sendHeartbeat }
1125
- });
1126
- const noPongAction = {
1127
- target: "@connecting.busy",
1128
- // Log implicit connection loss and drop the current open socket
1129
- effect: log(
1130
- 1 /* WARN */,
1131
- "Received no pong from server, assume implicit connection loss."
1132
- )
1169
+ effect: (ctx) => {
1170
+ var _a;
1171
+ (_a = ctx.socket) == null ? void 0 : _a.send("ping");
1172
+ }
1133
1173
  };
1134
- machine.onEnter("@ok.*", () => {
1174
+ machine.addTimedTransition("@ok.connected", HEARTBEAT_INTERVAL, sendHeartbeat).addTransitions("@ok.connected", {
1175
+ NAVIGATOR_OFFLINE: sendHeartbeat,
1176
+ // Don't take the browser's word for it when it says it's offline. Do a ping/pong to make sure.
1177
+ WINDOW_GOT_FOCUS: sendHeartbeat
1178
+ });
1179
+ machine.onEnter("@ok.*", (ctx) => {
1180
+ ctx.patch({ successCount: ctx.successCount + 1 });
1135
1181
  const timerID = setTimeout(
1136
1182
  // On the next tick, start delivering all messages that have already
1137
1183
  // been received, and continue synchronous delivery of all future
@@ -1139,13 +1185,20 @@ function createConnectionStateMachine(delegates, enableDebugLogging) {
1139
1185
  onMessage.unpause,
1140
1186
  0
1141
1187
  );
1142
- return (ctx) => {
1143
- teardownSocket(ctx.socket);
1144
- ctx.patch({ socket: null });
1188
+ return (ctx2) => {
1189
+ teardownSocket(ctx2.socket);
1190
+ ctx2.patch({ socket: null });
1145
1191
  clearTimeout(timerID);
1146
1192
  onMessage.pause();
1147
1193
  };
1148
- }).addTimedTransition("@ok.awaiting-pong", PONG_TIMEOUT, noPongAction).addTransitions("@ok.awaiting-pong", { PONG_TIMEOUT: noPongAction }).addTransitions("@ok.awaiting-pong", { PONG: "@ok.connected" }).addTransitions("@ok.*", {
1194
+ }).addTransitions("@ok.awaiting-pong", { PONG: "@ok.connected" }).addTimedTransition("@ok.awaiting-pong", PONG_TIMEOUT, {
1195
+ target: "@connecting.busy",
1196
+ // Log implicit connection loss and drop the current open socket
1197
+ effect: log(
1198
+ 1 /* WARN */,
1199
+ "Received no pong from server, assume implicit connection loss."
1200
+ )
1201
+ }).addTransitions("@ok.*", {
1149
1202
  // When a socket receives an error, this can cause the closing of the
1150
1203
  // socket, or not. So always check to see if the socket is still OPEN or
1151
1204
  // not. When still OPEN, don't transition.
@@ -1163,37 +1216,31 @@ function createConnectionStateMachine(delegates, enableDebugLogging) {
1163
1216
  if (e.event.code === 4999) {
1164
1217
  return {
1165
1218
  target: "@idle.failed",
1166
- effect: log(
1167
- 1 /* WARN */,
1168
- "Connection to WebSocket closed permanently. Won't retry."
1169
- )
1219
+ effect: logPermanentClose
1220
+ };
1221
+ }
1222
+ if (e.event.code === 4001) {
1223
+ return {
1224
+ target: "@auth.backoff",
1225
+ effect: [increaseBackoffDelay, logCloseEvent(e.event)]
1170
1226
  };
1171
1227
  }
1172
- if (e.event.code >= 4e3 && e.event.code <= 4100) {
1228
+ if (isCustomCloseEvent(e.event)) {
1173
1229
  return {
1174
1230
  target: "@connecting.backoff",
1175
1231
  effect: [
1176
1232
  increaseBackoffDelayAggressively,
1177
- (ctx) => warn(
1178
- `Connection to Liveblocks websocket server closed (code: ${e.event.code}). Retrying in ${ctx.backoffDelay}ms.`
1179
- ),
1180
- (_, { event }) => {
1181
- if (event.code >= 4e3 && event.code <= 4100) {
1182
- const err = new LiveblocksError(event.reason, event.code);
1183
- onLiveblocksError.notify(err);
1184
- }
1233
+ logCloseEvent(e.event),
1234
+ () => {
1235
+ const err = new LiveblocksError(e.event.reason, e.event.code);
1236
+ onLiveblocksError.notify(err);
1185
1237
  }
1186
1238
  ]
1187
1239
  };
1188
1240
  }
1189
1241
  return {
1190
1242
  target: "@connecting.backoff",
1191
- effect: [
1192
- increaseBackoffDelay,
1193
- (ctx) => warn(
1194
- `Connection to Liveblocks websocket server closed (code: ${e.event.code}). Retrying in ${ctx.backoffDelay}ms.`
1195
- )
1196
- ]
1243
+ effect: [increaseBackoffDelay, logCloseEvent(e.event)]
1197
1244
  };
1198
1245
  }
1199
1246
  });
@@ -1202,7 +1249,10 @@ function createConnectionStateMachine(delegates, enableDebugLogging) {
1202
1249
  const win = typeof window !== "undefined" ? window : void 0;
1203
1250
  const root = win != null ? win : doc;
1204
1251
  machine.onEnter("*", (ctx) => {
1205
- function onBackOnline() {
1252
+ function onNetworkOffline() {
1253
+ machine.send({ type: "NAVIGATOR_OFFLINE" });
1254
+ }
1255
+ function onNetworkBackOnline() {
1206
1256
  machine.send({ type: "NAVIGATOR_ONLINE" });
1207
1257
  }
1208
1258
  function onVisibilityChange() {
@@ -1210,11 +1260,13 @@ function createConnectionStateMachine(delegates, enableDebugLogging) {
1210
1260
  machine.send({ type: "WINDOW_GOT_FOCUS" });
1211
1261
  }
1212
1262
  }
1213
- win == null ? void 0 : win.addEventListener("online", onBackOnline);
1263
+ win == null ? void 0 : win.addEventListener("online", onNetworkBackOnline);
1264
+ win == null ? void 0 : win.addEventListener("offline", onNetworkOffline);
1214
1265
  root == null ? void 0 : root.addEventListener("visibilitychange", onVisibilityChange);
1215
1266
  return () => {
1216
1267
  root == null ? void 0 : root.removeEventListener("visibilitychange", onVisibilityChange);
1217
- win == null ? void 0 : win.removeEventListener("online", onBackOnline);
1268
+ win == null ? void 0 : win.removeEventListener("online", onNetworkBackOnline);
1269
+ win == null ? void 0 : win.removeEventListener("offline", onNetworkOffline);
1218
1270
  teardownSocket(ctx.socket);
1219
1271
  };
1220
1272
  });
@@ -1249,22 +1301,21 @@ var ManagedSocket = class {
1249
1301
  this.events = events;
1250
1302
  this.cleanups = cleanups;
1251
1303
  }
1252
- get status() {
1304
+ getLegacyStatus() {
1305
+ return newToLegacyStatus(this.getStatus());
1306
+ }
1307
+ getStatus() {
1253
1308
  try {
1254
- return toPublicConnectionStatus(this.machine.currentState);
1309
+ return toNewConnectionStatus(this.machine);
1255
1310
  } catch (e) {
1256
- return "closed";
1311
+ return "initial";
1257
1312
  }
1258
1313
  }
1259
1314
  /**
1260
1315
  * Returns the current auth token.
1261
1316
  */
1262
1317
  get token() {
1263
- const tok = this.machine.context.token;
1264
- if (tok === null) {
1265
- throw new Error("Unexpected null token here");
1266
- }
1267
- return tok;
1318
+ return this.machine.context.token;
1268
1319
  }
1269
1320
  /**
1270
1321
  * Call this method to try to connect to a WebSocket. This only has an effect
@@ -4173,9 +4224,6 @@ function makeIdFactory(connectionId) {
4173
4224
  let count = 0;
4174
4225
  return () => `${connectionId}:${count++}`;
4175
4226
  }
4176
- function isConnectionSelfAware(connection) {
4177
- return connection.status === "open" || connection.status === "connecting";
4178
- }
4179
4227
  function userToTreeNode(key, user) {
4180
4228
  return {
4181
4229
  type: "User",
@@ -4204,7 +4252,6 @@ function createRoom(options, config) {
4204
4252
  config.enableDebugLogging
4205
4253
  );
4206
4254
  const context = {
4207
- lastConnectionId: null,
4208
4255
  buffer: {
4209
4256
  flushTimerID: void 0,
4210
4257
  lastFlushedAt: 0,
@@ -4218,7 +4265,7 @@ function createRoom(options, config) {
4218
4265
  messages: [],
4219
4266
  storageOperations: []
4220
4267
  },
4221
- connection: new ValueRef({ status: "closed" }),
4268
+ sessionInfo: new ValueRef(null),
4222
4269
  me: new MeRef(initialPresence),
4223
4270
  others: new OthersRef(),
4224
4271
  initialStorage,
@@ -4238,55 +4285,78 @@ function createRoom(options, config) {
4238
4285
  };
4239
4286
  const doNotBatchUpdates = (cb) => cb();
4240
4287
  const batchUpdates = (_d = config.unstable_batchedUpdates) != null ? _d : doNotBatchUpdates;
4288
+ let lastToken;
4241
4289
  function onStatusDidChange(newStatus) {
4242
- if (newStatus === "open" || newStatus === "connecting") {
4243
- context.connection.set({
4244
- status: newStatus,
4245
- id: managedSocket.token.parsed.actor,
4246
- userInfo: managedSocket.token.parsed.info,
4247
- userId: managedSocket.token.parsed.id,
4248
- isReadOnly: isStorageReadOnly(managedSocket.token.parsed.scopes)
4290
+ var _a2;
4291
+ const token = (_a2 = managedSocket.token) == null ? void 0 : _a2.parsed;
4292
+ if (token !== void 0 && token !== lastToken) {
4293
+ context.sessionInfo.set({
4294
+ id: token.actor,
4295
+ userInfo: token.info,
4296
+ userId: token.id,
4297
+ isReadOnly: isStorageReadOnly(token.scopes)
4249
4298
  });
4250
- } else {
4251
- context.connection.set({ status: newStatus });
4299
+ lastToken = token;
4252
4300
  }
4253
4301
  batchUpdates(() => {
4254
- eventHub.connection.notify(newStatus);
4302
+ eventHub.status.notify(newStatus);
4303
+ eventHub.connection.notify(newToLegacyStatus(newStatus));
4255
4304
  });
4256
4305
  }
4257
- function onDidConnect() {
4258
- const conn = context.connection.current;
4259
- if (conn.status !== "open") {
4260
- throw new Error("Unexpected non-open state here");
4261
- }
4262
- if (context.lastConnectionId !== void 0) {
4263
- context.buffer.me = {
4264
- type: "full",
4265
- data: (
4266
- // Because state.me.current is a readonly object, we'll have to
4267
- // make a copy here. Otherwise, type errors happen later when
4268
- // "patching" my presence.
4269
- __spreadValues({}, context.me.current)
4270
- )
4271
- };
4272
- tryFlushing();
4306
+ let _connectionLossTimerId;
4307
+ let _hasLostConnection = false;
4308
+ function handleConnectionLossEvent(newStatus) {
4309
+ if (newStatus === "reconnecting") {
4310
+ _connectionLossTimerId = setTimeout(() => {
4311
+ batchUpdates(() => {
4312
+ eventHub.lostConnection.notify("lost");
4313
+ _hasLostConnection = true;
4314
+ context.others.clearOthers();
4315
+ notify({ others: [{ type: "reset" }] }, doNotBatchUpdates);
4316
+ });
4317
+ }, config.lostConnectionTimeout);
4318
+ } else {
4319
+ clearTimeout(_connectionLossTimerId);
4320
+ if (_hasLostConnection) {
4321
+ if (newStatus === "disconnected") {
4322
+ batchUpdates(() => {
4323
+ eventHub.lostConnection.notify("failed");
4324
+ });
4325
+ } else {
4326
+ batchUpdates(() => {
4327
+ eventHub.lostConnection.notify("restored");
4328
+ });
4329
+ }
4330
+ _hasLostConnection = false;
4331
+ }
4273
4332
  }
4274
- context.lastConnectionId = conn.id;
4275
- context.idFactory = makeIdFactory(conn.id);
4276
- if (context.root) {
4277
- context.buffer.messages.push({ type: 200 /* FETCH_STORAGE */ });
4333
+ }
4334
+ function onDidConnect() {
4335
+ const sessionInfo = context.sessionInfo.current;
4336
+ if (sessionInfo === null) {
4337
+ throw new Error("Unexpected missing session info");
4338
+ }
4339
+ context.buffer.me = {
4340
+ type: "full",
4341
+ data: (
4342
+ // Because context.me.current is a readonly object, we'll have to
4343
+ // make a copy here. Otherwise, type errors happen later when
4344
+ // "patching" my presence.
4345
+ __spreadValues({}, context.me.current)
4346
+ )
4347
+ };
4348
+ context.idFactory = makeIdFactory(sessionInfo.id);
4349
+ if (_getStorage$ !== null) {
4350
+ refreshStorage({ flush: false });
4278
4351
  }
4279
- tryFlushing();
4352
+ flushNowOrSoon();
4280
4353
  }
4281
4354
  function onDidDisconnect() {
4282
4355
  clearTimeout(context.buffer.flushTimerID);
4283
- batchUpdates(() => {
4284
- context.others.clearOthers();
4285
- notify({ others: [{ type: "reset" }] }, doNotBatchUpdates);
4286
- });
4287
4356
  }
4288
4357
  managedSocket.events.onMessage.subscribe(handleServerMessage);
4289
4358
  managedSocket.events.statusDidChange.subscribe(onStatusDidChange);
4359
+ managedSocket.events.statusDidChange.subscribe(handleConnectionLossEvent);
4290
4360
  managedSocket.events.didConnect.subscribe(onDidConnect);
4291
4361
  managedSocket.events.didDisconnect.subscribe(onDidDisconnect);
4292
4362
  managedSocket.events.onLiveblocksError.subscribe((err) => {
@@ -4340,7 +4410,8 @@ function createRoom(options, config) {
4340
4410
  }
4341
4411
  },
4342
4412
  assertStorageIsWritable: () => {
4343
- if (isConnectionSelfAware(context.connection.current) && context.connection.current.isReadOnly) {
4413
+ var _a2;
4414
+ if ((_a2 = context.sessionInfo.current) == null ? void 0 : _a2.isReadOnly) {
4344
4415
  throw new Error(
4345
4416
  "Cannot write to storage with a read only user, please ensure the user has write permissions"
4346
4417
  );
@@ -4348,22 +4419,26 @@ function createRoom(options, config) {
4348
4419
  }
4349
4420
  };
4350
4421
  const eventHub = {
4422
+ connection: makeEventSource(),
4423
+ // Old/deprecated API
4424
+ status: makeEventSource(),
4425
+ // New/recommended API
4426
+ lostConnection: makeEventSource(),
4351
4427
  customEvent: makeEventSource(),
4352
4428
  me: makeEventSource(),
4353
4429
  others: makeEventSource(),
4354
4430
  error: makeEventSource(),
4355
- connection: makeEventSource(),
4356
4431
  storage: makeEventSource(),
4357
4432
  history: makeEventSource(),
4358
4433
  storageDidLoad: makeEventSource(),
4359
4434
  storageStatus: makeEventSource()
4360
4435
  };
4361
4436
  function sendMessages(messageOrMessages) {
4362
- var _a2;
4437
+ var _a2, _b2;
4363
4438
  const message = JSON.stringify(messageOrMessages);
4364
4439
  if (config.unstable_fallbackToHTTP) {
4365
4440
  const size = new TextEncoder().encode(message).length;
4366
- if (size > MAX_MESSAGE_SIZE && managedSocket.token.raw && config.httpSendEndpoint) {
4441
+ if (size > MAX_MESSAGE_SIZE && ((_a2 = managedSocket.token) == null ? void 0 : _a2.raw) && config.httpSendEndpoint) {
4367
4442
  if (isTokenExpired(managedSocket.token.parsed)) {
4368
4443
  return managedSocket.reconnect();
4369
4444
  }
@@ -4371,7 +4446,7 @@ function createRoom(options, config) {
4371
4446
  message,
4372
4447
  managedSocket.token.raw,
4373
4448
  config.httpSendEndpoint,
4374
- (_a2 = config.polyfills) == null ? void 0 : _a2.fetch
4449
+ (_b2 = config.polyfills) == null ? void 0 : _b2.fetch
4375
4450
  );
4376
4451
  warn(
4377
4452
  "Message was too large for websockets and sent over HTTP instead"
@@ -4382,15 +4457,17 @@ function createRoom(options, config) {
4382
4457
  managedSocket.send(message);
4383
4458
  }
4384
4459
  const self = new DerivedRef(
4385
- context.connection,
4460
+ context.sessionInfo,
4386
4461
  context.me,
4387
- (conn, me) => isConnectionSelfAware(conn) ? {
4388
- connectionId: conn.id,
4389
- id: conn.userId,
4390
- info: conn.userInfo,
4391
- presence: me,
4392
- isReadOnly: conn.isReadOnly
4393
- } : null
4462
+ (info, me) => {
4463
+ return info !== null ? {
4464
+ connectionId: info.id,
4465
+ id: info.userId,
4466
+ info: info.userInfo,
4467
+ presence: me,
4468
+ isReadOnly: info.isReadOnly
4469
+ } : null;
4470
+ }
4394
4471
  );
4395
4472
  const selfAsTreeNode = new DerivedRef(
4396
4473
  self,
@@ -4400,7 +4477,7 @@ function createRoom(options, config) {
4400
4477
  if (message.items.length === 0) {
4401
4478
  throw new Error("Internal error: cannot load storage without items");
4402
4479
  }
4403
- if (context.root) {
4480
+ if (context.root !== void 0) {
4404
4481
  updateRoot(message.items, batchedUpdatesWrapper);
4405
4482
  } else {
4406
4483
  context.root = LiveObject._fromItems(message.items, pool);
@@ -4412,7 +4489,7 @@ function createRoom(options, config) {
4412
4489
  }
4413
4490
  }
4414
4491
  function updateRoot(items, batchedUpdatesWrapper) {
4415
- if (!context.root) {
4492
+ if (context.root === void 0) {
4416
4493
  return;
4417
4494
  }
4418
4495
  const currentItems = /* @__PURE__ */ new Map();
@@ -4459,11 +4536,9 @@ function createRoom(options, config) {
4459
4536
  });
4460
4537
  }
4461
4538
  function getConnectionId() {
4462
- const conn = context.connection.current;
4463
- if (isConnectionSelfAware(conn)) {
4464
- return conn.id;
4465
- } else if (context.lastConnectionId !== null) {
4466
- return context.lastConnectionId;
4539
+ const info = context.sessionInfo.current;
4540
+ if (info) {
4541
+ return info.id;
4467
4542
  }
4468
4543
  throw new Error(
4469
4544
  "Internal. Tried to get connection id but connection was never open"
@@ -4612,7 +4687,7 @@ function createRoom(options, config) {
4612
4687
  }
4613
4688
  context.activeBatch.updates.presence = true;
4614
4689
  } else {
4615
- tryFlushing();
4690
+ flushNowOrSoon();
4616
4691
  batchUpdates(() => {
4617
4692
  if (options2 == null ? void 0 : options2.addToHistory) {
4618
4693
  addToUndoStack(
@@ -4699,7 +4774,7 @@ function createRoom(options, config) {
4699
4774
  data: context.me.current,
4700
4775
  targetActor: message.actor
4701
4776
  });
4702
- tryFlushing();
4777
+ flushNowOrSoon();
4703
4778
  const user = context.others.getUser(message.actor);
4704
4779
  return user ? { type: "enter", user } : void 0;
4705
4780
  }
@@ -4785,9 +4860,7 @@ function createRoom(options, config) {
4785
4860
  const unacknowledgedOps = new Map(context.unacknowledgedOps);
4786
4861
  createOrUpdateRootFromMessage(message, doNotBatchUpdates);
4787
4862
  applyAndSendOps(unacknowledgedOps, doNotBatchUpdates);
4788
- if (_getInitialStateResolver !== null) {
4789
- _getInitialStateResolver();
4790
- }
4863
+ _resolveStoragePromise == null ? void 0 : _resolveStoragePromise();
4791
4864
  notifyStorageStatus();
4792
4865
  eventHub.storageDidLoad.notify();
4793
4866
  break;
@@ -4834,7 +4907,7 @@ ${Array.from(traces).join("\n\n")}`
4834
4907
  notify(updates, doNotBatchUpdates);
4835
4908
  });
4836
4909
  }
4837
- function tryFlushing() {
4910
+ function flushNowOrSoon() {
4838
4911
  const storageOps = context.buffer.storageOperations;
4839
4912
  if (storageOps.length > 0) {
4840
4913
  for (const op of storageOps) {
@@ -4842,7 +4915,7 @@ ${Array.from(traces).join("\n\n")}`
4842
4915
  }
4843
4916
  notifyStorageStatus();
4844
4917
  }
4845
- if (managedSocket.status !== "open") {
4918
+ if (managedSocket.getStatus() !== "connected") {
4846
4919
  context.buffer.storageOperations = [];
4847
4920
  return;
4848
4921
  }
@@ -4864,7 +4937,7 @@ ${Array.from(traces).join("\n\n")}`
4864
4937
  } else {
4865
4938
  clearTimeout(context.buffer.flushTimerID);
4866
4939
  context.buffer.flushTimerID = setTimeout(
4867
- tryFlushing,
4940
+ flushNowOrSoon,
4868
4941
  config.throttleDelay - elapsedMillis
4869
4942
  );
4870
4943
  }
@@ -4900,31 +4973,39 @@ ${Array.from(traces).join("\n\n")}`
4900
4973
  function broadcastEvent(event, options2 = {
4901
4974
  shouldQueueEventIfNotReady: false
4902
4975
  }) {
4903
- if (managedSocket.status !== "open" && !options2.shouldQueueEventIfNotReady) {
4976
+ if (managedSocket.getStatus() !== "connected" && !options2.shouldQueueEventIfNotReady) {
4904
4977
  return;
4905
4978
  }
4906
4979
  context.buffer.messages.push({
4907
4980
  type: 103 /* BROADCAST_EVENT */,
4908
4981
  event
4909
4982
  });
4910
- tryFlushing();
4983
+ flushNowOrSoon();
4911
4984
  }
4912
4985
  function dispatchOps(ops) {
4913
4986
  context.buffer.storageOperations.push(...ops);
4914
- tryFlushing();
4987
+ flushNowOrSoon();
4988
+ }
4989
+ let _getStorage$ = null;
4990
+ let _resolveStoragePromise = null;
4991
+ function refreshStorage(options2) {
4992
+ const messages = context.buffer.messages;
4993
+ if (!messages.some((msg) => msg.type === 200 /* FETCH_STORAGE */)) {
4994
+ messages.push({ type: 200 /* FETCH_STORAGE */ });
4995
+ }
4996
+ if (options2.flush) {
4997
+ flushNowOrSoon();
4998
+ }
4915
4999
  }
4916
- let _getInitialStatePromise = null;
4917
- let _getInitialStateResolver = null;
4918
5000
  function startLoadingStorage() {
4919
- if (_getInitialStatePromise === null) {
4920
- context.buffer.messages.push({ type: 200 /* FETCH_STORAGE */ });
4921
- tryFlushing();
4922
- _getInitialStatePromise = new Promise(
4923
- (resolve) => _getInitialStateResolver = resolve
4924
- );
5001
+ if (_getStorage$ === null) {
5002
+ refreshStorage({ flush: true });
5003
+ _getStorage$ = new Promise((resolve) => {
5004
+ _resolveStoragePromise = resolve;
5005
+ });
4925
5006
  notifyStorageStatus();
4926
5007
  }
4927
- return _getInitialStatePromise;
5008
+ return _getStorage$;
4928
5009
  }
4929
5010
  function getStorageSnapshot() {
4930
5011
  const root = context.root;
@@ -4937,7 +5018,7 @@ ${Array.from(traces).join("\n\n")}`
4937
5018
  }
4938
5019
  function getStorage() {
4939
5020
  return __async(this, null, function* () {
4940
- if (context.root) {
5021
+ if (context.root !== void 0) {
4941
5022
  return Promise.resolve({
4942
5023
  root: context.root
4943
5024
  });
@@ -4968,7 +5049,7 @@ ${Array.from(traces).join("\n\n")}`
4968
5049
  context.buffer.storageOperations.push(op);
4969
5050
  }
4970
5051
  }
4971
- tryFlushing();
5052
+ flushNowOrSoon();
4972
5053
  }
4973
5054
  function redo() {
4974
5055
  if (context.activeBatch) {
@@ -4990,7 +5071,7 @@ ${Array.from(traces).join("\n\n")}`
4990
5071
  context.buffer.storageOperations.push(op);
4991
5072
  }
4992
5073
  }
4993
- tryFlushing();
5074
+ flushNowOrSoon();
4994
5075
  }
4995
5076
  function batch(callback) {
4996
5077
  if (context.activeBatch) {
@@ -5022,7 +5103,7 @@ ${Array.from(traces).join("\n\n")}`
5022
5103
  dispatchOps(currentBatch.ops);
5023
5104
  }
5024
5105
  notify(currentBatch.updates, doNotBatchUpdates);
5025
- tryFlushing();
5106
+ flushNowOrSoon();
5026
5107
  }
5027
5108
  });
5028
5109
  return returnValue;
@@ -5038,13 +5119,11 @@ ${Array.from(traces).join("\n\n")}`
5038
5119
  }
5039
5120
  }
5040
5121
  function getStorageStatus() {
5041
- if (_getInitialStatePromise === null) {
5042
- return "not-loaded";
5043
- }
5044
5122
  if (context.root === void 0) {
5045
- return "loading";
5123
+ return _getStorage$ === null ? "not-loaded" : "loading";
5124
+ } else {
5125
+ return context.unacknowledgedOps.size === 0 ? "synchronized" : "synchronizing";
5046
5126
  }
5047
- return context.unacknowledgedOps.size === 0 ? "synchronized" : "synchronizing";
5048
5127
  }
5049
5128
  let _lastStorageStatus = getStorageStatus();
5050
5129
  function notifyStorageStatus() {
@@ -5059,11 +5138,15 @@ ${Array.from(traces).join("\n\n")}`
5059
5138
  (others) => others.map((other, index) => userToTreeNode(`Other ${index}`, other))
5060
5139
  );
5061
5140
  const events = {
5141
+ connection: eventHub.connection.observable,
5142
+ // Old/deprecated API
5143
+ status: eventHub.status.observable,
5144
+ // New/recommended API
5145
+ lostConnection: eventHub.lostConnection.observable,
5062
5146
  customEvent: eventHub.customEvent.observable,
5063
5147
  others: eventHub.others.observable,
5064
5148
  me: eventHub.me.observable,
5065
5149
  error: eventHub.error.observable,
5066
- connection: eventHub.connection.observable,
5067
5150
  storage: eventHub.storage.observable,
5068
5151
  history: eventHub.history.observable,
5069
5152
  storageDidLoad: eventHub.storageDidLoad.observable,
@@ -5091,7 +5174,7 @@ ${Array.from(traces).join("\n\n")}`
5091
5174
  send: {
5092
5175
  // These exist only for our E2E testing app
5093
5176
  explicitClose: (event) => managedSocket._privateSendMachineEvent({ type: "EXPLICIT_SOCKET_CLOSE", event }),
5094
- implicitClose: () => managedSocket._privateSendMachineEvent({ type: "PONG_TIMEOUT" })
5177
+ implicitClose: () => managedSocket._privateSendMachineEvent({ type: "NAVIGATOR_OFFLINE" })
5095
5178
  }
5096
5179
  },
5097
5180
  id: config.roomId,
@@ -5118,8 +5201,9 @@ ${Array.from(traces).join("\n\n")}`
5118
5201
  getStorageStatus,
5119
5202
  events,
5120
5203
  // Core
5121
- getConnectionState: () => context.connection.current.status,
5122
- isSelfAware: () => isConnectionSelfAware(context.connection.current),
5204
+ getStatus: () => managedSocket.getStatus(),
5205
+ getConnectionState: () => managedSocket.getLegacyStatus(),
5206
+ isSelfAware: () => context.sessionInfo.current !== null,
5123
5207
  getSelf: () => self.current,
5124
5208
  // Presence
5125
5209
  getPresence: () => context.me.current,
@@ -5171,6 +5255,12 @@ function makeClassicSubscribeFn(events) {
5171
5255
  return events.connection.subscribe(
5172
5256
  callback
5173
5257
  );
5258
+ case "status":
5259
+ return events.status.subscribe(callback);
5260
+ case "lost-connection":
5261
+ return events.lostConnection.subscribe(
5262
+ callback
5263
+ );
5174
5264
  case "history":
5175
5265
  return events.history.subscribe(callback);
5176
5266
  case "storage-status":
@@ -5204,7 +5294,7 @@ function makeClassicSubscribeFn(events) {
5204
5294
  return subscribe;
5205
5295
  }
5206
5296
  function isRoomEventName(value) {
5207
- return value === "my-presence" || value === "others" || value === "event" || value === "error" || value === "connection" || value === "history" || value === "storage-status";
5297
+ return value === "my-presence" || value === "others" || value === "event" || value === "error" || value === "history" || value === "status" || value === "storage-status" || value === "lost-connection" || value === "connection";
5208
5298
  }
5209
5299
  function makeCreateSocketDelegateForRoom(liveblocksServer, WebSocketPolyfill) {
5210
5300
  return (richToken) => {
@@ -5221,7 +5311,7 @@ function makeCreateSocketDelegateForRoom(liveblocksServer, WebSocketPolyfill) {
5221
5311
  // @ts-ignore (__PACKAGE_VERSION__ will be injected by the build script)
5222
5312
  true ? (
5223
5313
  /* istanbul ignore next */
5224
- "1.1.0-test1"
5314
+ "1.1.0"
5225
5315
  ) : "dev"}`
5226
5316
  );
5227
5317
  };
@@ -5324,20 +5414,28 @@ function fetchAuthEndpoint(fetch2, endpoint, body) {
5324
5414
  var MIN_THROTTLE = 16;
5325
5415
  var MAX_THROTTLE = 1e3;
5326
5416
  var DEFAULT_THROTTLE = 100;
5417
+ var MIN_LOST_CONNECTION_TIMEOUT = 200;
5418
+ var RECOMMENDED_MIN_LOST_CONNECTION_TIMEOUT = 1e3;
5419
+ var MAX_LOST_CONNECTION_TIMEOUT = 3e4;
5420
+ var DEFAULT_LOST_CONNECTION_TIMEOUT = 5e3;
5327
5421
  function getServerFromClientOptions(clientOptions) {
5328
5422
  const rawOptions = clientOptions;
5329
5423
  return typeof rawOptions.liveblocksServer === "string" ? rawOptions.liveblocksServer : "wss://api.liveblocks.io/v6";
5330
5424
  }
5331
5425
  function createClient(options) {
5426
+ var _a, _b;
5332
5427
  const clientOptions = options;
5333
- const throttleDelay = getThrottleDelayFromOptions(clientOptions);
5428
+ const throttleDelay = getThrottle((_a = clientOptions.throttle) != null ? _a : DEFAULT_THROTTLE);
5429
+ const lostConnectionTimeout = getLostConnectionTimeout(
5430
+ (_b = clientOptions.lostConnectionTimeout) != null ? _b : DEFAULT_LOST_CONNECTION_TIMEOUT
5431
+ );
5334
5432
  const rooms = /* @__PURE__ */ new Map();
5335
5433
  function getRoom(roomId) {
5336
5434
  const room = rooms.get(roomId);
5337
5435
  return room ? room : null;
5338
5436
  }
5339
5437
  function enter(roomId, options2) {
5340
- var _a, _b, _c;
5438
+ var _a2, _b2, _c;
5341
5439
  const existingRoom = rooms.get(roomId);
5342
5440
  if (existingRoom !== void 0) {
5343
5441
  return existingRoom;
@@ -5348,12 +5446,13 @@ function createClient(options) {
5348
5446
  );
5349
5447
  const newRoom = createRoom(
5350
5448
  {
5351
- initialPresence: (_a = options2.initialPresence) != null ? _a : {},
5449
+ initialPresence: (_a2 = options2.initialPresence) != null ? _a2 : {},
5352
5450
  initialStorage: options2.initialStorage
5353
5451
  },
5354
5452
  {
5355
5453
  roomId,
5356
5454
  throttleDelay,
5455
+ lostConnectionTimeout,
5357
5456
  polyfills: clientOptions.polyfills,
5358
5457
  delegates: clientOptions.mockedDelegates,
5359
5458
  enableDebugLogging: clientOptions.enableDebugLogging,
@@ -5370,7 +5469,7 @@ function createClient(options) {
5370
5469
  rooms.set(roomId, newRoom);
5371
5470
  setupDevTools(() => Array.from(rooms.keys()));
5372
5471
  linkDevTools(roomId, newRoom);
5373
- const shouldConnect = (_b = options2.shouldInitiallyConnect) != null ? _b : true;
5472
+ const shouldConnect = (_b2 = options2.shouldInitiallyConnect) != null ? _b2 : true;
5374
5473
  if (shouldConnect) {
5375
5474
  if (typeof atob === "undefined") {
5376
5475
  if (((_c = clientOptions.polyfills) == null ? void 0 : _c.atob) === void 0) {
@@ -5398,16 +5497,25 @@ function createClient(options) {
5398
5497
  leave
5399
5498
  };
5400
5499
  }
5401
- function getThrottleDelayFromOptions(options) {
5402
- if (options.throttle === void 0) {
5403
- return DEFAULT_THROTTLE;
5404
- }
5405
- if (typeof options.throttle !== "number" || options.throttle < MIN_THROTTLE || options.throttle > MAX_THROTTLE) {
5500
+ function checkBounds(option, value, min, max, recommendedMin) {
5501
+ if (typeof value !== "number" || value < min || value > max) {
5406
5502
  throw new Error(
5407
- `throttle should be a number between ${MIN_THROTTLE} and ${MAX_THROTTLE}.`
5503
+ `${option} should be a number between ${recommendedMin != null ? recommendedMin : min} and ${max}.`
5408
5504
  );
5409
5505
  }
5410
- return options.throttle;
5506
+ return value;
5507
+ }
5508
+ function getThrottle(value) {
5509
+ return checkBounds("throttle", value, MIN_THROTTLE, MAX_THROTTLE);
5510
+ }
5511
+ function getLostConnectionTimeout(value) {
5512
+ return checkBounds(
5513
+ "lostConnectionTimeout",
5514
+ value,
5515
+ MIN_LOST_CONNECTION_TIMEOUT,
5516
+ MAX_LOST_CONNECTION_TIMEOUT,
5517
+ RECOMMENDED_MIN_LOST_CONNECTION_TIMEOUT
5518
+ );
5411
5519
  }
5412
5520
  function prepareAuthentication(clientOptions, roomId) {
5413
5521
  const { publicApiKey, authEndpoint } = clientOptions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liveblocks/core",
3
- "version": "1.1.0-test1",
3
+ "version": "1.1.0",
4
4
  "description": "Shared code and foundational internals for Liveblocks",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",