@liveblocks/core 1.0.8 → 1.1.0-fsm1

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 +66 -59
  2. package/dist/index.js +949 -362
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -117,7 +117,7 @@ var onMessageFromPanel = eventSource.observable;
117
117
  // src/devtools/index.ts
118
118
  var VERSION = true ? (
119
119
  /* istanbul ignore next */
120
- "1.0.8"
120
+ "1.1.0-fsm1"
121
121
  ) : "dev";
122
122
  var _devtoolsSetupHasRun = false;
123
123
  function setupDevTools(getAllRooms) {
@@ -345,6 +345,773 @@ function nn(value, errmsg = "Expected value to be non-nullable") {
345
345
  return value;
346
346
  }
347
347
 
348
+ // src/lib/fsm.ts
349
+ function distance(state1, state2) {
350
+ if (state1 === state2) {
351
+ return [0, 0];
352
+ }
353
+ const chunks1 = state1.split(".");
354
+ const chunks2 = state2.split(".");
355
+ const minLen = Math.min(chunks1.length, chunks2.length);
356
+ let shared = 0;
357
+ for (; shared < minLen; shared++) {
358
+ if (chunks1[shared] !== chunks2[shared]) {
359
+ break;
360
+ }
361
+ }
362
+ const up = chunks1.length - shared;
363
+ const down = chunks2.length - shared;
364
+ return [up, down];
365
+ }
366
+ function patterns(targetState, levels) {
367
+ const parts = targetState.split(".");
368
+ if (levels < 1 || levels > parts.length + 1) {
369
+ throw new Error("Invalid number of levels");
370
+ }
371
+ const result = [];
372
+ if (levels > parts.length) {
373
+ result.push("*");
374
+ }
375
+ for (let i = parts.length - levels + 1; i < parts.length; i++) {
376
+ const slice = parts.slice(0, i);
377
+ if (slice.length > 0) {
378
+ result.push(slice.join(".") + ".*");
379
+ }
380
+ }
381
+ result.push(targetState);
382
+ return result;
383
+ }
384
+ var nextId = 1;
385
+ var FSM = class {
386
+ /**
387
+ * Returns the initial state, which is defined by the first call made to
388
+ * .addState().
389
+ */
390
+ get initialState() {
391
+ const result = this.states.values()[Symbol.iterator]().next();
392
+ if (result.done) {
393
+ throw new Error("No states defined yet");
394
+ } else {
395
+ return result.value;
396
+ }
397
+ }
398
+ get currentState() {
399
+ if (this.currentStateOrNull === null) {
400
+ throw new Error("Not started yet");
401
+ }
402
+ return this.currentStateOrNull;
403
+ }
404
+ /**
405
+ * Starts the machine by entering the initial state.
406
+ */
407
+ start() {
408
+ if (this.runningState !== 0 /* NOT_STARTED_YET */) {
409
+ throw new Error("State machine has already started");
410
+ }
411
+ this.runningState = 1 /* STARTED */;
412
+ this.currentStateOrNull = this.initialState;
413
+ this.enter(null);
414
+ return this;
415
+ }
416
+ /**
417
+ * Stops the state machine. Stopping the state machine will call exit
418
+ * handlers for the current state, but not enter a new state.
419
+ */
420
+ stop() {
421
+ if (this.runningState !== 1 /* STARTED */) {
422
+ throw new Error("Cannot stop a state machine that isn't started yet");
423
+ }
424
+ this.runningState = 2 /* STOPPED */;
425
+ this.exit(null);
426
+ this.currentStateOrNull = null;
427
+ }
428
+ constructor(initialContext) {
429
+ this.id = nextId++;
430
+ this.runningState = 0 /* NOT_STARTED_YET */;
431
+ this.currentStateOrNull = null;
432
+ this.states = /* @__PURE__ */ new Set();
433
+ this.enterFns = /* @__PURE__ */ new Map();
434
+ this.cleanupStack = [];
435
+ this.knownEventTypes = /* @__PURE__ */ new Set();
436
+ this.allowedTransitions = /* @__PURE__ */ new Map();
437
+ this.currentContext = Object.assign({}, initialContext);
438
+ this.eventHub = {
439
+ didReceiveEvent: makeEventSource(),
440
+ willTransition: makeEventSource(),
441
+ didPatchContext: makeEventSource(),
442
+ didIgnoreEvent: makeEventSource(),
443
+ willExitState: makeEventSource(),
444
+ didEnterState: makeEventSource()
445
+ };
446
+ this.events = {
447
+ didReceiveEvent: this.eventHub.didReceiveEvent.observable,
448
+ willTransition: this.eventHub.willTransition.observable,
449
+ didPatchContext: this.eventHub.didPatchContext.observable,
450
+ didIgnoreEvent: this.eventHub.didIgnoreEvent.observable,
451
+ willExitState: this.eventHub.willExitState.observable,
452
+ didEnterState: this.eventHub.didEnterState.observable
453
+ };
454
+ }
455
+ get context() {
456
+ return this.currentContext;
457
+ }
458
+ /**
459
+ * Define an explicit finite state in the state machine.
460
+ */
461
+ addState(state) {
462
+ if (this.runningState !== 0 /* NOT_STARTED_YET */) {
463
+ throw new Error("Already started");
464
+ }
465
+ this.states.add(state);
466
+ return this;
467
+ }
468
+ onEnter(nameOrPattern, enterFn) {
469
+ if (this.runningState !== 0 /* NOT_STARTED_YET */) {
470
+ throw new Error("Already started");
471
+ } else if (this.enterFns.has(nameOrPattern)) {
472
+ throw new Error(
473
+ // TODO We _currently_ don't support multiple .onEnters() for the same
474
+ // state, but this is not a fundamental limitation. Just not
475
+ // implemented yet. If we wanted to, we could make this an array.
476
+ `enter/exit function for ${nameOrPattern} already exists`
477
+ );
478
+ }
479
+ this.enterFns.set(nameOrPattern, enterFn);
480
+ return this;
481
+ }
482
+ onEnterAsync(nameOrPattern, promiseFn, onOK, onError) {
483
+ return this.onEnter(nameOrPattern, () => {
484
+ let cancelled = false;
485
+ void promiseFn(this.currentContext).then(
486
+ // On OK
487
+ (data) => {
488
+ if (!cancelled) {
489
+ this.transition({ type: "ASYNC_OK", data }, onOK);
490
+ }
491
+ },
492
+ // On Error
493
+ (reason) => {
494
+ if (!cancelled) {
495
+ this.transition({ type: "ASYNC_ERROR", reason }, onError);
496
+ }
497
+ }
498
+ );
499
+ return () => {
500
+ cancelled = true;
501
+ };
502
+ });
503
+ }
504
+ getStatesMatching(nameOrPattern) {
505
+ const matches = [];
506
+ if (nameOrPattern === "*") {
507
+ for (const state of this.states) {
508
+ matches.push(state);
509
+ }
510
+ } else if (nameOrPattern.endsWith(".*")) {
511
+ const prefix = nameOrPattern.slice(0, -1);
512
+ for (const state of this.states) {
513
+ if (state.startsWith(prefix)) {
514
+ matches.push(state);
515
+ }
516
+ }
517
+ } else {
518
+ const name = nameOrPattern;
519
+ if (this.states.has(name)) {
520
+ matches.push(name);
521
+ }
522
+ }
523
+ if (matches.length === 0) {
524
+ throw new Error(`No states match ${JSON.stringify(nameOrPattern)}`);
525
+ }
526
+ return matches;
527
+ }
528
+ /**
529
+ * Define all allowed outgoing transitions for a state.
530
+ *
531
+ * The targets for each event can be defined as a function which returns the
532
+ * next state to transition to. These functions can look at the `event` or
533
+ * `context` params to conditionally decide which next state to transition
534
+ * to.
535
+ *
536
+ * If you set it to `null`, then the transition will be explicitly forbidden
537
+ * and throw an error. If you don't define a target for a transition, then
538
+ * such events will get ignored.
539
+ */
540
+ addTransitions(nameOrPattern, mapping) {
541
+ if (this.runningState !== 0 /* NOT_STARTED_YET */) {
542
+ throw new Error("Already started");
543
+ }
544
+ for (const srcState of this.getStatesMatching(nameOrPattern)) {
545
+ let map = this.allowedTransitions.get(srcState);
546
+ if (map === void 0) {
547
+ map = /* @__PURE__ */ new Map();
548
+ this.allowedTransitions.set(srcState, map);
549
+ }
550
+ for (const [type, targetConfig_] of Object.entries(mapping)) {
551
+ const targetConfig = targetConfig_;
552
+ this.knownEventTypes.add(type);
553
+ if (targetConfig !== void 0 && targetConfig !== null) {
554
+ const targetFn = typeof targetConfig === "function" ? targetConfig : () => targetConfig;
555
+ map.set(type, targetFn);
556
+ }
557
+ }
558
+ }
559
+ return this;
560
+ }
561
+ /**
562
+ * Like `.addTransition()`, but takes an (anonymous) transition whenever the
563
+ * timer fires.
564
+ *
565
+ * @param stateOrPattern The state name, or state group pattern name.
566
+ * @param after Number of milliseconds after which to take the
567
+ * transition. If in the mean time, another transition
568
+ * is taken, the timer will get cancelled.
569
+ * @param target The target state to go to.
570
+ */
571
+ addTimedTransition(stateOrPattern, after2, target) {
572
+ return this.onEnter(stateOrPattern, () => {
573
+ const ms = typeof after2 === "function" ? after2(this.currentContext) : after2;
574
+ const timeoutID = setTimeout(() => {
575
+ this.transition({ type: "TIMER" }, target);
576
+ }, ms);
577
+ return () => {
578
+ clearTimeout(timeoutID);
579
+ };
580
+ });
581
+ }
582
+ getTargetFn(eventName) {
583
+ var _a;
584
+ return (_a = this.allowedTransitions.get(this.currentState)) == null ? void 0 : _a.get(eventName);
585
+ }
586
+ /**
587
+ * Exits the current state, and executes any necessary cleanup functions.
588
+ * Call this before changing the current state to the next state.
589
+ *
590
+ * @param levels Defines how many "levels" of nesting will be exited. For
591
+ * example, if you transition from `foo.bar.qux` to `foo.bar.baz`, then
592
+ * the level is 1. But if you transition from `foo.bar.qux` to `bla.bla`,
593
+ * then the level is 3.
594
+ */
595
+ exit(levels) {
596
+ var _a;
597
+ this.eventHub.willExitState.notify(this.currentState);
598
+ levels = levels != null ? levels : this.cleanupStack.length;
599
+ for (let i = 0; i < levels; i++) {
600
+ (_a = this.cleanupStack.pop()) == null ? void 0 : _a();
601
+ }
602
+ }
603
+ /**
604
+ * Enters the current state, and executes any necessary onEnter handlers.
605
+ * Call this directly _after_ setting the current state to the next state.
606
+ */
607
+ enter(levels) {
608
+ const enterPatterns = patterns(
609
+ this.currentState,
610
+ levels != null ? levels : this.currentState.split(".").length + 1
611
+ );
612
+ for (const pattern of enterPatterns) {
613
+ const enterFn = this.enterFns.get(pattern);
614
+ const cleanupFn = enterFn == null ? void 0 : enterFn(this.currentContext);
615
+ if (typeof cleanupFn === "function") {
616
+ this.cleanupStack.push(cleanupFn);
617
+ } else {
618
+ this.cleanupStack.push(null);
619
+ }
620
+ }
621
+ this.eventHub.didEnterState.notify(this.currentState);
622
+ }
623
+ /**
624
+ * Sends an event to the machine, which may cause an internal state
625
+ * transition to happen. When that happens, will trigger side effects.
626
+ */
627
+ send(event) {
628
+ const targetFn = this.getTargetFn(event.type);
629
+ if (targetFn !== void 0) {
630
+ return this.transition(event, targetFn);
631
+ }
632
+ if (!this.knownEventTypes.has(event.type)) {
633
+ throw new Error(`Invalid event ${JSON.stringify(event.type)}`);
634
+ } else {
635
+ this.eventHub.didIgnoreEvent.notify(event);
636
+ }
637
+ }
638
+ transition(event, target) {
639
+ this.eventHub.didReceiveEvent.notify(event);
640
+ const oldState = this.currentState;
641
+ const targetFn = typeof target === "function" ? target : () => target;
642
+ const nextTarget = targetFn(event, this.currentContext);
643
+ let nextState;
644
+ let assign = void 0;
645
+ let effect = void 0;
646
+ if (nextTarget === null) {
647
+ this.eventHub.didIgnoreEvent.notify(event);
648
+ return;
649
+ }
650
+ if (typeof nextTarget === "string") {
651
+ nextState = nextTarget;
652
+ } else {
653
+ nextState = nextTarget.target;
654
+ assign = nextTarget.assign;
655
+ effect = nextTarget.effect;
656
+ }
657
+ if (!this.states.has(nextState)) {
658
+ throw new Error(`Invalid next state name: ${JSON.stringify(nextState)}`);
659
+ }
660
+ this.eventHub.willTransition.notify({ from: oldState, to: nextState });
661
+ const [up, down] = distance(this.currentState, nextState);
662
+ if (up > 0) {
663
+ this.exit(up);
664
+ }
665
+ this.currentStateOrNull = nextState;
666
+ if (assign !== void 0) {
667
+ const patch = typeof assign === "function" ? assign(this.context, event) : assign;
668
+ this.currentContext = Object.assign({}, this.currentContext, patch);
669
+ this.eventHub.didPatchContext.notify(patch);
670
+ }
671
+ if (effect !== void 0) {
672
+ effect(this.context, event);
673
+ }
674
+ if (down > 0) {
675
+ this.enter(down);
676
+ }
677
+ }
678
+ };
679
+
680
+ // src/connection.ts
681
+ function toPublicConnectionStatus(state) {
682
+ switch (state) {
683
+ case "@ok.connected":
684
+ case "@ok.awaiting-pong":
685
+ return "open";
686
+ case "@idle.initial":
687
+ return "closed";
688
+ case "@auth.busy":
689
+ case "@auth.backoff":
690
+ return "authenticating";
691
+ case "@connecting.busy":
692
+ return "connecting";
693
+ case "@connecting.backoff":
694
+ return "unavailable";
695
+ case "@idle.failed":
696
+ return "failed";
697
+ default:
698
+ return assertNever(state, "Unknown state");
699
+ }
700
+ }
701
+ var BACKOFF_DELAYS = [250, 500, 1e3, 2e3, 4e3, 8e3, 1e4];
702
+ var LOW_DELAY = BACKOFF_DELAYS[0];
703
+ var BACKOFF_DELAYS_SLOW = [2e3, 3e4, 6e4, 3e5];
704
+ var HEARTBEAT_INTERVAL = 3e4;
705
+ var PONG_TIMEOUT = 2e3;
706
+ var AUTH_TIMEOUT = 1e4;
707
+ var SOCKET_CONNECT_TIMEOUT = 1e4;
708
+ var UnauthorizedError = class extends Error {
709
+ };
710
+ var LiveblocksError = class extends Error {
711
+ constructor(message, code) {
712
+ super(message);
713
+ this.code = code;
714
+ }
715
+ };
716
+ function nextBackoffDelay(currentDelay, delays = BACKOFF_DELAYS) {
717
+ var _a;
718
+ return (_a = delays.find((delay) => delay > currentDelay)) != null ? _a : delays[delays.length - 1];
719
+ }
720
+ function increaseBackoffDelay(context) {
721
+ return { backoffDelay: nextBackoffDelay(context.backoffDelay) };
722
+ }
723
+ function increaseBackoffDelayAggressively(context) {
724
+ return {
725
+ backoffDelay: nextBackoffDelay(context.backoffDelay, BACKOFF_DELAYS_SLOW)
726
+ };
727
+ }
728
+ function timeoutAfter(millis) {
729
+ return new Promise((_, reject) => {
730
+ setTimeout(() => {
731
+ reject(new Error("Timed out"));
732
+ }, millis);
733
+ });
734
+ }
735
+ function sendHeartbeat(ctx) {
736
+ var _a;
737
+ if (!ctx.socket) {
738
+ error("This should never happen");
739
+ }
740
+ (_a = ctx.socket) == null ? void 0 : _a.send("ping");
741
+ }
742
+ function enableTracing(fsm) {
743
+ const start = (/* @__PURE__ */ new Date()).getTime();
744
+ function log(...args) {
745
+ warn(
746
+ `${(((/* @__PURE__ */ new Date()).getTime() - start) / 1e3).toFixed(2)} [FSM #${fsm.id}]`,
747
+ ...args
748
+ );
749
+ }
750
+ const unsubs = [
751
+ fsm.events.didReceiveEvent.subscribe((e) => {
752
+ log(`Event ${e.type}`);
753
+ }),
754
+ fsm.events.willTransition.subscribe(({ from, to }) => {
755
+ log("Transitioning", from, "\u2192", to);
756
+ }),
757
+ fsm.events.didPatchContext.subscribe((patch) => {
758
+ log(`Patched: ${JSON.stringify(patch)}`);
759
+ }),
760
+ fsm.events.didIgnoreEvent.subscribe((e) => {
761
+ log("Ignored event", e, "(current state won't handle it)");
762
+ })
763
+ // fsm.events.willExitState.subscribe((s) => {
764
+ // log("Exiting state", s);
765
+ // }),
766
+ // fsm.events.didEnterState.subscribe((s) => {
767
+ // log("Entering state", s);
768
+ // }),
769
+ ];
770
+ return () => {
771
+ for (const unsub of unsubs) {
772
+ unsub();
773
+ }
774
+ };
775
+ }
776
+ function defineConnectivityEvents(fsm) {
777
+ const statusDidChange = makeEventSource();
778
+ const didConnect = makeEventSource();
779
+ const didDisconnect = makeEventSource();
780
+ let oldPublicStatus = null;
781
+ fsm.events.didEnterState.subscribe((newState) => {
782
+ const newPublicStatus = toPublicConnectionStatus(newState);
783
+ statusDidChange.notify(newPublicStatus);
784
+ if (oldPublicStatus === "open" && newPublicStatus !== "open") {
785
+ didDisconnect.notify();
786
+ } else if (oldPublicStatus !== "open" && newPublicStatus === "open") {
787
+ didConnect.notify();
788
+ }
789
+ oldPublicStatus = newPublicStatus;
790
+ });
791
+ return {
792
+ statusDidChange: statusDidChange.observable,
793
+ didConnect: didConnect.observable,
794
+ didDisconnect: didDisconnect.observable
795
+ };
796
+ }
797
+ function createStateMachine(delegates) {
798
+ const onMessage = makeEventSource();
799
+ const onLiveblocksError = makeEventSource();
800
+ const initialContext = {
801
+ token: null,
802
+ socket: null,
803
+ // Bumped to the next "tier" every time a connection attempt fails (no matter
804
+ // whether this is for the authentication server or the websocket server).
805
+ // Reset every time a connection succeeded.
806
+ backoffDelay: LOW_DELAY
807
+ // numRetries: 0,
808
+ };
809
+ const fsm = new FSM(initialContext).addState("@idle.initial").addState("@idle.failed").addState("@auth.busy").addState("@auth.backoff").addState("@connecting.busy").addState("@connecting.backoff").addState("@ok.connected").addState("@ok.awaiting-pong");
810
+ fsm.addTransitions("*", {
811
+ RECONNECT: {
812
+ target: "@auth.backoff",
813
+ assign: increaseBackoffDelay
814
+ },
815
+ DISCONNECT: "@idle.initial"
816
+ });
817
+ fsm.addTransitions("@idle.*", {
818
+ CONNECT: (_, ctx) => (
819
+ // If we still have a known token, try to reconnect to the socket directly,
820
+ // otherwise, try to obtain a new token
821
+ ctx.token !== null ? "@connecting.busy" : "@auth.busy"
822
+ )
823
+ });
824
+ fsm.addTransitions("@auth.backoff", {
825
+ NAVIGATOR_ONLINE: {
826
+ target: "@auth.busy",
827
+ assign: { backoffDelay: LOW_DELAY }
828
+ }
829
+ }).addTimedTransition(
830
+ "@auth.backoff",
831
+ (ctx) => ctx.backoffDelay,
832
+ "@auth.busy"
833
+ ).onEnterAsync(
834
+ "@auth.busy",
835
+ () => Promise.race([delegates.authenticate(), timeoutAfter(AUTH_TIMEOUT)]),
836
+ // On successful authentication
837
+ (okEvent) => ({
838
+ target: "@connecting.busy",
839
+ assign: {
840
+ token: okEvent.data,
841
+ backoffDelay: LOW_DELAY
842
+ }
843
+ }),
844
+ // Auth failed
845
+ (failedEvent) => failedEvent.reason instanceof UnauthorizedError ? {
846
+ target: "@idle.failed",
847
+ effect: () => error(
848
+ `Unauthorized, will stop retrying: ${failedEvent.reason.message}`
849
+ )
850
+ } : {
851
+ target: "@auth.backoff",
852
+ assign: increaseBackoffDelay
853
+ // effect: () => {
854
+ // console.log(`Authentication failed: ${String(failedEvent.reason)}`);
855
+ // },
856
+ }
857
+ );
858
+ const onSocketError = (event) => fsm.send({ type: "EXPLICIT_SOCKET_ERROR", event });
859
+ const onSocketClose = (event) => fsm.send({ type: "EXPLICIT_SOCKET_CLOSE", event });
860
+ const onSocketMessage = (event) => event.data === "pong" ? fsm.send({ type: "PONG" }) : onMessage.notify(event);
861
+ function teardownSocket(socket) {
862
+ if (socket) {
863
+ socket.removeEventListener("error", onSocketError);
864
+ socket.removeEventListener("close", onSocketClose);
865
+ socket.removeEventListener("message", onSocketMessage);
866
+ socket.close();
867
+ }
868
+ }
869
+ fsm.addTransitions("@connecting.backoff", {
870
+ NAVIGATOR_ONLINE: {
871
+ target: "@connecting.busy",
872
+ assign: { backoffDelay: LOW_DELAY }
873
+ }
874
+ }).addTimedTransition(
875
+ "@connecting.backoff",
876
+ (ctx) => ctx.backoffDelay,
877
+ "@connecting.busy"
878
+ ).onEnterAsync(
879
+ "@connecting.busy",
880
+ (ctx) => {
881
+ if (ctx.socket) {
882
+ throw new Error(
883
+ "Oops! Old socket should already be cleaned up by the time this state is entered! You may have found an edge case. Please tell Vincent about this."
884
+ );
885
+ }
886
+ const promise = new Promise((resolve, reject) => {
887
+ if (ctx.token === null) {
888
+ throw new Error("No auth token");
889
+ }
890
+ const socket = delegates.createSocket(ctx.token);
891
+ socket.addEventListener("error", reject);
892
+ socket.addEventListener("close", reject);
893
+ socket.addEventListener("open", () => {
894
+ socket.removeEventListener("error", reject);
895
+ socket.removeEventListener("close", reject);
896
+ socket.addEventListener("error", onSocketError);
897
+ socket.addEventListener("close", onSocketClose);
898
+ socket.addEventListener("message", onSocketMessage);
899
+ resolve(socket);
900
+ });
901
+ });
902
+ return Promise.race([promise, timeoutAfter(SOCKET_CONNECT_TIMEOUT)]);
903
+ },
904
+ // On successful authentication
905
+ (okEvent) => ({
906
+ target: "@ok.connected",
907
+ assign: {
908
+ socket: okEvent.data,
909
+ backoffDelay: LOW_DELAY
910
+ }
911
+ }),
912
+ // On failure
913
+ (failedEvent) => (
914
+ // XXX TODO If _UNAUTHORIZED_, we should discard the token and jump back
915
+ // to @auth.busy to reattempt authentication
916
+ {
917
+ target: "@auth.backoff",
918
+ assign: (ctx) => {
919
+ if (ctx.socket) {
920
+ throw new Error(
921
+ "Oops! This is unexpected! You may have found an edge case. Please tell Vincent about this."
922
+ );
923
+ }
924
+ return {
925
+ // XXX If failed because of a "room full" or "rate limit", back off more aggressively here
926
+ backoffDelay: nextBackoffDelay(ctx.backoffDelay)
927
+ };
928
+ },
929
+ effect: () => {
930
+ error(
931
+ `Connection to WebSocket could not be established, reason: ${String(
932
+ failedEvent.reason
933
+ )}`
934
+ );
935
+ }
936
+ }
937
+ )
938
+ );
939
+ fsm.addTimedTransition("@ok.connected", HEARTBEAT_INTERVAL, {
940
+ target: "@ok.awaiting-pong",
941
+ effect: sendHeartbeat
942
+ }).addTransitions("@ok.connected", {
943
+ WINDOW_GOT_FOCUS: { target: "@ok.awaiting-pong", effect: sendHeartbeat }
944
+ });
945
+ const noPongAction = {
946
+ target: "@connecting.busy",
947
+ effect: () => {
948
+ warn(
949
+ "Received no pong from server, assume implicit connection loss."
950
+ );
951
+ }
952
+ };
953
+ fsm.onEnter("@ok.*", (ctx) => {
954
+ return () => {
955
+ teardownSocket(ctx.socket);
956
+ ctx.socket = null;
957
+ };
958
+ }).addTimedTransition("@ok.awaiting-pong", PONG_TIMEOUT, noPongAction).addTransitions("@ok.awaiting-pong", { PONG_TIMEOUT: noPongAction }).addTransitions("@ok.awaiting-pong", { PONG: "@ok.connected" }).addTransitions("@ok.*", {
959
+ // When a socket receives an error, this can cause the closing of the
960
+ // socket, or not. So always check to see if the socket is still OPEN or
961
+ // not. When still OPEN, don't transition.
962
+ EXPLICIT_SOCKET_ERROR: (_, context) => {
963
+ var _a;
964
+ if (((_a = context.socket) == null ? void 0 : _a.readyState) === WebSocket.OPEN) {
965
+ return null;
966
+ }
967
+ return {
968
+ target: "@connecting.busy",
969
+ assign: increaseBackoffDelay
970
+ };
971
+ },
972
+ EXPLICIT_SOCKET_CLOSE: (e) => {
973
+ if (e.event.code === 4999) {
974
+ return {
975
+ target: "@idle.failed",
976
+ effect: () => warn(
977
+ "Connection to WebSocket closed permanently. Won't retry."
978
+ )
979
+ };
980
+ }
981
+ if (e.event.code >= 4e3 && e.event.code <= 4100) {
982
+ return {
983
+ target: "@connecting.busy",
984
+ assign: increaseBackoffDelayAggressively,
985
+ effect: (_, { event }) => {
986
+ if (event.code >= 4e3 && event.code <= 4100) {
987
+ const err = new LiveblocksError(event.reason, event.code);
988
+ onLiveblocksError.notify(err);
989
+ }
990
+ }
991
+ };
992
+ }
993
+ return {
994
+ target: "@connecting.busy",
995
+ assign: increaseBackoffDelay
996
+ };
997
+ }
998
+ });
999
+ if (typeof document !== "undefined") {
1000
+ const doc = typeof document !== "undefined" ? document : void 0;
1001
+ const win = typeof window !== "undefined" ? window : void 0;
1002
+ const root = win != null ? win : doc;
1003
+ fsm.onEnter("*", (ctx) => {
1004
+ function onBackOnline() {
1005
+ fsm.send({ type: "NAVIGATOR_ONLINE" });
1006
+ }
1007
+ function onVisibilityChange() {
1008
+ if ((doc == null ? void 0 : doc.visibilityState) === "visible") {
1009
+ fsm.send({ type: "WINDOW_GOT_FOCUS" });
1010
+ }
1011
+ }
1012
+ win == null ? void 0 : win.addEventListener("online", onBackOnline);
1013
+ root == null ? void 0 : root.addEventListener("visibilitychange", onVisibilityChange);
1014
+ return () => {
1015
+ root == null ? void 0 : root.removeEventListener("visibilitychange", onVisibilityChange);
1016
+ win == null ? void 0 : win.removeEventListener("online", onBackOnline);
1017
+ teardownSocket(ctx.socket);
1018
+ };
1019
+ });
1020
+ }
1021
+ const { statusDidChange, didConnect, didDisconnect } = defineConnectivityEvents(fsm);
1022
+ const cleanup = enableTracing(fsm);
1023
+ fsm.start();
1024
+ return {
1025
+ fsm,
1026
+ cleanup,
1027
+ // Observable events that will be emitted by this machine
1028
+ events: {
1029
+ statusDidChange,
1030
+ didConnect,
1031
+ didDisconnect,
1032
+ onMessage: onMessage.observable,
1033
+ onLiveblocksError: onLiveblocksError.observable
1034
+ }
1035
+ };
1036
+ }
1037
+ var ManagedSocket = class {
1038
+ constructor(delegates) {
1039
+ const { fsm, events, cleanup } = createStateMachine(delegates);
1040
+ this.fsm = fsm;
1041
+ this.events = events;
1042
+ this.cleanup = cleanup;
1043
+ }
1044
+ get status() {
1045
+ try {
1046
+ return toPublicConnectionStatus(this.fsm.currentState);
1047
+ } catch (e) {
1048
+ return "closed";
1049
+ }
1050
+ }
1051
+ /**
1052
+ * Returns the current auth token.
1053
+ */
1054
+ get token() {
1055
+ const tok = this.fsm.context.token;
1056
+ if (tok === null) {
1057
+ throw new Error("Unexpected null token here");
1058
+ }
1059
+ return tok;
1060
+ }
1061
+ /**
1062
+ * Call this method to try to connect to a WebSocket. This only has an effect
1063
+ * if the machine is idle at the moment, otherwise this is a no-op.
1064
+ */
1065
+ connect() {
1066
+ this.fsm.send({ type: "CONNECT" });
1067
+ }
1068
+ /**
1069
+ * If idle, will try to connect. Otherwise, it will attempt to reconnect to
1070
+ * the socket, potentially obtaining a new token first, if needed.
1071
+ */
1072
+ reconnect() {
1073
+ this.fsm.send({ type: "RECONNECT" });
1074
+ }
1075
+ /**
1076
+ * Call this method to disconnect from the current WebSocket. Is going to be
1077
+ * a no-op if there is no active connection.
1078
+ */
1079
+ disconnect() {
1080
+ this.fsm.send({ type: "DISCONNECT" });
1081
+ }
1082
+ /**
1083
+ * Call this to stop the machine and run necessary cleanup functions. After
1084
+ * calling destroy(), you can no longer use this instance. Call this before
1085
+ * letting the instance get garbage collected.
1086
+ */
1087
+ destroy() {
1088
+ this.fsm.stop();
1089
+ this.cleanup();
1090
+ }
1091
+ /**
1092
+ * Safely send a message to the current WebSocket connection. Will emit a log
1093
+ * message if this is somehow impossible.
1094
+ */
1095
+ send(data) {
1096
+ var _a;
1097
+ const socket = (_a = this.fsm.context) == null ? void 0 : _a.socket;
1098
+ if (socket === null) {
1099
+ warn("Cannot send: not connected yet", data);
1100
+ } else if (socket.readyState !== WebSocket.OPEN) {
1101
+ warn("Cannot send: WebSocket no longer open", data);
1102
+ } else {
1103
+ socket.send(data);
1104
+ }
1105
+ }
1106
+ /**
1107
+ * NOTE: Used by the E2E app only, to simulate explicit events.
1108
+ * Not ideal to keep exposed :(
1109
+ */
1110
+ _privateSend(event) {
1111
+ return this.fsm.send(event);
1112
+ }
1113
+ };
1114
+
348
1115
  // src/lib/position.ts
349
1116
  var MIN_CODE = 32;
350
1117
  var MAX_CODE = 126;
@@ -2953,10 +3720,6 @@ function hasJwtMeta(data) {
2953
3720
  const { iat, exp } = data;
2954
3721
  return typeof iat === "number" && typeof exp === "number";
2955
3722
  }
2956
- function isTokenExpired(token) {
2957
- const now = Date.now() / 1e3;
2958
- return now > token.exp - 300 || now < token.iat + 300;
2959
- }
2960
3723
  function isStringList(value) {
2961
3724
  return Array.isArray(value) && value.every((i) => typeof i === "string");
2962
3725
  }
@@ -2983,20 +3746,22 @@ function parseJwtToken(token) {
2983
3746
  }
2984
3747
  function parseRoomAuthToken(tokenString) {
2985
3748
  const data = parseJwtToken(tokenString);
2986
- if (data && isRoomAuthToken(data)) {
2987
- const _a = data, {
2988
- maxConnections: _legacyField
2989
- } = _a, token = __objRest(_a, [
2990
- // If this legacy field is found on the token, pretend it wasn't there,
2991
- // to make all internally used token payloads uniform
2992
- "maxConnections"
2993
- ]);
2994
- return token;
2995
- } else {
3749
+ if (!(data && isRoomAuthToken(data))) {
2996
3750
  throw new Error(
2997
3751
  "Authentication error: we expected a room token but did not get one. Hint: if you are using a callback, ensure the room is passed when creating the token. For more information: https://liveblocks.io/docs/api-reference/liveblocks-client#createClientCallback"
2998
3752
  );
2999
3753
  }
3754
+ const _a = data, {
3755
+ maxConnections: _legacyField
3756
+ } = _a, parsedToken = __objRest(_a, [
3757
+ // If this legacy field is found on the token, pretend it wasn't there,
3758
+ // to make all internally used token payloads uniform
3759
+ "maxConnections"
3760
+ ]);
3761
+ return {
3762
+ raw: tokenString,
3763
+ parsed: parsedToken
3764
+ };
3000
3765
  }
3001
3766
 
3002
3767
  // src/protocol/ClientMsg.ts
@@ -3237,31 +4002,11 @@ var DerivedRef = class extends ImmutableRef {
3237
4002
  }
3238
4003
  };
3239
4004
 
3240
- // src/types/IWebSocket.ts
3241
- var WebsocketCloseCodes = /* @__PURE__ */ ((WebsocketCloseCodes2) => {
3242
- WebsocketCloseCodes2[WebsocketCloseCodes2["CLOSE_ABNORMAL"] = 1006] = "CLOSE_ABNORMAL";
3243
- WebsocketCloseCodes2[WebsocketCloseCodes2["INVALID_MESSAGE_FORMAT"] = 4e3] = "INVALID_MESSAGE_FORMAT";
3244
- WebsocketCloseCodes2[WebsocketCloseCodes2["NOT_ALLOWED"] = 4001] = "NOT_ALLOWED";
3245
- WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_MESSAGES_PER_SECONDS"] = 4002] = "MAX_NUMBER_OF_MESSAGES_PER_SECONDS";
3246
- WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_CONCURRENT_CONNECTIONS"] = 4003] = "MAX_NUMBER_OF_CONCURRENT_CONNECTIONS";
3247
- WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP"] = 4004] = "MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP";
3248
- WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_CONCURRENT_CONNECTIONS_PER_ROOM"] = 4005] = "MAX_NUMBER_OF_CONCURRENT_CONNECTIONS_PER_ROOM";
3249
- WebsocketCloseCodes2[WebsocketCloseCodes2["CLOSE_WITHOUT_RETRY"] = 4999] = "CLOSE_WITHOUT_RETRY";
3250
- return WebsocketCloseCodes2;
3251
- })(WebsocketCloseCodes || {});
3252
-
3253
4005
  // src/room.ts
3254
- var BACKOFF_RETRY_DELAYS = [250, 500, 1e3, 2e3, 4e3, 8e3, 1e4];
3255
- var BACKOFF_RETRY_DELAYS_SLOW = [2e3, 3e4, 6e4, 3e5];
3256
- var HEARTBEAT_INTERVAL = 3e4;
3257
- var PONG_TIMEOUT = 2e3;
3258
4006
  function makeIdFactory(connectionId) {
3259
4007
  let count = 0;
3260
4008
  return () => `${connectionId}:${count++}`;
3261
4009
  }
3262
- function log(..._params) {
3263
- return;
3264
- }
3265
4010
  function isConnectionSelfAware(connection) {
3266
4011
  return connection.status === "open" || connection.status === "connecting";
3267
4012
  }
@@ -3274,19 +4019,25 @@ function userToTreeNode(key, user) {
3274
4019
  };
3275
4020
  }
3276
4021
  function createRoom(options, config) {
3277
- var _a;
4022
+ var _a, _b, _c;
3278
4023
  const initialPresence = typeof options.initialPresence === "function" ? options.initialPresence(config.roomId) : options.initialPresence;
3279
4024
  const initialStorage = typeof options.initialStorage === "function" ? options.initialStorage(config.roomId) : options.initialStorage;
4025
+ const delegates = {
4026
+ authenticate: makeAuthDelegateForRoom(
4027
+ config.roomId,
4028
+ config.authentication,
4029
+ (_a = config.polyfills) == null ? void 0 : _a.fetch
4030
+ ),
4031
+ createSocket: makeCreateSocketDelegateForRoom(
4032
+ config.liveblocksServer,
4033
+ (_b = config.polyfills) == null ? void 0 : _b.WebSocket
4034
+ )
4035
+ };
4036
+ const managedSocket = new ManagedSocket(delegates);
3280
4037
  const context = {
3281
- token: null,
3282
4038
  lastConnectionId: null,
3283
- socket: null,
3284
- numRetries: 0,
3285
4039
  timers: {
3286
- flush: void 0,
3287
- reconnect: void 0,
3288
- heartbeat: void 0,
3289
- pongTimeout: void 0
4040
+ flush: void 0
3290
4041
  },
3291
4042
  buffer: {
3292
4043
  lastFlushedAt: 0,
@@ -3319,7 +4070,68 @@ function createRoom(options, config) {
3319
4070
  opStackTraces: process.env.NODE_ENV !== "production" ? /* @__PURE__ */ new Map() : void 0
3320
4071
  };
3321
4072
  const doNotBatchUpdates = (cb) => cb();
3322
- const batchUpdates = (_a = config.unstable_batchedUpdates) != null ? _a : doNotBatchUpdates;
4073
+ const batchUpdates = (_c = config.unstable_batchedUpdates) != null ? _c : doNotBatchUpdates;
4074
+ function onStatusDidChange(newStatus) {
4075
+ if (newStatus !== "open" && newStatus !== "connecting") {
4076
+ context.connection.set({ status: newStatus });
4077
+ } else {
4078
+ context.connection.set({
4079
+ status: newStatus,
4080
+ id: managedSocket.token.parsed.actor,
4081
+ userInfo: managedSocket.token.parsed.info,
4082
+ userId: managedSocket.token.parsed.id,
4083
+ isReadOnly: isStorageReadOnly(managedSocket.token.parsed.scopes)
4084
+ });
4085
+ }
4086
+ batchUpdates(() => {
4087
+ eventHub.connection.notify(newStatus);
4088
+ });
4089
+ }
4090
+ function onDidConnect() {
4091
+ const conn = context.connection.current;
4092
+ if (conn.status !== "open") {
4093
+ throw new Error("Unexpected non-open state here");
4094
+ }
4095
+ if (context.lastConnectionId !== void 0) {
4096
+ context.buffer.me = {
4097
+ type: "full",
4098
+ data: (
4099
+ // Because state.me.current is a readonly object, we'll have to
4100
+ // make a copy here. Otherwise, type errors happen later when
4101
+ // "patching" my presence.
4102
+ __spreadValues({}, context.me.current)
4103
+ )
4104
+ };
4105
+ tryFlushing();
4106
+ }
4107
+ context.lastConnectionId = conn.id;
4108
+ context.idFactory = makeIdFactory(conn.id);
4109
+ if (context.root) {
4110
+ context.buffer.messages.push({ type: 200 /* FETCH_STORAGE */ });
4111
+ }
4112
+ tryFlushing();
4113
+ }
4114
+ function onDidDisconnect() {
4115
+ clearTimeout(context.timers.flush);
4116
+ batchUpdates(() => {
4117
+ context.others.clearOthers();
4118
+ notify({ others: [{ type: "reset" }] }, doNotBatchUpdates);
4119
+ });
4120
+ }
4121
+ managedSocket.events.onMessage.subscribe(handleServerMessage);
4122
+ managedSocket.events.statusDidChange.subscribe(onStatusDidChange);
4123
+ managedSocket.events.didConnect.subscribe(onDidConnect);
4124
+ managedSocket.events.didDisconnect.subscribe(onDidDisconnect);
4125
+ managedSocket.events.onLiveblocksError.subscribe((err) => {
4126
+ batchUpdates(() => {
4127
+ if (process.env.NODE_ENV !== "production") {
4128
+ error(
4129
+ `Connection to websocket server closed. Reason: ${err.message} (code: ${err.code}).`
4130
+ );
4131
+ }
4132
+ eventHub.error.notify(err);
4133
+ });
4134
+ });
3323
4135
  const pool = {
3324
4136
  roomId: config.roomId,
3325
4137
  getNode: (id) => context.nodes.get(id),
@@ -3380,41 +4192,9 @@ function createRoom(options, config) {
3380
4192
  storageStatus: makeEventSource()
3381
4193
  };
3382
4194
  const effects = config.mockedEffects || {
3383
- authenticate(auth, createWebSocket) {
3384
- const prevToken = context.token;
3385
- if (prevToken !== null && !isTokenExpired(prevToken.parsed)) {
3386
- const socket = createWebSocket(prevToken.raw);
3387
- authenticationSuccess(prevToken.parsed, socket);
3388
- return void 0;
3389
- } else {
3390
- void auth(config.roomId).then(({ token }) => {
3391
- if (context.connection.current.status !== "authenticating") {
3392
- return;
3393
- }
3394
- const parsedToken = parseRoomAuthToken(token);
3395
- const socket = createWebSocket(token);
3396
- authenticationSuccess(parsedToken, socket);
3397
- context.token = { raw: token, parsed: parsedToken };
3398
- }).catch(
3399
- (er) => authenticationFailure(
3400
- er instanceof Error ? er : new Error(String(er))
3401
- )
3402
- );
3403
- return void 0;
3404
- }
3405
- },
3406
4195
  send(messageOrMessages) {
3407
- if (context.socket === null) {
3408
- throw new Error("Can't send message if socket is null");
3409
- }
3410
- if (context.socket.readyState === context.socket.OPEN) {
3411
- context.socket.send(JSON.stringify(messageOrMessages));
3412
- }
3413
- },
3414
- scheduleFlush: (delay) => setTimeout(tryFlushing, delay),
3415
- scheduleReconnect: (delay) => setTimeout(connect, delay),
3416
- startHeartbeatInterval: () => setInterval(heartbeat, HEARTBEAT_INTERVAL),
3417
- schedulePongTimeout: () => setTimeout(pongTimeout, PONG_TIMEOUT)
4196
+ managedSocket.send(JSON.stringify(messageOrMessages));
4197
+ }
3418
4198
  };
3419
4199
  const self = new DerivedRef(
3420
4200
  context.connection,
@@ -3621,22 +4401,6 @@ function createRoom(options, config) {
3621
4401
  }
3622
4402
  }
3623
4403
  }
3624
- function connect() {
3625
- var _a2, _b;
3626
- if (context.connection.current.status !== "closed" && context.connection.current.status !== "unavailable") {
3627
- return;
3628
- }
3629
- const auth = prepareAuthEndpoint(
3630
- config.authentication,
3631
- (_a2 = config.polyfills) == null ? void 0 : _a2.fetch
3632
- );
3633
- const createWebSocket = prepareCreateWebSocket(
3634
- config.liveblocksServer,
3635
- (_b = config.polyfills) == null ? void 0 : _b.WebSocket
3636
- );
3637
- updateConnection({ status: "authenticating" }, batchUpdates);
3638
- effects.authenticate(auth, createWebSocket);
3639
- }
3640
4404
  function updatePresence(patch, options2) {
3641
4405
  const oldValues = {};
3642
4406
  if (context.buffer.me === null) {
@@ -3678,40 +4442,6 @@ function createRoom(options, config) {
3678
4442
  function isStorageReadOnly(scopes) {
3679
4443
  return scopes.includes("room:read" /* Read */) && scopes.includes("room:presence:write" /* PresenceWrite */) && !scopes.includes("room:write" /* Write */);
3680
4444
  }
3681
- function authenticationSuccess(token, socket) {
3682
- socket.addEventListener("message", onMessage);
3683
- socket.addEventListener("open", onOpen);
3684
- socket.addEventListener("close", onClose);
3685
- socket.addEventListener("error", onError);
3686
- updateConnection(
3687
- {
3688
- status: "connecting",
3689
- id: token.actor,
3690
- userInfo: token.info,
3691
- userId: token.id,
3692
- isReadOnly: isStorageReadOnly(token.scopes)
3693
- },
3694
- batchUpdates
3695
- );
3696
- context.idFactory = makeIdFactory(token.actor);
3697
- context.socket = socket;
3698
- }
3699
- function authenticationFailure(error2) {
3700
- if (process.env.NODE_ENV !== "production") {
3701
- error("Call to authentication endpoint failed", error2);
3702
- }
3703
- context.token = null;
3704
- updateConnection({ status: "unavailable" }, batchUpdates);
3705
- context.numRetries++;
3706
- clearTimeout(context.timers.reconnect);
3707
- context.timers.reconnect = effects.scheduleReconnect(getRetryDelay());
3708
- }
3709
- function onVisibilityChange(visibilityState) {
3710
- if (visibilityState === "visible" && context.connection.current.status === "open") {
3711
- log("Heartbeat after visibility change");
3712
- heartbeat();
3713
- }
3714
- }
3715
4445
  function onUpdatePresenceMessage(message) {
3716
4446
  if (message.targetActor !== void 0) {
3717
4447
  const oldUser = context.others.getUser(message.actor);
@@ -3761,12 +4491,6 @@ function createRoom(options, config) {
3761
4491
  }
3762
4492
  return { type: "reset" };
3763
4493
  }
3764
- function onNavigatorOnline() {
3765
- if (context.connection.current.status === "unavailable") {
3766
- log("Try to reconnect after connectivity change");
3767
- reconnect();
3768
- }
3769
- }
3770
4494
  function canUndo() {
3771
4495
  return context.undoStack.length > 0;
3772
4496
  }
@@ -3824,11 +4548,7 @@ function createRoom(options, config) {
3824
4548
  notify(result.updates, batchedUpdatesWrapper);
3825
4549
  effects.send(messages);
3826
4550
  }
3827
- function onMessage(event) {
3828
- if (event.data === "pong") {
3829
- clearTimeout(context.timers.pongTimeout);
3830
- return;
3831
- }
4551
+ function handleServerMessage(event) {
3832
4552
  if (typeof event.data !== "string") {
3833
4553
  return;
3834
4554
  }
@@ -3929,140 +4649,6 @@ ${Array.from(traces).join("\n\n")}`
3929
4649
  notify(updates, doNotBatchUpdates);
3930
4650
  });
3931
4651
  }
3932
- function onClose(event) {
3933
- context.socket = null;
3934
- clearTimeout(context.timers.flush);
3935
- clearTimeout(context.timers.reconnect);
3936
- clearInterval(context.timers.heartbeat);
3937
- clearTimeout(context.timers.pongTimeout);
3938
- context.others.clearOthers();
3939
- batchUpdates(() => {
3940
- notify({ others: [{ type: "reset" }] }, doNotBatchUpdates);
3941
- if (event.code >= 4e3 && event.code <= 4100) {
3942
- updateConnection({ status: "failed" }, doNotBatchUpdates);
3943
- const error2 = new LiveblocksError(event.reason, event.code);
3944
- eventHub.error.notify(error2);
3945
- const delay = getRetryDelay(true);
3946
- context.numRetries++;
3947
- if (process.env.NODE_ENV !== "production") {
3948
- error(
3949
- `Connection to websocket server closed. Reason: ${error2.message} (code: ${error2.code}). Retrying in ${delay}ms.`
3950
- );
3951
- }
3952
- updateConnection({ status: "unavailable" }, doNotBatchUpdates);
3953
- clearTimeout(context.timers.reconnect);
3954
- context.timers.reconnect = effects.scheduleReconnect(delay);
3955
- } else if (event.code === 4999 /* CLOSE_WITHOUT_RETRY */) {
3956
- updateConnection({ status: "closed" }, doNotBatchUpdates);
3957
- } else {
3958
- const delay = getRetryDelay();
3959
- context.numRetries++;
3960
- if (process.env.NODE_ENV !== "production") {
3961
- warn(
3962
- `Connection to Liveblocks websocket server closed (code: ${event.code}). Retrying in ${delay}ms.`
3963
- );
3964
- }
3965
- updateConnection({ status: "unavailable" }, doNotBatchUpdates);
3966
- clearTimeout(context.timers.reconnect);
3967
- context.timers.reconnect = effects.scheduleReconnect(delay);
3968
- }
3969
- });
3970
- }
3971
- function updateConnection(connection, batchedUpdatesWrapper) {
3972
- context.connection.set(connection);
3973
- batchedUpdatesWrapper(() => {
3974
- eventHub.connection.notify(connection.status);
3975
- });
3976
- }
3977
- function getRetryDelay(slow = false) {
3978
- if (slow) {
3979
- return BACKOFF_RETRY_DELAYS_SLOW[context.numRetries < BACKOFF_RETRY_DELAYS_SLOW.length ? context.numRetries : BACKOFF_RETRY_DELAYS_SLOW.length - 1];
3980
- }
3981
- return BACKOFF_RETRY_DELAYS[context.numRetries < BACKOFF_RETRY_DELAYS.length ? context.numRetries : BACKOFF_RETRY_DELAYS.length - 1];
3982
- }
3983
- function onError() {
3984
- }
3985
- function onOpen() {
3986
- clearInterval(context.timers.heartbeat);
3987
- context.timers.heartbeat = effects.startHeartbeatInterval();
3988
- if (context.connection.current.status === "connecting") {
3989
- updateConnection(
3990
- __spreadProps(__spreadValues({}, context.connection.current), { status: "open" }),
3991
- batchUpdates
3992
- );
3993
- context.numRetries = 0;
3994
- if (context.lastConnectionId !== void 0) {
3995
- context.buffer.me = {
3996
- type: "full",
3997
- data: (
3998
- // Because state.me.current is a readonly object, we'll have to
3999
- // make a copy here. Otherwise, type errors happen later when
4000
- // "patching" my presence.
4001
- __spreadValues({}, context.me.current)
4002
- )
4003
- };
4004
- tryFlushing();
4005
- }
4006
- context.lastConnectionId = context.connection.current.id;
4007
- if (context.root) {
4008
- context.buffer.messages.push({ type: 200 /* FETCH_STORAGE */ });
4009
- }
4010
- tryFlushing();
4011
- } else {
4012
- }
4013
- }
4014
- function heartbeat() {
4015
- if (context.socket === null) {
4016
- return;
4017
- }
4018
- clearTimeout(context.timers.pongTimeout);
4019
- context.timers.pongTimeout = effects.schedulePongTimeout();
4020
- if (context.socket.readyState === context.socket.OPEN) {
4021
- context.socket.send("ping");
4022
- }
4023
- }
4024
- function pongTimeout() {
4025
- log("Pong timeout. Trying to reconnect.");
4026
- reconnect();
4027
- }
4028
- function disconnect() {
4029
- if (context.socket) {
4030
- context.socket.removeEventListener("open", onOpen);
4031
- context.socket.removeEventListener("message", onMessage);
4032
- context.socket.removeEventListener("close", onClose);
4033
- context.socket.removeEventListener("error", onError);
4034
- context.socket.close();
4035
- context.socket = null;
4036
- }
4037
- clearTimeout(context.timers.flush);
4038
- clearTimeout(context.timers.reconnect);
4039
- clearInterval(context.timers.heartbeat);
4040
- clearTimeout(context.timers.pongTimeout);
4041
- batchUpdates(() => {
4042
- updateConnection({ status: "closed" }, doNotBatchUpdates);
4043
- context.others.clearOthers();
4044
- notify({ others: [{ type: "reset" }] }, doNotBatchUpdates);
4045
- });
4046
- for (const eventSource2 of Object.values(eventHub)) {
4047
- eventSource2.clear();
4048
- }
4049
- }
4050
- function reconnect() {
4051
- if (context.socket) {
4052
- context.socket.removeEventListener("open", onOpen);
4053
- context.socket.removeEventListener("message", onMessage);
4054
- context.socket.removeEventListener("close", onClose);
4055
- context.socket.removeEventListener("error", onError);
4056
- context.socket.close();
4057
- context.socket = null;
4058
- }
4059
- clearTimeout(context.timers.flush);
4060
- clearTimeout(context.timers.reconnect);
4061
- clearInterval(context.timers.heartbeat);
4062
- clearTimeout(context.timers.pongTimeout);
4063
- updateConnection({ status: "unavailable" }, batchUpdates);
4064
- connect();
4065
- }
4066
4652
  function tryFlushing() {
4067
4653
  const storageOps = context.buffer.storageOperations;
4068
4654
  if (storageOps.length > 0) {
@@ -4071,7 +4657,7 @@ ${Array.from(traces).join("\n\n")}`
4071
4657
  }
4072
4658
  notifyStorageStatus();
4073
4659
  }
4074
- if (context.socket === null || context.socket.readyState !== context.socket.OPEN) {
4660
+ if (managedSocket.status !== "open") {
4075
4661
  context.buffer.storageOperations = [];
4076
4662
  return;
4077
4663
  }
@@ -4091,7 +4677,8 @@ ${Array.from(traces).join("\n\n")}`
4091
4677
  };
4092
4678
  } else {
4093
4679
  clearTimeout(context.timers.flush);
4094
- context.timers.flush = effects.scheduleFlush(
4680
+ context.timers.flush = setTimeout(
4681
+ tryFlushing,
4095
4682
  config.throttleDelay - elapsedMillis
4096
4683
  );
4097
4684
  }
@@ -4127,7 +4714,7 @@ ${Array.from(traces).join("\n\n")}`
4127
4714
  function broadcastEvent(event, options2 = {
4128
4715
  shouldQueueEventIfNotReady: false
4129
4716
  }) {
4130
- if (context.socket === null && !options2.shouldQueueEventIfNotReady) {
4717
+ if (managedSocket.status !== "open" && !options2.shouldQueueEventIfNotReady) {
4131
4718
  return;
4132
4719
  }
4133
4720
  context.buffer.messages.push({
@@ -4264,14 +4851,6 @@ ${Array.from(traces).join("\n\n")}`
4264
4851
  _addToRealUndoStack(historyOps, batchUpdates);
4265
4852
  }
4266
4853
  }
4267
- function simulateCloseWebsocket() {
4268
- if (context.socket) {
4269
- context.socket = null;
4270
- }
4271
- }
4272
- function simulateSendCloseEvent(event) {
4273
- onClose(event);
4274
- }
4275
4854
  function getStorageStatus() {
4276
4855
  if (_getInitialStatePromise === null) {
4277
4856
  return "not-loaded";
@@ -4311,28 +4890,39 @@ ${Array.from(traces).join("\n\n")}`
4311
4890
  return context.buffer;
4312
4891
  },
4313
4892
  // prettier-ignore
4314
- get numRetries() {
4315
- return context.numRetries;
4893
+ get undoStack() {
4894
+ return context.undoStack;
4895
+ },
4896
+ // prettier-ignore
4897
+ get nodeCount() {
4898
+ return context.nodes.size;
4316
4899
  },
4317
4900
  // prettier-ignore
4318
- onClose,
4319
- onMessage,
4320
- authenticationSuccess,
4321
- onNavigatorOnline,
4322
- simulateCloseWebsocket,
4323
- simulateSendCloseEvent,
4324
- onVisibilityChange,
4325
- getUndoStack: () => context.undoStack,
4326
- getItemsCount: () => context.nodes.size,
4327
- connect,
4328
- disconnect,
4329
4901
  // Support for the Liveblocks browser extension
4330
4902
  getSelf_forDevTools: () => selfAsTreeNode.current,
4331
- getOthers_forDevTools: () => others_forDevTools.current
4903
+ getOthers_forDevTools: () => others_forDevTools.current,
4904
+ // prettier-ignore
4905
+ send: {
4906
+ // These exist only for our E2E testing app
4907
+ explicitClose: (event) => managedSocket._privateSend({ type: "EXPLICIT_SOCKET_CLOSE", event }),
4908
+ implicitClose: () => managedSocket._privateSend({ type: "PONG_TIMEOUT" }),
4909
+ connect: () => managedSocket.connect(),
4910
+ disconnect: () => managedSocket.disconnect(),
4911
+ // XXX Do we really need disconnect() now that we have destroy()? Seems still useful to reset the machine, but also... YAGNI?
4912
+ destroy: () => managedSocket.destroy(),
4913
+ /**
4914
+ * This one looks differently from the rest, because receiving messages
4915
+ * is handled orthorgonally from all other possible events above,
4916
+ * because it does not matter what the connectivity state of the
4917
+ * machine is, so there won't be an explicit state machine transition
4918
+ * needed for this event.
4919
+ */
4920
+ incomingMessage: handleServerMessage
4921
+ }
4332
4922
  },
4333
4923
  id: config.roomId,
4334
4924
  subscribe: makeClassicSubscribeFn(events),
4335
- reconnect,
4925
+ reconnect: () => managedSocket.reconnect(),
4336
4926
  // Presence
4337
4927
  updatePresence,
4338
4928
  broadcastEvent,
@@ -4404,10 +4994,6 @@ function makeClassicSubscribeFn(events) {
4404
4994
  return events.connection.subscribe(
4405
4995
  callback
4406
4996
  );
4407
- case "storage":
4408
- return events.storage.subscribe(
4409
- callback
4410
- );
4411
4997
  case "history":
4412
4998
  return events.history.subscribe(callback);
4413
4999
  case "storage-status":
@@ -4443,67 +5029,66 @@ function makeClassicSubscribeFn(events) {
4443
5029
  function isRoomEventName(value) {
4444
5030
  return value === "my-presence" || value === "others" || value === "event" || value === "error" || value === "connection" || value === "history" || value === "storage-status";
4445
5031
  }
4446
- var LiveblocksError = class extends Error {
4447
- constructor(message, code) {
4448
- super(message);
4449
- this.code = code;
4450
- }
4451
- };
4452
- function prepareCreateWebSocket(liveblocksServer, WebSocketPolyfill) {
4453
- if (typeof window === "undefined" && WebSocketPolyfill === void 0) {
4454
- throw new Error(
4455
- "To use Liveblocks client in a non-dom environment, you need to provide a WebSocket polyfill."
4456
- );
4457
- }
4458
- const ws = WebSocketPolyfill || WebSocket;
4459
- return (token) => {
5032
+ function makeCreateSocketDelegateForRoom(liveblocksServer, WebSocketPolyfill) {
5033
+ return (richToken) => {
5034
+ const ws = WebSocketPolyfill || WebSocket;
5035
+ if (typeof WebSocket === "undefined" && WebSocketPolyfill === void 0) {
5036
+ throw new Error(
5037
+ "To use Liveblocks client in a non-dom environment, you need to provide a WebSocket polyfill."
5038
+ );
5039
+ }
5040
+ const token = richToken.raw;
4460
5041
  return new ws(
4461
5042
  `${liveblocksServer}/?token=${token}&version=${// prettier-ignore
4462
5043
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
4463
5044
  // @ts-ignore (__PACKAGE_VERSION__ will be injected by the build script)
4464
5045
  true ? (
4465
5046
  /* istanbul ignore next */
4466
- "1.0.8"
5047
+ "1.1.0-fsm1"
4467
5048
  ) : "dev"}`
4468
5049
  );
4469
5050
  };
4470
5051
  }
4471
- function prepareAuthEndpoint(authentication, fetchPolyfill) {
5052
+ function makeAuthDelegateForRoom(roomId, authentication, fetchPolyfill) {
4472
5053
  if (authentication.type === "public") {
4473
- if (typeof window === "undefined" && fetchPolyfill === void 0) {
4474
- throw new Error(
4475
- "To use Liveblocks client in a non-dom environment with a publicApiKey, you need to provide a fetch polyfill."
4476
- );
4477
- }
4478
- return (room) => fetchAuthEndpoint(
4479
- fetchPolyfill || /* istanbul ignore next */
4480
- fetch,
4481
- authentication.url,
4482
- {
4483
- room,
4484
- publicApiKey: authentication.publicApiKey
5054
+ return () => __async(this, null, function* () {
5055
+ if (typeof window === "undefined" && fetchPolyfill === void 0) {
5056
+ throw new Error(
5057
+ "To use Liveblocks client in a non-dom environment with a publicApiKey, you need to provide a fetch polyfill."
5058
+ );
4485
5059
  }
4486
- );
5060
+ return fetchAuthEndpoint(
5061
+ fetchPolyfill || /* istanbul ignore next */
5062
+ fetch,
5063
+ authentication.url,
5064
+ {
5065
+ room: roomId,
5066
+ publicApiKey: authentication.publicApiKey
5067
+ }
5068
+ ).then(({ token }) => parseRoomAuthToken(token));
5069
+ });
4487
5070
  }
4488
5071
  if (authentication.type === "private") {
4489
- if (typeof window === "undefined" && fetchPolyfill === void 0) {
4490
- throw new Error(
4491
- "To use Liveblocks client in a non-dom environment with a url as auth endpoint, you need to provide a fetch polyfill."
4492
- );
4493
- }
4494
- return (room) => fetchAuthEndpoint(fetchPolyfill || fetch, authentication.url, {
4495
- room
5072
+ return () => __async(this, null, function* () {
5073
+ if (typeof window === "undefined" && fetchPolyfill === void 0) {
5074
+ throw new Error(
5075
+ "To use Liveblocks client in a non-dom environment with a url as auth endpoint, you need to provide a fetch polyfill."
5076
+ );
5077
+ }
5078
+ return fetchAuthEndpoint(fetchPolyfill || fetch, authentication.url, {
5079
+ room: roomId
5080
+ }).then(({ token }) => parseRoomAuthToken(token));
4496
5081
  });
4497
5082
  }
4498
5083
  if (authentication.type === "custom") {
4499
- return (room) => __async(this, null, function* () {
4500
- const response = yield authentication.callback(room);
5084
+ return () => __async(this, null, function* () {
5085
+ const response = yield authentication.callback(roomId);
4501
5086
  if (!response || !response.token) {
4502
5087
  throw new Error(
4503
5088
  'Authentication error. We expect the authentication callback to return a token, but it does not. Hint: the return value should look like: { token: "..." }'
4504
5089
  );
4505
5090
  }
4506
- return response;
5091
+ return parseRoomAuthToken(response.token);
4507
5092
  });
4508
5093
  }
4509
5094
  throw new Error("Internal error. Unexpected authentication type");
@@ -4520,9 +5105,13 @@ function fetchAuthEndpoint(fetch2, endpoint, body) {
4520
5105
  body: JSON.stringify(body)
4521
5106
  });
4522
5107
  if (!res.ok) {
4523
- throw new AuthenticationError(
4524
- `Expected a status 200 but got ${res.status} when doing a POST request on "${endpoint}"`
4525
- );
5108
+ if (res.status === 401 || res.status === 403) {
5109
+ throw new UnauthorizedError(yield res.text());
5110
+ } else {
5111
+ throw new AuthenticationError(
5112
+ `Expected a status 200 but got ${res.status} when doing a POST request on "${endpoint}"`
5113
+ );
5114
+ }
4526
5115
  }
4527
5116
  let data;
4528
5117
  try {
@@ -4604,7 +5193,7 @@ function createClient(options) {
4604
5193
  }
4605
5194
  global.atob = clientOptions.polyfills.atob;
4606
5195
  }
4607
- newRoom.__internal.connect();
5196
+ newRoom.__internal.send.connect();
4608
5197
  }
4609
5198
  return newRoom;
4610
5199
  }
@@ -4612,25 +5201,10 @@ function createClient(options) {
4612
5201
  unlinkDevTools(roomId);
4613
5202
  const room = rooms.get(roomId);
4614
5203
  if (room !== void 0) {
4615
- room.__internal.disconnect();
5204
+ room.__internal.send.destroy();
4616
5205
  rooms.delete(roomId);
4617
5206
  }
4618
5207
  }
4619
- if (typeof window !== "undefined" && // istanbul ignore next: React Native environment doesn't implement window.addEventListener
4620
- typeof window.addEventListener !== "undefined") {
4621
- window.addEventListener("online", () => {
4622
- for (const [, room] of rooms) {
4623
- room.__internal.onNavigatorOnline();
4624
- }
4625
- });
4626
- }
4627
- if (typeof document !== "undefined") {
4628
- document.addEventListener("visibilitychange", () => {
4629
- for (const [, room] of rooms) {
4630
- room.__internal.onVisibilityChange(document.visibilityState);
4631
- }
4632
- });
4633
- }
4634
5208
  return {
4635
5209
  getRoom,
4636
5210
  enter,
@@ -5041,6 +5615,19 @@ function shallow(a, b) {
5041
5615
  return shallowObj(a, b);
5042
5616
  }
5043
5617
 
5618
+ // src/types/IWebSocket.ts
5619
+ var WebsocketCloseCodes = /* @__PURE__ */ ((WebsocketCloseCodes2) => {
5620
+ WebsocketCloseCodes2[WebsocketCloseCodes2["CLOSE_ABNORMAL"] = 1006] = "CLOSE_ABNORMAL";
5621
+ WebsocketCloseCodes2[WebsocketCloseCodes2["INVALID_MESSAGE_FORMAT"] = 4e3] = "INVALID_MESSAGE_FORMAT";
5622
+ WebsocketCloseCodes2[WebsocketCloseCodes2["NOT_ALLOWED"] = 4001] = "NOT_ALLOWED";
5623
+ WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_MESSAGES_PER_SECONDS"] = 4002] = "MAX_NUMBER_OF_MESSAGES_PER_SECONDS";
5624
+ WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_CONCURRENT_CONNECTIONS"] = 4003] = "MAX_NUMBER_OF_CONCURRENT_CONNECTIONS";
5625
+ WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP"] = 4004] = "MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP";
5626
+ WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_CONCURRENT_CONNECTIONS_PER_ROOM"] = 4005] = "MAX_NUMBER_OF_CONCURRENT_CONNECTIONS_PER_ROOM";
5627
+ WebsocketCloseCodes2[WebsocketCloseCodes2["CLOSE_WITHOUT_RETRY"] = 4999] = "CLOSE_WITHOUT_RETRY";
5628
+ return WebsocketCloseCodes2;
5629
+ })(WebsocketCloseCodes || {});
5630
+
5044
5631
 
5045
5632
 
5046
5633