@liveblocks/core 1.0.9 → 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 (2) hide show
  1. package/dist/index.js +913 -368
  2. 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.9"
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
  }
@@ -3239,31 +4002,11 @@ var DerivedRef = class extends ImmutableRef {
3239
4002
  }
3240
4003
  };
3241
4004
 
3242
- // src/types/IWebSocket.ts
3243
- var WebsocketCloseCodes = /* @__PURE__ */ ((WebsocketCloseCodes2) => {
3244
- WebsocketCloseCodes2[WebsocketCloseCodes2["CLOSE_ABNORMAL"] = 1006] = "CLOSE_ABNORMAL";
3245
- WebsocketCloseCodes2[WebsocketCloseCodes2["INVALID_MESSAGE_FORMAT"] = 4e3] = "INVALID_MESSAGE_FORMAT";
3246
- WebsocketCloseCodes2[WebsocketCloseCodes2["NOT_ALLOWED"] = 4001] = "NOT_ALLOWED";
3247
- WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_MESSAGES_PER_SECONDS"] = 4002] = "MAX_NUMBER_OF_MESSAGES_PER_SECONDS";
3248
- WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_CONCURRENT_CONNECTIONS"] = 4003] = "MAX_NUMBER_OF_CONCURRENT_CONNECTIONS";
3249
- WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP"] = 4004] = "MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP";
3250
- WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_CONCURRENT_CONNECTIONS_PER_ROOM"] = 4005] = "MAX_NUMBER_OF_CONCURRENT_CONNECTIONS_PER_ROOM";
3251
- WebsocketCloseCodes2[WebsocketCloseCodes2["CLOSE_WITHOUT_RETRY"] = 4999] = "CLOSE_WITHOUT_RETRY";
3252
- return WebsocketCloseCodes2;
3253
- })(WebsocketCloseCodes || {});
3254
-
3255
4005
  // src/room.ts
3256
- var BACKOFF_RETRY_DELAYS = [250, 500, 1e3, 2e3, 4e3, 8e3, 1e4];
3257
- var BACKOFF_RETRY_DELAYS_SLOW = [2e3, 3e4, 6e4, 3e5];
3258
- var HEARTBEAT_INTERVAL = 3e4;
3259
- var PONG_TIMEOUT = 2e3;
3260
4006
  function makeIdFactory(connectionId) {
3261
4007
  let count = 0;
3262
4008
  return () => `${connectionId}:${count++}`;
3263
4009
  }
3264
- function log(..._params) {
3265
- return;
3266
- }
3267
4010
  function isConnectionSelfAware(connection) {
3268
4011
  return connection.status === "open" || connection.status === "connecting";
3269
4012
  }
@@ -3276,19 +4019,25 @@ function userToTreeNode(key, user) {
3276
4019
  };
3277
4020
  }
3278
4021
  function createRoom(options, config) {
3279
- var _a;
4022
+ var _a, _b, _c;
3280
4023
  const initialPresence = typeof options.initialPresence === "function" ? options.initialPresence(config.roomId) : options.initialPresence;
3281
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);
3282
4037
  const context = {
3283
- token: null,
3284
4038
  lastConnectionId: null,
3285
- socket: null,
3286
- numRetries: 0,
3287
4039
  timers: {
3288
- flush: void 0,
3289
- reconnect: void 0,
3290
- heartbeat: void 0,
3291
- pongTimeout: void 0
4040
+ flush: void 0
3292
4041
  },
3293
4042
  buffer: {
3294
4043
  lastFlushedAt: 0,
@@ -3321,7 +4070,68 @@ function createRoom(options, config) {
3321
4070
  opStackTraces: process.env.NODE_ENV !== "production" ? /* @__PURE__ */ new Map() : void 0
3322
4071
  };
3323
4072
  const doNotBatchUpdates = (cb) => cb();
3324
- 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
+ });
3325
4135
  const pool = {
3326
4136
  roomId: config.roomId,
3327
4137
  getNode: (id) => context.nodes.get(id),
@@ -3382,40 +4192,9 @@ function createRoom(options, config) {
3382
4192
  storageStatus: makeEventSource()
3383
4193
  };
3384
4194
  const effects = config.mockedEffects || {
3385
- authenticateAndConnect(auth, createWebSocket) {
3386
- const prevToken = context.token;
3387
- if (prevToken !== null && !isTokenExpired(prevToken.parsed)) {
3388
- const socket = createWebSocket(prevToken);
3389
- handleAuthSuccess(prevToken.parsed, socket);
3390
- return void 0;
3391
- } else {
3392
- void auth().then((token) => {
3393
- if (context.connection.current.status !== "authenticating") {
3394
- return;
3395
- }
3396
- const socket = createWebSocket(token);
3397
- handleAuthSuccess(token.parsed, socket);
3398
- context.token = token;
3399
- }).catch(
3400
- (er) => authenticationFailure(
3401
- er instanceof Error ? er : new Error(String(er))
3402
- )
3403
- );
3404
- return void 0;
3405
- }
3406
- },
3407
4195
  send(messageOrMessages) {
3408
- if (context.socket === null) {
3409
- throw new Error("Can't send message if socket is null");
3410
- }
3411
- if (context.socket.readyState === context.socket.OPEN) {
3412
- context.socket.send(JSON.stringify(messageOrMessages));
3413
- }
3414
- },
3415
- scheduleFlush: (delay) => setTimeout(tryFlushing, delay),
3416
- scheduleReconnect: (delay) => setTimeout(handleConnect, delay),
3417
- startHeartbeatInterval: () => setInterval(heartbeat, HEARTBEAT_INTERVAL),
3418
- schedulePongTimeout: () => setTimeout(pongTimeout, PONG_TIMEOUT)
4196
+ managedSocket.send(JSON.stringify(messageOrMessages));
4197
+ }
3419
4198
  };
3420
4199
  const self = new DerivedRef(
3421
4200
  context.connection,
@@ -3622,23 +4401,6 @@ function createRoom(options, config) {
3622
4401
  }
3623
4402
  }
3624
4403
  }
3625
- function handleConnect() {
3626
- var _a2, _b;
3627
- if (context.connection.current.status !== "closed" && context.connection.current.status !== "unavailable") {
3628
- return;
3629
- }
3630
- const auth = prepareAuthEndpoint(
3631
- config.roomId,
3632
- config.authentication,
3633
- (_a2 = config.polyfills) == null ? void 0 : _a2.fetch
3634
- );
3635
- const createWebSocket = prepareCreateWebSocket(
3636
- config.liveblocksServer,
3637
- (_b = config.polyfills) == null ? void 0 : _b.WebSocket
3638
- );
3639
- updateConnection({ status: "authenticating" }, batchUpdates);
3640
- effects.authenticateAndConnect(auth, createWebSocket);
3641
- }
3642
4404
  function updatePresence(patch, options2) {
3643
4405
  const oldValues = {};
3644
4406
  if (context.buffer.me === null) {
@@ -3680,40 +4442,6 @@ function createRoom(options, config) {
3680
4442
  function isStorageReadOnly(scopes) {
3681
4443
  return scopes.includes("room:read" /* Read */) && scopes.includes("room:presence:write" /* PresenceWrite */) && !scopes.includes("room:write" /* Write */);
3682
4444
  }
3683
- function handleAuthSuccess(token, socket) {
3684
- socket.addEventListener("message", handleRawSocketMessage);
3685
- socket.addEventListener("open", handleSocketOpen);
3686
- socket.addEventListener("close", handleExplicitClose);
3687
- socket.addEventListener("error", handleSocketError);
3688
- updateConnection(
3689
- {
3690
- status: "connecting",
3691
- id: token.actor,
3692
- userInfo: token.info,
3693
- userId: token.id,
3694
- isReadOnly: isStorageReadOnly(token.scopes)
3695
- },
3696
- batchUpdates
3697
- );
3698
- context.idFactory = makeIdFactory(token.actor);
3699
- context.socket = socket;
3700
- }
3701
- function authenticationFailure(error2) {
3702
- if (process.env.NODE_ENV !== "production") {
3703
- error("Call to authentication endpoint failed", error2);
3704
- }
3705
- context.token = null;
3706
- updateConnection({ status: "unavailable" }, batchUpdates);
3707
- context.numRetries++;
3708
- clearTimeout(context.timers.reconnect);
3709
- context.timers.reconnect = effects.scheduleReconnect(getRetryDelay());
3710
- }
3711
- function handleWindowGotFocus() {
3712
- if (context.connection.current.status === "open") {
3713
- log("Heartbeat after visibility change");
3714
- heartbeat();
3715
- }
3716
- }
3717
4445
  function onUpdatePresenceMessage(message) {
3718
4446
  if (message.targetActor !== void 0) {
3719
4447
  const oldUser = context.others.getUser(message.actor);
@@ -3763,12 +4491,6 @@ function createRoom(options, config) {
3763
4491
  }
3764
4492
  return { type: "reset" };
3765
4493
  }
3766
- function handleNavigatorBackOnline() {
3767
- if (context.connection.current.status === "unavailable") {
3768
- log("Try to reconnect after connectivity change");
3769
- reconnect();
3770
- }
3771
- }
3772
4494
  function canUndo() {
3773
4495
  return context.undoStack.length > 0;
3774
4496
  }
@@ -3826,16 +4548,6 @@ function createRoom(options, config) {
3826
4548
  notify(result.updates, batchedUpdatesWrapper);
3827
4549
  effects.send(messages);
3828
4550
  }
3829
- function handleRawSocketMessage(event) {
3830
- if (event.data === "pong") {
3831
- transition({ type: "RECEIVE_PONG" });
3832
- } else {
3833
- handleServerMessage(event);
3834
- }
3835
- }
3836
- function handlePong() {
3837
- clearTimeout(context.timers.pongTimeout);
3838
- }
3839
4551
  function handleServerMessage(event) {
3840
4552
  if (typeof event.data !== "string") {
3841
4553
  return;
@@ -3937,140 +4649,6 @@ ${Array.from(traces).join("\n\n")}`
3937
4649
  notify(updates, doNotBatchUpdates);
3938
4650
  });
3939
4651
  }
3940
- function handleExplicitClose(event) {
3941
- context.socket = null;
3942
- clearTimeout(context.timers.flush);
3943
- clearTimeout(context.timers.reconnect);
3944
- clearInterval(context.timers.heartbeat);
3945
- clearTimeout(context.timers.pongTimeout);
3946
- context.others.clearOthers();
3947
- batchUpdates(() => {
3948
- notify({ others: [{ type: "reset" }] }, doNotBatchUpdates);
3949
- if (event.code >= 4e3 && event.code <= 4100) {
3950
- updateConnection({ status: "failed" }, doNotBatchUpdates);
3951
- const error2 = new LiveblocksError(event.reason, event.code);
3952
- eventHub.error.notify(error2);
3953
- const delay = getRetryDelay(true);
3954
- context.numRetries++;
3955
- if (process.env.NODE_ENV !== "production") {
3956
- error(
3957
- `Connection to websocket server closed. Reason: ${error2.message} (code: ${error2.code}). Retrying in ${delay}ms.`
3958
- );
3959
- }
3960
- updateConnection({ status: "unavailable" }, doNotBatchUpdates);
3961
- clearTimeout(context.timers.reconnect);
3962
- context.timers.reconnect = effects.scheduleReconnect(delay);
3963
- } else if (event.code === 4999 /* CLOSE_WITHOUT_RETRY */) {
3964
- updateConnection({ status: "closed" }, doNotBatchUpdates);
3965
- } else {
3966
- const delay = getRetryDelay();
3967
- context.numRetries++;
3968
- if (process.env.NODE_ENV !== "production") {
3969
- warn(
3970
- `Connection to Liveblocks websocket server closed (code: ${event.code}). Retrying in ${delay}ms.`
3971
- );
3972
- }
3973
- updateConnection({ status: "unavailable" }, doNotBatchUpdates);
3974
- clearTimeout(context.timers.reconnect);
3975
- context.timers.reconnect = effects.scheduleReconnect(delay);
3976
- }
3977
- });
3978
- }
3979
- function updateConnection(connection, batchedUpdatesWrapper) {
3980
- context.connection.set(connection);
3981
- batchedUpdatesWrapper(() => {
3982
- eventHub.connection.notify(connection.status);
3983
- });
3984
- }
3985
- function getRetryDelay(slow = false) {
3986
- if (slow) {
3987
- return BACKOFF_RETRY_DELAYS_SLOW[context.numRetries < BACKOFF_RETRY_DELAYS_SLOW.length ? context.numRetries : BACKOFF_RETRY_DELAYS_SLOW.length - 1];
3988
- }
3989
- return BACKOFF_RETRY_DELAYS[context.numRetries < BACKOFF_RETRY_DELAYS.length ? context.numRetries : BACKOFF_RETRY_DELAYS.length - 1];
3990
- }
3991
- function handleSocketError() {
3992
- }
3993
- function handleSocketOpen() {
3994
- clearInterval(context.timers.heartbeat);
3995
- context.timers.heartbeat = effects.startHeartbeatInterval();
3996
- if (context.connection.current.status === "connecting") {
3997
- updateConnection(
3998
- __spreadProps(__spreadValues({}, context.connection.current), { status: "open" }),
3999
- batchUpdates
4000
- );
4001
- context.numRetries = 0;
4002
- if (context.lastConnectionId !== void 0) {
4003
- context.buffer.me = {
4004
- type: "full",
4005
- data: (
4006
- // Because state.me.current is a readonly object, we'll have to
4007
- // make a copy here. Otherwise, type errors happen later when
4008
- // "patching" my presence.
4009
- __spreadValues({}, context.me.current)
4010
- )
4011
- };
4012
- tryFlushing();
4013
- }
4014
- context.lastConnectionId = context.connection.current.id;
4015
- if (context.root) {
4016
- context.buffer.messages.push({ type: 200 /* FETCH_STORAGE */ });
4017
- }
4018
- tryFlushing();
4019
- } else {
4020
- }
4021
- }
4022
- function heartbeat() {
4023
- if (context.socket === null) {
4024
- return;
4025
- }
4026
- clearTimeout(context.timers.pongTimeout);
4027
- context.timers.pongTimeout = effects.schedulePongTimeout();
4028
- if (context.socket.readyState === context.socket.OPEN) {
4029
- context.socket.send("ping");
4030
- }
4031
- }
4032
- function pongTimeout() {
4033
- log("Pong timeout. Trying to reconnect.");
4034
- reconnect();
4035
- }
4036
- function handleDisconnect() {
4037
- if (context.socket) {
4038
- context.socket.removeEventListener("open", handleSocketOpen);
4039
- context.socket.removeEventListener("message", handleRawSocketMessage);
4040
- context.socket.removeEventListener("close", handleExplicitClose);
4041
- context.socket.removeEventListener("error", handleSocketError);
4042
- context.socket.close();
4043
- context.socket = null;
4044
- }
4045
- clearTimeout(context.timers.flush);
4046
- clearTimeout(context.timers.reconnect);
4047
- clearInterval(context.timers.heartbeat);
4048
- clearTimeout(context.timers.pongTimeout);
4049
- batchUpdates(() => {
4050
- updateConnection({ status: "closed" }, doNotBatchUpdates);
4051
- context.others.clearOthers();
4052
- notify({ others: [{ type: "reset" }] }, doNotBatchUpdates);
4053
- });
4054
- for (const eventSource2 of Object.values(eventHub)) {
4055
- eventSource2.clear();
4056
- }
4057
- }
4058
- function reconnect() {
4059
- if (context.socket) {
4060
- context.socket.removeEventListener("open", handleSocketOpen);
4061
- context.socket.removeEventListener("message", handleRawSocketMessage);
4062
- context.socket.removeEventListener("close", handleExplicitClose);
4063
- context.socket.removeEventListener("error", handleSocketError);
4064
- context.socket.close();
4065
- context.socket = null;
4066
- }
4067
- clearTimeout(context.timers.flush);
4068
- clearTimeout(context.timers.reconnect);
4069
- clearInterval(context.timers.heartbeat);
4070
- clearTimeout(context.timers.pongTimeout);
4071
- updateConnection({ status: "unavailable" }, batchUpdates);
4072
- handleConnect();
4073
- }
4074
4652
  function tryFlushing() {
4075
4653
  const storageOps = context.buffer.storageOperations;
4076
4654
  if (storageOps.length > 0) {
@@ -4079,7 +4657,7 @@ ${Array.from(traces).join("\n\n")}`
4079
4657
  }
4080
4658
  notifyStorageStatus();
4081
4659
  }
4082
- if (context.socket === null || context.socket.readyState !== context.socket.OPEN) {
4660
+ if (managedSocket.status !== "open") {
4083
4661
  context.buffer.storageOperations = [];
4084
4662
  return;
4085
4663
  }
@@ -4099,7 +4677,8 @@ ${Array.from(traces).join("\n\n")}`
4099
4677
  };
4100
4678
  } else {
4101
4679
  clearTimeout(context.timers.flush);
4102
- context.timers.flush = effects.scheduleFlush(
4680
+ context.timers.flush = setTimeout(
4681
+ tryFlushing,
4103
4682
  config.throttleDelay - elapsedMillis
4104
4683
  );
4105
4684
  }
@@ -4135,7 +4714,7 @@ ${Array.from(traces).join("\n\n")}`
4135
4714
  function broadcastEvent(event, options2 = {
4136
4715
  shouldQueueEventIfNotReady: false
4137
4716
  }) {
4138
- if (context.socket === null && !options2.shouldQueueEventIfNotReady) {
4717
+ if (managedSocket.status !== "open" && !options2.shouldQueueEventIfNotReady) {
4139
4718
  return;
4140
4719
  }
4141
4720
  context.buffer.messages.push({
@@ -4272,11 +4851,6 @@ ${Array.from(traces).join("\n\n")}`
4272
4851
  _addToRealUndoStack(historyOps, batchUpdates);
4273
4852
  }
4274
4853
  }
4275
- function handleImplicitClose() {
4276
- if (context.socket) {
4277
- context.socket = null;
4278
- }
4279
- }
4280
4854
  function getStorageStatus() {
4281
4855
  if (_getInitialStatePromise === null) {
4282
4856
  return "not-loaded";
@@ -4309,28 +4883,6 @@ ${Array.from(traces).join("\n\n")}`
4309
4883
  storageDidLoad: eventHub.storageDidLoad.observable,
4310
4884
  storageStatus: eventHub.storageStatus.observable
4311
4885
  };
4312
- function transition(event) {
4313
- switch (event.type) {
4314
- case "CONNECT":
4315
- return handleConnect();
4316
- case "DISCONNECT":
4317
- return handleDisconnect();
4318
- case "RECEIVE_PONG":
4319
- return handlePong();
4320
- case "AUTH_SUCCESS":
4321
- return handleAuthSuccess(event.token, event.socket);
4322
- case "WINDOW_GOT_FOCUS":
4323
- return handleWindowGotFocus();
4324
- case "NAVIGATOR_ONLINE":
4325
- return handleNavigatorBackOnline();
4326
- case "IMPLICIT_CLOSE":
4327
- return handleImplicitClose();
4328
- case "EXPLICIT_CLOSE":
4329
- return handleExplicitClose(event.closeEvent);
4330
- default:
4331
- return assertNever(event, "Invalid event");
4332
- }
4333
- }
4334
4886
  return {
4335
4887
  /* NOTE: Exposing __internal here only to allow testing implementation details in unit tests */
4336
4888
  __internal: {
@@ -4338,10 +4890,6 @@ ${Array.from(traces).join("\n\n")}`
4338
4890
  return context.buffer;
4339
4891
  },
4340
4892
  // prettier-ignore
4341
- get numRetries() {
4342
- return context.numRetries;
4343
- },
4344
- // prettier-ignore
4345
4893
  get undoStack() {
4346
4894
  return context.undoStack;
4347
4895
  },
@@ -4355,14 +4903,13 @@ ${Array.from(traces).join("\n\n")}`
4355
4903
  getOthers_forDevTools: () => others_forDevTools.current,
4356
4904
  // prettier-ignore
4357
4905
  send: {
4358
- explicitClose: (closeEvent) => transition({ type: "EXPLICIT_CLOSE", closeEvent }),
4359
- implicitClose: () => transition({ type: "IMPLICIT_CLOSE" }),
4360
- authSuccess: (token, socket) => transition({ type: "AUTH_SUCCESS", token, socket }),
4361
- navigatorOnline: () => transition({ type: "NAVIGATOR_ONLINE" }),
4362
- windowGotFocus: () => transition({ type: "WINDOW_GOT_FOCUS" }),
4363
- pong: () => transition({ type: "RECEIVE_PONG" }),
4364
- connect: () => transition({ type: "CONNECT" }),
4365
- disconnect: () => transition({ type: "DISCONNECT" }),
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(),
4366
4913
  /**
4367
4914
  * This one looks differently from the rest, because receiving messages
4368
4915
  * is handled orthorgonally from all other possible events above,
@@ -4375,7 +4922,7 @@ ${Array.from(traces).join("\n\n")}`
4375
4922
  },
4376
4923
  id: config.roomId,
4377
4924
  subscribe: makeClassicSubscribeFn(events),
4378
- reconnect,
4925
+ reconnect: () => managedSocket.reconnect(),
4379
4926
  // Presence
4380
4927
  updatePresence,
4381
4928
  broadcastEvent,
@@ -4482,20 +5029,14 @@ function makeClassicSubscribeFn(events) {
4482
5029
  function isRoomEventName(value) {
4483
5030
  return value === "my-presence" || value === "others" || value === "event" || value === "error" || value === "connection" || value === "history" || value === "storage-status";
4484
5031
  }
4485
- var LiveblocksError = class extends Error {
4486
- constructor(message, code) {
4487
- super(message);
4488
- this.code = code;
4489
- }
4490
- };
4491
- function prepareCreateWebSocket(liveblocksServer, WebSocketPolyfill) {
4492
- if (typeof window === "undefined" && WebSocketPolyfill === void 0) {
4493
- throw new Error(
4494
- "To use Liveblocks client in a non-dom environment, you need to provide a WebSocket polyfill."
4495
- );
4496
- }
4497
- const ws = WebSocketPolyfill || WebSocket;
5032
+ function makeCreateSocketDelegateForRoom(liveblocksServer, WebSocketPolyfill) {
4498
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
+ }
4499
5040
  const token = richToken.raw;
4500
5041
  return new ws(
4501
5042
  `${liveblocksServer}/?token=${token}&version=${// prettier-ignore
@@ -4503,37 +5044,41 @@ function prepareCreateWebSocket(liveblocksServer, WebSocketPolyfill) {
4503
5044
  // @ts-ignore (__PACKAGE_VERSION__ will be injected by the build script)
4504
5045
  true ? (
4505
5046
  /* istanbul ignore next */
4506
- "1.0.9"
5047
+ "1.1.0-fsm1"
4507
5048
  ) : "dev"}`
4508
5049
  );
4509
5050
  };
4510
5051
  }
4511
- function prepareAuthEndpoint(roomId, authentication, fetchPolyfill) {
5052
+ function makeAuthDelegateForRoom(roomId, authentication, fetchPolyfill) {
4512
5053
  if (authentication.type === "public") {
4513
- if (typeof window === "undefined" && fetchPolyfill === void 0) {
4514
- throw new Error(
4515
- "To use Liveblocks client in a non-dom environment with a publicApiKey, you need to provide a fetch polyfill."
4516
- );
4517
- }
4518
- return () => fetchAuthEndpoint(
4519
- fetchPolyfill || /* istanbul ignore next */
4520
- fetch,
4521
- authentication.url,
4522
- {
4523
- room: roomId,
4524
- 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
+ );
4525
5059
  }
4526
- ).then(({ token }) => parseRoomAuthToken(token));
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
+ });
4527
5070
  }
4528
5071
  if (authentication.type === "private") {
4529
- if (typeof window === "undefined" && fetchPolyfill === void 0) {
4530
- throw new Error(
4531
- "To use Liveblocks client in a non-dom environment with a url as auth endpoint, you need to provide a fetch polyfill."
4532
- );
4533
- }
4534
- return () => fetchAuthEndpoint(fetchPolyfill || fetch, authentication.url, {
4535
- room: roomId
4536
- }).then(({ token }) => parseRoomAuthToken(token));
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));
5081
+ });
4537
5082
  }
4538
5083
  if (authentication.type === "custom") {
4539
5084
  return () => __async(this, null, function* () {
@@ -4560,9 +5105,13 @@ function fetchAuthEndpoint(fetch2, endpoint, body) {
4560
5105
  body: JSON.stringify(body)
4561
5106
  });
4562
5107
  if (!res.ok) {
4563
- throw new AuthenticationError(
4564
- `Expected a status 200 but got ${res.status} when doing a POST request on "${endpoint}"`
4565
- );
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
+ }
4566
5115
  }
4567
5116
  let data;
4568
5117
  try {
@@ -4652,27 +5201,10 @@ function createClient(options) {
4652
5201
  unlinkDevTools(roomId);
4653
5202
  const room = rooms.get(roomId);
4654
5203
  if (room !== void 0) {
4655
- room.__internal.send.disconnect();
5204
+ room.__internal.send.destroy();
4656
5205
  rooms.delete(roomId);
4657
5206
  }
4658
5207
  }
4659
- if (typeof window !== "undefined" && // istanbul ignore next: React Native environment doesn't implement window.addEventListener
4660
- typeof window.addEventListener !== "undefined") {
4661
- window.addEventListener("online", () => {
4662
- for (const [, room] of rooms) {
4663
- room.__internal.send.navigatorOnline();
4664
- }
4665
- });
4666
- }
4667
- if (typeof document !== "undefined") {
4668
- document.addEventListener("visibilitychange", () => {
4669
- if (document.visibilityState === "visible") {
4670
- for (const [, room] of rooms) {
4671
- room.__internal.send.windowGotFocus();
4672
- }
4673
- }
4674
- });
4675
- }
4676
5208
  return {
4677
5209
  getRoom,
4678
5210
  enter,
@@ -5083,6 +5615,19 @@ function shallow(a, b) {
5083
5615
  return shallowObj(a, b);
5084
5616
  }
5085
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
+
5086
5631
 
5087
5632
 
5088
5633