@liveblocks/core 1.0.12 → 1.1.0-beta1

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 +156 -59
  2. package/dist/index.js +1272 -522
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -54,6 +54,19 @@ var __async = (__this, __arguments, generator) => {
54
54
  function makeEventSource() {
55
55
  const _onetimeObservers = /* @__PURE__ */ new Set();
56
56
  const _observers = /* @__PURE__ */ new Set();
57
+ let _buffer = null;
58
+ function pause() {
59
+ _buffer = [];
60
+ }
61
+ function unpause() {
62
+ if (_buffer === null) {
63
+ return;
64
+ }
65
+ for (const event of _buffer) {
66
+ notify(event);
67
+ }
68
+ _buffer = null;
69
+ }
57
70
  function subscribe(callback) {
58
71
  _observers.add(callback);
59
72
  return () => _observers.delete(callback);
@@ -62,6 +75,25 @@ function makeEventSource() {
62
75
  _onetimeObservers.add(callback);
63
76
  return () => _onetimeObservers.delete(callback);
64
77
  }
78
+ function waitUntil(predicate) {
79
+ return __async(this, null, function* () {
80
+ let unsub;
81
+ return new Promise((res) => {
82
+ unsub = subscribe((event) => {
83
+ if (predicate === void 0 || predicate(event)) {
84
+ res(event);
85
+ }
86
+ });
87
+ }).finally(() => unsub == null ? void 0 : unsub());
88
+ });
89
+ }
90
+ function notifyOrBuffer(event) {
91
+ if (_buffer !== null) {
92
+ _buffer.push(event);
93
+ } else {
94
+ notify(event);
95
+ }
96
+ }
65
97
  function notify(event) {
66
98
  _onetimeObservers.forEach((callback) => callback(event));
67
99
  _onetimeObservers.clear();
@@ -71,16 +103,24 @@ function makeEventSource() {
71
103
  _onetimeObservers.clear();
72
104
  _observers.clear();
73
105
  }
106
+ function count() {
107
+ return _onetimeObservers.size + _observers.size;
108
+ }
74
109
  return {
75
110
  // Private/internal control over event emission
76
- notify,
111
+ notify: notifyOrBuffer,
77
112
  subscribe,
78
113
  subscribeOnce,
79
114
  clear,
115
+ count,
116
+ waitUntil,
117
+ pause,
118
+ unpause,
80
119
  // Publicly exposable subscription API
81
120
  observable: {
82
121
  subscribe,
83
- subscribeOnce
122
+ subscribeOnce,
123
+ waitUntil
84
124
  }
85
125
  };
86
126
  }
@@ -117,7 +157,7 @@ var onMessageFromPanel = eventSource.observable;
117
157
  // src/devtools/index.ts
118
158
  var VERSION = true ? (
119
159
  /* istanbul ignore next */
120
- "1.0.12"
160
+ "1.1.0-beta1"
121
161
  ) : "dev";
122
162
  var _devtoolsSetupHasRun = false;
123
163
  function setupDevTools(getAllRooms) {
@@ -159,7 +199,7 @@ function startSyncStream(room) {
159
199
  fullSync(room);
160
200
  unsubsByRoomId.set(room.id, [
161
201
  // When the connection status changes
162
- room.events.connection.subscribe(() => partialSyncConnection(room)),
202
+ room.events.status.subscribe(() => partialSyncConnection(room)),
163
203
  // When storage initializes, send the update
164
204
  room.events.storageDidLoad.subscribeOnce(() => partialSyncStorage(room)),
165
205
  // Any time storage updates, send the new storage root
@@ -173,7 +213,7 @@ function partialSyncConnection(room) {
173
213
  sendToPanel({
174
214
  msg: "room::sync::partial",
175
215
  roomId: room.id,
176
- status: room.getConnectionState()
216
+ status: room.getStatus()
177
217
  });
178
218
  }
179
219
  function partialSyncStorage(room) {
@@ -214,7 +254,7 @@ function fullSync(room) {
214
254
  sendToPanel({
215
255
  msg: "room::sync::full",
216
256
  roomId: room.id,
217
- status: room.getConnectionState(),
257
+ status: room.getStatus(),
218
258
  storage: (_a = root == null ? void 0 : root.toTreeNode("root").payload) != null ? _a : null,
219
259
  me,
220
260
  others
@@ -345,6 +385,965 @@ function nn(value, errmsg = "Expected value to be non-nullable") {
345
385
  return value;
346
386
  }
347
387
 
388
+ // src/lib/fsm.ts
389
+ function distance(state1, state2) {
390
+ if (state1 === state2) {
391
+ return [0, 0];
392
+ }
393
+ const chunks1 = state1.split(".");
394
+ const chunks2 = state2.split(".");
395
+ const minLen = Math.min(chunks1.length, chunks2.length);
396
+ let shared = 0;
397
+ for (; shared < minLen; shared++) {
398
+ if (chunks1[shared] !== chunks2[shared]) {
399
+ break;
400
+ }
401
+ }
402
+ const up = chunks1.length - shared;
403
+ const down = chunks2.length - shared;
404
+ return [up, down];
405
+ }
406
+ function patterns(targetState, levels) {
407
+ const parts = targetState.split(".");
408
+ if (levels < 1 || levels > parts.length + 1) {
409
+ throw new Error("Invalid number of levels");
410
+ }
411
+ const result = [];
412
+ if (levels > parts.length) {
413
+ result.push("*");
414
+ }
415
+ for (let i = parts.length - levels + 1; i < parts.length; i++) {
416
+ const slice = parts.slice(0, i);
417
+ if (slice.length > 0) {
418
+ result.push(slice.join(".") + ".*");
419
+ }
420
+ }
421
+ result.push(targetState);
422
+ return result;
423
+ }
424
+ var SafeContext = class {
425
+ constructor(initialContext) {
426
+ this.curr = initialContext;
427
+ }
428
+ get current() {
429
+ return this.curr;
430
+ }
431
+ /**
432
+ * Call a callback function that allows patching of the context, by
433
+ * calling `context.patch()`. Patching is only allowed for the duration
434
+ * of this window.
435
+ */
436
+ allowPatching(callback) {
437
+ const self = this;
438
+ let allowed = true;
439
+ const patchableContext = __spreadProps(__spreadValues({}, this.curr), {
440
+ patch(patch) {
441
+ if (allowed) {
442
+ self.curr = Object.assign({}, self.curr, patch);
443
+ for (const pair of Object.entries(patch)) {
444
+ const [key, value] = pair;
445
+ if (key !== "patch") {
446
+ this[key] = value;
447
+ }
448
+ }
449
+ } else {
450
+ throw new Error("Can no longer patch stale context");
451
+ }
452
+ }
453
+ });
454
+ callback(patchableContext);
455
+ allowed = false;
456
+ return;
457
+ }
458
+ };
459
+ var nextId = 1;
460
+ var FSM = class {
461
+ /**
462
+ * Returns the initial state, which is defined by the first call made to
463
+ * .addState().
464
+ */
465
+ get initialState() {
466
+ const result = this.states.values()[Symbol.iterator]().next();
467
+ if (result.done) {
468
+ throw new Error("No states defined yet");
469
+ } else {
470
+ return result.value;
471
+ }
472
+ }
473
+ get currentState() {
474
+ if (this.currentStateOrNull === null) {
475
+ throw new Error("Not started yet");
476
+ }
477
+ return this.currentStateOrNull;
478
+ }
479
+ /**
480
+ * Starts the machine by entering the initial state.
481
+ */
482
+ start() {
483
+ if (this.runningState !== 0 /* NOT_STARTED_YET */) {
484
+ throw new Error("State machine has already started");
485
+ }
486
+ this.runningState = 1 /* STARTED */;
487
+ this.currentStateOrNull = this.initialState;
488
+ this.enter(null);
489
+ return this;
490
+ }
491
+ /**
492
+ * Stops the state machine. Stopping the state machine will call exit
493
+ * handlers for the current state, but not enter a new state.
494
+ */
495
+ stop() {
496
+ if (this.runningState !== 1 /* STARTED */) {
497
+ throw new Error("Cannot stop a state machine that isn't started yet");
498
+ }
499
+ this.runningState = 2 /* STOPPED */;
500
+ this.exit(null);
501
+ this.currentStateOrNull = null;
502
+ }
503
+ constructor(initialContext) {
504
+ this.id = nextId++;
505
+ this.runningState = 0 /* NOT_STARTED_YET */;
506
+ this.currentStateOrNull = null;
507
+ this.states = /* @__PURE__ */ new Set();
508
+ this.enterFns = /* @__PURE__ */ new Map();
509
+ this.cleanupStack = [];
510
+ this.knownEventTypes = /* @__PURE__ */ new Set();
511
+ this.allowedTransitions = /* @__PURE__ */ new Map();
512
+ this.currentContext = new SafeContext(initialContext);
513
+ this.eventHub = {
514
+ didReceiveEvent: makeEventSource(),
515
+ willTransition: makeEventSource(),
516
+ didIgnoreEvent: makeEventSource(),
517
+ willExitState: makeEventSource(),
518
+ didEnterState: makeEventSource()
519
+ };
520
+ this.events = {
521
+ didReceiveEvent: this.eventHub.didReceiveEvent.observable,
522
+ willTransition: this.eventHub.willTransition.observable,
523
+ didIgnoreEvent: this.eventHub.didIgnoreEvent.observable,
524
+ willExitState: this.eventHub.willExitState.observable,
525
+ didEnterState: this.eventHub.didEnterState.observable
526
+ };
527
+ }
528
+ get context() {
529
+ return this.currentContext.current;
530
+ }
531
+ /**
532
+ * Define an explicit finite state in the state machine.
533
+ */
534
+ addState(state) {
535
+ if (this.runningState !== 0 /* NOT_STARTED_YET */) {
536
+ throw new Error("Already started");
537
+ }
538
+ this.states.add(state);
539
+ return this;
540
+ }
541
+ onEnter(nameOrPattern, enterFn) {
542
+ if (this.runningState !== 0 /* NOT_STARTED_YET */) {
543
+ throw new Error("Already started");
544
+ } else if (this.enterFns.has(nameOrPattern)) {
545
+ throw new Error(
546
+ // TODO We _currently_ don't support multiple .onEnters() for the same
547
+ // state, but this is not a fundamental limitation. Just not
548
+ // implemented yet. If we wanted to, we could make this an array.
549
+ `enter/exit function for ${nameOrPattern} already exists`
550
+ );
551
+ }
552
+ this.enterFns.set(nameOrPattern, enterFn);
553
+ return this;
554
+ }
555
+ onEnterAsync(nameOrPattern, promiseFn, onOK, onError) {
556
+ return this.onEnter(nameOrPattern, () => {
557
+ let cancelled = false;
558
+ void promiseFn(this.currentContext.current).then(
559
+ // On OK
560
+ (data) => {
561
+ if (!cancelled) {
562
+ this.transition({ type: "ASYNC_OK", data }, onOK);
563
+ }
564
+ },
565
+ // On Error
566
+ (reason) => {
567
+ if (!cancelled) {
568
+ this.transition({ type: "ASYNC_ERROR", reason }, onError);
569
+ }
570
+ }
571
+ );
572
+ return () => {
573
+ cancelled = true;
574
+ };
575
+ });
576
+ }
577
+ getStatesMatching(nameOrPattern) {
578
+ const matches = [];
579
+ if (nameOrPattern === "*") {
580
+ for (const state of this.states) {
581
+ matches.push(state);
582
+ }
583
+ } else if (nameOrPattern.endsWith(".*")) {
584
+ const prefix = nameOrPattern.slice(0, -1);
585
+ for (const state of this.states) {
586
+ if (state.startsWith(prefix)) {
587
+ matches.push(state);
588
+ }
589
+ }
590
+ } else {
591
+ const name = nameOrPattern;
592
+ if (this.states.has(name)) {
593
+ matches.push(name);
594
+ }
595
+ }
596
+ if (matches.length === 0) {
597
+ throw new Error(`No states match ${JSON.stringify(nameOrPattern)}`);
598
+ }
599
+ return matches;
600
+ }
601
+ /**
602
+ * Define all allowed outgoing transitions for a state.
603
+ *
604
+ * The targets for each event can be defined as a function which returns the
605
+ * next state to transition to. These functions can look at the `event` or
606
+ * `context` params to conditionally decide which next state to transition
607
+ * to.
608
+ *
609
+ * If you set it to `null`, then the transition will be explicitly forbidden
610
+ * and throw an error. If you don't define a target for a transition, then
611
+ * such events will get ignored.
612
+ */
613
+ addTransitions(nameOrPattern, mapping) {
614
+ if (this.runningState !== 0 /* NOT_STARTED_YET */) {
615
+ throw new Error("Already started");
616
+ }
617
+ for (const srcState of this.getStatesMatching(nameOrPattern)) {
618
+ let map = this.allowedTransitions.get(srcState);
619
+ if (map === void 0) {
620
+ map = /* @__PURE__ */ new Map();
621
+ this.allowedTransitions.set(srcState, map);
622
+ }
623
+ for (const [type, target_] of Object.entries(mapping)) {
624
+ if (map.has(type)) {
625
+ throw new Error(
626
+ `Trying to set transition "${type}" on "${srcState}" (via "${nameOrPattern}"), but a transition already exists there.`
627
+ );
628
+ }
629
+ const target = target_;
630
+ this.knownEventTypes.add(type);
631
+ if (target !== void 0) {
632
+ const targetFn = typeof target === "function" ? target : () => target;
633
+ map.set(type, targetFn);
634
+ }
635
+ }
636
+ }
637
+ return this;
638
+ }
639
+ /**
640
+ * Like `.addTransition()`, but takes an (anonymous) transition whenever the
641
+ * timer fires.
642
+ *
643
+ * @param stateOrPattern The state name, or state group pattern name.
644
+ * @param after Number of milliseconds after which to take the
645
+ * transition. If in the mean time, another transition
646
+ * is taken, the timer will get cancelled.
647
+ * @param target The target state to go to.
648
+ */
649
+ addTimedTransition(stateOrPattern, after2, target) {
650
+ return this.onEnter(stateOrPattern, () => {
651
+ const ms = typeof after2 === "function" ? after2(this.currentContext.current) : after2;
652
+ const timeoutID = setTimeout(() => {
653
+ this.transition({ type: "TIMER" }, target);
654
+ }, ms);
655
+ return () => {
656
+ clearTimeout(timeoutID);
657
+ };
658
+ });
659
+ }
660
+ getTargetFn(eventName) {
661
+ var _a;
662
+ return (_a = this.allowedTransitions.get(this.currentState)) == null ? void 0 : _a.get(eventName);
663
+ }
664
+ /**
665
+ * Exits the current state, and executes any necessary cleanup functions.
666
+ * Call this before changing the current state to the next state.
667
+ *
668
+ * @param levels Defines how many "levels" of nesting will be exited. For
669
+ * example, if you transition from `foo.bar.qux` to `foo.bar.baz`, then
670
+ * the level is 1. But if you transition from `foo.bar.qux` to `bla.bla`,
671
+ * then the level is 3.
672
+ */
673
+ exit(levels) {
674
+ this.eventHub.willExitState.notify(this.currentState);
675
+ this.currentContext.allowPatching((patchableContext) => {
676
+ var _a;
677
+ levels = levels != null ? levels : this.cleanupStack.length;
678
+ for (let i = 0; i < levels; i++) {
679
+ (_a = this.cleanupStack.pop()) == null ? void 0 : _a(patchableContext);
680
+ }
681
+ });
682
+ }
683
+ /**
684
+ * Enters the current state, and executes any necessary onEnter handlers.
685
+ * Call this directly _after_ setting the current state to the next state.
686
+ */
687
+ enter(levels) {
688
+ const enterPatterns = patterns(
689
+ this.currentState,
690
+ levels != null ? levels : this.currentState.split(".").length + 1
691
+ );
692
+ this.currentContext.allowPatching((patchableContext) => {
693
+ for (const pattern of enterPatterns) {
694
+ const enterFn = this.enterFns.get(pattern);
695
+ const cleanupFn = enterFn == null ? void 0 : enterFn(patchableContext);
696
+ if (typeof cleanupFn === "function") {
697
+ this.cleanupStack.push(cleanupFn);
698
+ } else {
699
+ this.cleanupStack.push(null);
700
+ }
701
+ }
702
+ });
703
+ this.eventHub.didEnterState.notify(this.currentState);
704
+ }
705
+ /**
706
+ * Sends an event to the machine, which may cause an internal state
707
+ * transition to happen. When that happens, will trigger side effects.
708
+ */
709
+ send(event) {
710
+ const targetFn = this.getTargetFn(event.type);
711
+ if (targetFn !== void 0) {
712
+ return this.transition(event, targetFn);
713
+ }
714
+ if (!this.knownEventTypes.has(event.type)) {
715
+ throw new Error(`Invalid event ${JSON.stringify(event.type)}`);
716
+ } else {
717
+ this.eventHub.didIgnoreEvent.notify(event);
718
+ }
719
+ }
720
+ transition(event, target) {
721
+ this.eventHub.didReceiveEvent.notify(event);
722
+ const oldState = this.currentState;
723
+ const targetFn = typeof target === "function" ? target : () => target;
724
+ const nextTarget = targetFn(event, this.currentContext.current);
725
+ let nextState;
726
+ let effects = void 0;
727
+ if (nextTarget === null) {
728
+ this.eventHub.didIgnoreEvent.notify(event);
729
+ return;
730
+ }
731
+ if (typeof nextTarget === "string") {
732
+ nextState = nextTarget;
733
+ } else {
734
+ nextState = nextTarget.target;
735
+ effects = Array.isArray(nextTarget.effect) ? nextTarget.effect : [nextTarget.effect];
736
+ }
737
+ if (!this.states.has(nextState)) {
738
+ throw new Error(`Invalid next state name: ${JSON.stringify(nextState)}`);
739
+ }
740
+ this.eventHub.willTransition.notify({ from: oldState, to: nextState });
741
+ const [up, down] = distance(this.currentState, nextState);
742
+ if (up > 0) {
743
+ this.exit(up);
744
+ }
745
+ this.currentStateOrNull = nextState;
746
+ if (effects !== void 0) {
747
+ const effectsToRun = effects;
748
+ this.currentContext.allowPatching((patchableContext) => {
749
+ for (const effect of effectsToRun) {
750
+ if (typeof effect === "function") {
751
+ effect(patchableContext, event);
752
+ } else {
753
+ patchableContext.patch(effect);
754
+ }
755
+ }
756
+ });
757
+ }
758
+ if (down > 0) {
759
+ this.enter(down);
760
+ }
761
+ }
762
+ };
763
+
764
+ // src/lib/utils.ts
765
+ function isPlainObject(blob) {
766
+ return blob !== null && typeof blob === "object" && Object.prototype.toString.call(blob) === "[object Object]";
767
+ }
768
+ function fromEntries(iterable) {
769
+ const obj = {};
770
+ for (const [key, val] of iterable) {
771
+ obj[key] = val;
772
+ }
773
+ return obj;
774
+ }
775
+ function entries(obj) {
776
+ return Object.entries(obj);
777
+ }
778
+ function tryParseJson(rawMessage) {
779
+ try {
780
+ return JSON.parse(rawMessage);
781
+ } catch (e) {
782
+ return void 0;
783
+ }
784
+ }
785
+ function b64decode(b64value) {
786
+ try {
787
+ const formattedValue = b64value.replace(/-/g, "+").replace(/_/g, "/");
788
+ const decodedValue = decodeURIComponent(
789
+ atob(formattedValue).split("").map(function(c) {
790
+ return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
791
+ }).join("")
792
+ );
793
+ return decodedValue;
794
+ } catch (err) {
795
+ return atob(b64value);
796
+ }
797
+ }
798
+ function compact(items) {
799
+ return items.filter(
800
+ (item) => item !== null && item !== void 0
801
+ );
802
+ }
803
+ function compactObject(obj) {
804
+ const newObj = __spreadValues({}, obj);
805
+ Object.keys(obj).forEach((k) => {
806
+ const key = k;
807
+ if (newObj[key] === void 0) {
808
+ delete newObj[key];
809
+ }
810
+ });
811
+ return newObj;
812
+ }
813
+ function withTimeout(promise, millis, errmsg = "Timed out") {
814
+ return __async(this, null, function* () {
815
+ let timerID;
816
+ const timer$ = new Promise((_, reject) => {
817
+ timerID = setTimeout(() => {
818
+ reject(new Error(errmsg));
819
+ }, millis);
820
+ });
821
+ return Promise.race([promise, timer$]).finally(() => clearTimeout(timerID));
822
+ });
823
+ }
824
+
825
+ // src/connection.ts
826
+ function newToLegacyStatus(status) {
827
+ switch (status) {
828
+ case "connecting":
829
+ return "connecting";
830
+ case "connected":
831
+ return "open";
832
+ case "reconnecting":
833
+ return "unavailable";
834
+ case "disconnected":
835
+ return "failed";
836
+ case "initial":
837
+ return "closed";
838
+ default:
839
+ return "closed";
840
+ }
841
+ }
842
+ function toNewConnectionStatus(machine) {
843
+ const state = machine.currentState;
844
+ switch (state) {
845
+ case "@ok.connected":
846
+ case "@ok.awaiting-pong":
847
+ return "connected";
848
+ case "@idle.initial":
849
+ return "initial";
850
+ case "@auth.busy":
851
+ case "@auth.backoff":
852
+ case "@connecting.busy":
853
+ case "@connecting.backoff":
854
+ return machine.context.successCount > 0 ? "reconnecting" : "connecting";
855
+ case "@idle.failed":
856
+ return "disconnected";
857
+ default:
858
+ return assertNever(state, "Unknown state");
859
+ }
860
+ }
861
+ var BACKOFF_DELAYS = [250, 500, 1e3, 2e3, 4e3, 8e3, 1e4];
862
+ var RESET_DELAY = BACKOFF_DELAYS[0] - 1;
863
+ var BACKOFF_DELAYS_SLOW = [2e3, 3e4, 6e4, 3e5];
864
+ var HEARTBEAT_INTERVAL = 3e4;
865
+ var PONG_TIMEOUT = 2e3;
866
+ var AUTH_TIMEOUT = 1e4;
867
+ var SOCKET_CONNECT_TIMEOUT = 1e4;
868
+ var StopRetrying = class extends Error {
869
+ constructor(reason) {
870
+ super(reason);
871
+ }
872
+ };
873
+ var LiveblocksError = class extends Error {
874
+ constructor(message, code) {
875
+ super(message);
876
+ this.code = code;
877
+ }
878
+ };
879
+ function nextBackoffDelay(currentDelay, delays = BACKOFF_DELAYS) {
880
+ var _a;
881
+ return (_a = delays.find((delay) => delay > currentDelay)) != null ? _a : delays[delays.length - 1];
882
+ }
883
+ function increaseBackoffDelay(context) {
884
+ context.patch({ backoffDelay: nextBackoffDelay(context.backoffDelay) });
885
+ }
886
+ function increaseBackoffDelayAggressively(context) {
887
+ context.patch({
888
+ backoffDelay: nextBackoffDelay(context.backoffDelay, BACKOFF_DELAYS_SLOW)
889
+ });
890
+ }
891
+ function resetSuccessCount(context) {
892
+ context.patch({ successCount: 0 });
893
+ }
894
+ function log(level, message) {
895
+ const logger = level === 2 /* ERROR */ ? error : level === 1 /* WARN */ ? warn : (
896
+ /* black hole */
897
+ () => {
898
+ }
899
+ );
900
+ return () => {
901
+ logger(message);
902
+ };
903
+ }
904
+ function sendHeartbeat(ctx) {
905
+ var _a;
906
+ (_a = ctx.socket) == null ? void 0 : _a.send("ping");
907
+ }
908
+ function enableTracing(machine) {
909
+ const start = (/* @__PURE__ */ new Date()).getTime();
910
+ function log2(...args) {
911
+ warn(
912
+ `${(((/* @__PURE__ */ new Date()).getTime() - start) / 1e3).toFixed(2)} [FSM #${machine.id}]`,
913
+ ...args
914
+ );
915
+ }
916
+ const unsubs = [
917
+ machine.events.didReceiveEvent.subscribe((e) => log2(`Event ${e.type}`)),
918
+ machine.events.willTransition.subscribe(
919
+ ({ from, to }) => log2("Transitioning", from, "\u2192", to)
920
+ ),
921
+ machine.events.didIgnoreEvent.subscribe(
922
+ (e) => log2("Ignored event", e.type, e, "(current state won't handle it)")
923
+ )
924
+ // machine.events.willExitState.subscribe((s) => log("Exiting state", s)),
925
+ // machine.events.didEnterState.subscribe((s) => log("Entering state", s)),
926
+ ];
927
+ return () => {
928
+ for (const unsub of unsubs) {
929
+ unsub();
930
+ }
931
+ };
932
+ }
933
+ function defineConnectivityEvents(machine) {
934
+ const statusDidChange = makeEventSource();
935
+ const didConnect = makeEventSource();
936
+ const didDisconnect = makeEventSource();
937
+ let lastStatus = null;
938
+ const unsubscribe = machine.events.didEnterState.subscribe(() => {
939
+ const currStatus = toNewConnectionStatus(machine);
940
+ if (currStatus !== lastStatus) {
941
+ statusDidChange.notify(currStatus);
942
+ }
943
+ if (lastStatus === "connected" && currStatus !== "connected") {
944
+ didDisconnect.notify();
945
+ } else if (lastStatus !== "connected" && currStatus === "connected") {
946
+ didConnect.notify();
947
+ }
948
+ lastStatus = currStatus;
949
+ });
950
+ return {
951
+ statusDidChange: statusDidChange.observable,
952
+ didConnect: didConnect.observable,
953
+ didDisconnect: didDisconnect.observable,
954
+ unsubscribe
955
+ };
956
+ }
957
+ var assign = (patch) => (ctx) => ctx.patch(patch);
958
+ function createConnectionStateMachine(delegates, enableDebugLogging) {
959
+ const onMessage = makeEventSource();
960
+ onMessage.pause();
961
+ const onLiveblocksError = makeEventSource();
962
+ const initialContext = {
963
+ successCount: 0,
964
+ token: null,
965
+ socket: null,
966
+ backoffDelay: RESET_DELAY
967
+ };
968
+ const machine = 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");
969
+ machine.addTransitions("*", {
970
+ RECONNECT: {
971
+ target: "@auth.backoff",
972
+ effect: [increaseBackoffDelay, resetSuccessCount]
973
+ },
974
+ DISCONNECT: "@idle.initial"
975
+ });
976
+ machine.onEnter("@idle.*", resetSuccessCount).addTransitions("@idle.*", {
977
+ CONNECT: (_, ctx) => (
978
+ // If we still have a known token, try to reconnect to the socket directly,
979
+ // otherwise, try to obtain a new token
980
+ ctx.token !== null ? "@connecting.busy" : "@auth.busy"
981
+ )
982
+ });
983
+ machine.addTransitions("@auth.backoff", {
984
+ NAVIGATOR_ONLINE: {
985
+ target: "@auth.busy",
986
+ effect: assign({ backoffDelay: RESET_DELAY })
987
+ }
988
+ }).addTimedTransition(
989
+ "@auth.backoff",
990
+ (ctx) => ctx.backoffDelay,
991
+ "@auth.busy"
992
+ ).onEnterAsync(
993
+ "@auth.busy",
994
+ () => withTimeout(delegates.authenticate(), AUTH_TIMEOUT),
995
+ // On successful authentication
996
+ (okEvent) => ({
997
+ target: "@connecting.busy",
998
+ effect: assign({
999
+ token: okEvent.data,
1000
+ backoffDelay: RESET_DELAY
1001
+ })
1002
+ }),
1003
+ // Auth failed
1004
+ (failedEvent) => {
1005
+ if (failedEvent.reason instanceof StopRetrying) {
1006
+ return {
1007
+ target: "@idle.failed",
1008
+ effect: log(2 /* ERROR */, failedEvent.reason.message)
1009
+ };
1010
+ }
1011
+ return {
1012
+ target: "@auth.backoff",
1013
+ effect: [
1014
+ increaseBackoffDelay,
1015
+ log(
1016
+ 2 /* ERROR */,
1017
+ `Authentication failed: ${failedEvent.reason instanceof Error ? failedEvent.reason.message : String(failedEvent.reason)}`
1018
+ )
1019
+ ]
1020
+ };
1021
+ }
1022
+ );
1023
+ const onSocketError = (event) => machine.send({ type: "EXPLICIT_SOCKET_ERROR", event });
1024
+ const onSocketClose = (event) => machine.send({ type: "EXPLICIT_SOCKET_CLOSE", event });
1025
+ const onSocketMessage = (event) => event.data === "pong" ? machine.send({ type: "PONG" }) : onMessage.notify(event);
1026
+ function teardownSocket(socket) {
1027
+ if (socket) {
1028
+ socket.removeEventListener("error", onSocketError);
1029
+ socket.removeEventListener("close", onSocketClose);
1030
+ socket.removeEventListener("message", onSocketMessage);
1031
+ socket.close();
1032
+ }
1033
+ }
1034
+ machine.addTransitions("@connecting.backoff", {
1035
+ NAVIGATOR_ONLINE: {
1036
+ target: "@connecting.busy",
1037
+ effect: assign({ backoffDelay: RESET_DELAY })
1038
+ }
1039
+ }).addTimedTransition(
1040
+ "@connecting.backoff",
1041
+ (ctx) => ctx.backoffDelay,
1042
+ "@connecting.busy"
1043
+ ).onEnterAsync(
1044
+ "@connecting.busy",
1045
+ //
1046
+ // Use the "createSocket" delegate function (provided to the
1047
+ // ManagedSocket) to create the actual WebSocket connection instance.
1048
+ // Then, set up all the necessary event listeners, and wait for the
1049
+ // "open" event to occur.
1050
+ //
1051
+ // When the "open" event happens, we're ready to transition to the
1052
+ // OK state. This is done by resolving the Promise.
1053
+ //
1054
+ (ctx) => __async(this, null, function* () {
1055
+ let capturedPrematureEvent = null;
1056
+ const connect$ = new Promise(
1057
+ (resolve, rej) => {
1058
+ if (ctx.token === null) {
1059
+ throw new Error("No auth token");
1060
+ }
1061
+ const socket = delegates.createSocket(ctx.token);
1062
+ function reject(event) {
1063
+ capturedPrematureEvent = event;
1064
+ socket.removeEventListener("message", onSocketMessage);
1065
+ rej(event);
1066
+ }
1067
+ socket.addEventListener("message", onSocketMessage);
1068
+ socket.addEventListener("error", reject);
1069
+ socket.addEventListener("close", reject);
1070
+ socket.addEventListener("open", () => {
1071
+ socket.addEventListener("error", onSocketError);
1072
+ socket.addEventListener("close", onSocketClose);
1073
+ const unsub = () => {
1074
+ socket.removeEventListener("error", reject);
1075
+ socket.removeEventListener("close", reject);
1076
+ };
1077
+ resolve([socket, unsub]);
1078
+ });
1079
+ }
1080
+ );
1081
+ return withTimeout(connect$, SOCKET_CONNECT_TIMEOUT).then(
1082
+ //
1083
+ // Part 3:
1084
+ // By now, our "open" event has fired, and the promise has been
1085
+ // resolved. Two possible scenarios:
1086
+ //
1087
+ // 1. The happy path. Most likely.
1088
+ // 2. Uh-oh. A premature close/error event has been observed. Let's
1089
+ // reject the promise after all.
1090
+ //
1091
+ // Any close/error event that will get scheduled after this point
1092
+ // onwards, will be caught in the OK state, and dealt with
1093
+ // accordingly.
1094
+ //
1095
+ ([socket, unsub]) => {
1096
+ unsub();
1097
+ if (capturedPrematureEvent) {
1098
+ throw capturedPrematureEvent;
1099
+ }
1100
+ return socket;
1101
+ }
1102
+ );
1103
+ }),
1104
+ // Only transition to OK state after a successfully opened WebSocket connection
1105
+ (okEvent) => ({
1106
+ target: "@ok.connected",
1107
+ effect: assign({
1108
+ socket: okEvent.data,
1109
+ backoffDelay: RESET_DELAY
1110
+ })
1111
+ }),
1112
+ // If the WebSocket connection cannot be established
1113
+ (failure) => {
1114
+ const err = failure.reason;
1115
+ if (err instanceof StopRetrying) {
1116
+ return {
1117
+ target: "@idle.failed",
1118
+ effect: log(2 /* ERROR */, err.message)
1119
+ };
1120
+ }
1121
+ return {
1122
+ target: "@auth.backoff",
1123
+ effect: [
1124
+ // Increase the backoff delay conditionally
1125
+ // TODO: This is ugly. DRY this up with the other code 40xx checks elsewhere.
1126
+ !(err instanceof Error) && err.type === "close" && err.code >= 4e3 && err.code <= 4100 ? increaseBackoffDelayAggressively : increaseBackoffDelay,
1127
+ // Produce a useful log message
1128
+ (ctx) => {
1129
+ if (err instanceof Error) {
1130
+ warn(String(err));
1131
+ } else {
1132
+ warn(
1133
+ err.type === "close" ? `Connection to Liveblocks websocket server closed prematurely (code: ${err.code}). Retrying in ${ctx.backoffDelay}ms.` : "Connection to Liveblocks websocket server could not be established."
1134
+ );
1135
+ }
1136
+ }
1137
+ ]
1138
+ };
1139
+ }
1140
+ );
1141
+ machine.addTimedTransition("@ok.connected", HEARTBEAT_INTERVAL, {
1142
+ target: "@ok.awaiting-pong",
1143
+ effect: sendHeartbeat
1144
+ }).addTransitions("@ok.connected", {
1145
+ WINDOW_GOT_FOCUS: { target: "@ok.awaiting-pong", effect: sendHeartbeat }
1146
+ });
1147
+ const noPongAction = {
1148
+ target: "@connecting.busy",
1149
+ // Log implicit connection loss and drop the current open socket
1150
+ effect: log(
1151
+ 1 /* WARN */,
1152
+ "Received no pong from server, assume implicit connection loss."
1153
+ )
1154
+ };
1155
+ machine.onEnter("@ok.*", (ctx) => {
1156
+ ctx.patch({ successCount: ctx.successCount + 1 });
1157
+ const timerID = setTimeout(
1158
+ // On the next tick, start delivering all messages that have already
1159
+ // been received, and continue synchronous delivery of all future
1160
+ // incoming messages.
1161
+ onMessage.unpause,
1162
+ 0
1163
+ );
1164
+ return (ctx2) => {
1165
+ teardownSocket(ctx2.socket);
1166
+ ctx2.patch({ socket: null });
1167
+ clearTimeout(timerID);
1168
+ onMessage.pause();
1169
+ };
1170
+ }).addTimedTransition("@ok.awaiting-pong", PONG_TIMEOUT, noPongAction).addTransitions("@ok.awaiting-pong", { PONG_TIMEOUT: noPongAction }).addTransitions("@ok.awaiting-pong", { PONG: "@ok.connected" }).addTransitions("@ok.*", {
1171
+ // When a socket receives an error, this can cause the closing of the
1172
+ // socket, or not. So always check to see if the socket is still OPEN or
1173
+ // not. When still OPEN, don't transition.
1174
+ EXPLICIT_SOCKET_ERROR: (_, context) => {
1175
+ var _a;
1176
+ if (((_a = context.socket) == null ? void 0 : _a.readyState) === 1) {
1177
+ return null;
1178
+ }
1179
+ return {
1180
+ target: "@connecting.backoff",
1181
+ effect: increaseBackoffDelay
1182
+ };
1183
+ },
1184
+ EXPLICIT_SOCKET_CLOSE: (e) => {
1185
+ if (e.event.code === 4999) {
1186
+ return {
1187
+ target: "@idle.failed",
1188
+ effect: log(
1189
+ 1 /* WARN */,
1190
+ "Connection to WebSocket closed permanently. Won't retry."
1191
+ )
1192
+ };
1193
+ }
1194
+ if (e.event.code >= 4e3 && e.event.code <= 4100) {
1195
+ return {
1196
+ target: "@connecting.backoff",
1197
+ effect: [
1198
+ increaseBackoffDelayAggressively,
1199
+ (ctx) => warn(
1200
+ `Connection to Liveblocks websocket server closed (code: ${e.event.code}). Retrying in ${ctx.backoffDelay}ms.`
1201
+ ),
1202
+ (_, { event }) => {
1203
+ if (event.code >= 4e3 && event.code <= 4100) {
1204
+ const err = new LiveblocksError(event.reason, event.code);
1205
+ onLiveblocksError.notify(err);
1206
+ }
1207
+ }
1208
+ ]
1209
+ };
1210
+ }
1211
+ return {
1212
+ target: "@connecting.backoff",
1213
+ effect: [
1214
+ increaseBackoffDelay,
1215
+ (ctx) => warn(
1216
+ `Connection to Liveblocks websocket server closed (code: ${e.event.code}). Retrying in ${ctx.backoffDelay}ms.`
1217
+ )
1218
+ ]
1219
+ };
1220
+ }
1221
+ });
1222
+ if (typeof document !== "undefined") {
1223
+ const doc = typeof document !== "undefined" ? document : void 0;
1224
+ const win = typeof window !== "undefined" ? window : void 0;
1225
+ const root = win != null ? win : doc;
1226
+ machine.onEnter("*", (ctx) => {
1227
+ function onBackOnline() {
1228
+ machine.send({ type: "NAVIGATOR_ONLINE" });
1229
+ }
1230
+ function onVisibilityChange() {
1231
+ if ((doc == null ? void 0 : doc.visibilityState) === "visible") {
1232
+ machine.send({ type: "WINDOW_GOT_FOCUS" });
1233
+ }
1234
+ }
1235
+ win == null ? void 0 : win.addEventListener("online", onBackOnline);
1236
+ root == null ? void 0 : root.addEventListener("visibilitychange", onVisibilityChange);
1237
+ return () => {
1238
+ root == null ? void 0 : root.removeEventListener("visibilitychange", onVisibilityChange);
1239
+ win == null ? void 0 : win.removeEventListener("online", onBackOnline);
1240
+ teardownSocket(ctx.socket);
1241
+ };
1242
+ });
1243
+ }
1244
+ const cleanups = [];
1245
+ const { statusDidChange, didConnect, didDisconnect, unsubscribe } = defineConnectivityEvents(machine);
1246
+ cleanups.push(unsubscribe);
1247
+ if (enableDebugLogging) {
1248
+ cleanups.push(enableTracing(machine));
1249
+ }
1250
+ machine.start();
1251
+ return {
1252
+ machine,
1253
+ cleanups,
1254
+ // Observable events that will be emitted by this machine
1255
+ events: {
1256
+ statusDidChange,
1257
+ didConnect,
1258
+ didDisconnect,
1259
+ onMessage: onMessage.observable,
1260
+ onLiveblocksError: onLiveblocksError.observable
1261
+ }
1262
+ };
1263
+ }
1264
+ var ManagedSocket = class {
1265
+ constructor(delegates, enableDebugLogging = false) {
1266
+ const { machine, events, cleanups } = createConnectionStateMachine(
1267
+ delegates,
1268
+ enableDebugLogging
1269
+ );
1270
+ this.machine = machine;
1271
+ this.events = events;
1272
+ this.cleanups = cleanups;
1273
+ }
1274
+ getLegacyStatus() {
1275
+ return newToLegacyStatus(this.getStatus());
1276
+ }
1277
+ getStatus() {
1278
+ try {
1279
+ return toNewConnectionStatus(this.machine);
1280
+ } catch (e) {
1281
+ return "initial";
1282
+ }
1283
+ }
1284
+ /**
1285
+ * Returns the current auth token.
1286
+ */
1287
+ get token() {
1288
+ return this.machine.context.token;
1289
+ }
1290
+ /**
1291
+ * Call this method to try to connect to a WebSocket. This only has an effect
1292
+ * if the machine is idle at the moment, otherwise this is a no-op.
1293
+ */
1294
+ connect() {
1295
+ this.machine.send({ type: "CONNECT" });
1296
+ }
1297
+ /**
1298
+ * If idle, will try to connect. Otherwise, it will attempt to reconnect to
1299
+ * the socket, potentially obtaining a new token first, if needed.
1300
+ */
1301
+ reconnect() {
1302
+ this.machine.send({ type: "RECONNECT" });
1303
+ }
1304
+ /**
1305
+ * Call this method to disconnect from the current WebSocket. Is going to be
1306
+ * a no-op if there is no active connection.
1307
+ */
1308
+ disconnect() {
1309
+ this.machine.send({ type: "DISCONNECT" });
1310
+ }
1311
+ /**
1312
+ * Call this to stop the machine and run necessary cleanup functions. After
1313
+ * calling destroy(), you can no longer use this instance. Call this before
1314
+ * letting the instance get garbage collected.
1315
+ */
1316
+ destroy() {
1317
+ this.machine.stop();
1318
+ let cleanup;
1319
+ while (cleanup = this.cleanups.pop()) {
1320
+ cleanup();
1321
+ }
1322
+ }
1323
+ /**
1324
+ * Safely send a message to the current WebSocket connection. Will emit a log
1325
+ * message if this is somehow impossible.
1326
+ */
1327
+ send(data) {
1328
+ var _a;
1329
+ const socket = (_a = this.machine.context) == null ? void 0 : _a.socket;
1330
+ if (socket === null) {
1331
+ warn("Cannot send: not connected yet", data);
1332
+ } else if (socket.readyState !== 1) {
1333
+ warn("Cannot send: WebSocket no longer open", data);
1334
+ } else {
1335
+ socket.send(data);
1336
+ }
1337
+ }
1338
+ /**
1339
+ * NOTE: Used by the E2E app only, to simulate explicit events.
1340
+ * Not ideal to keep exposed :(
1341
+ */
1342
+ _privateSendMachineEvent(event) {
1343
+ this.machine.send(event);
1344
+ }
1345
+ };
1346
+
348
1347
  // src/lib/position.ts
349
1348
  var MIN_CODE = 32;
350
1349
  var MAX_CODE = 126;
@@ -626,85 +1625,35 @@ var AbstractCrdt = class {
626
1625
  */
627
1626
  invalidate() {
628
1627
  if (this._cachedImmutable !== void 0 || this._cachedTreeNode !== void 0) {
629
- this._cachedImmutable = void 0;
630
- this._cachedTreeNode = void 0;
631
- if (this.parent.type === "HasParent") {
632
- this.parent.node.invalidate();
633
- }
634
- }
635
- }
636
- /**
637
- * @internal
638
- *
639
- * Return an snapshot of this Live tree for use in DevTools.
640
- */
641
- toTreeNode(key) {
642
- if (this._cachedTreeNode === void 0 || this._cachedTreeNodeKey !== key) {
643
- this._cachedTreeNodeKey = key;
644
- this._cachedTreeNode = this._toTreeNode(key);
645
- }
646
- return this._cachedTreeNode;
647
- }
648
- /**
649
- * Return an immutable snapshot of this Live node and its children.
650
- */
651
- toImmutable() {
652
- if (this._cachedImmutable === void 0) {
653
- this._cachedImmutable = this._toImmutable();
654
- }
655
- return this._cachedImmutable;
656
- }
657
- };
658
-
659
- // src/lib/utils.ts
660
- function isPlainObject(blob) {
661
- return blob !== null && typeof blob === "object" && Object.prototype.toString.call(blob) === "[object Object]";
662
- }
663
- function fromEntries(iterable) {
664
- const obj = {};
665
- for (const [key, val] of iterable) {
666
- obj[key] = val;
667
- }
668
- return obj;
669
- }
670
- function entries(obj) {
671
- return Object.entries(obj);
672
- }
673
- function tryParseJson(rawMessage) {
674
- try {
675
- return JSON.parse(rawMessage);
676
- } catch (e) {
677
- return void 0;
678
- }
679
- }
680
- function b64decode(b64value) {
681
- try {
682
- const formattedValue = b64value.replace(/-/g, "+").replace(/_/g, "/");
683
- const decodedValue = decodeURIComponent(
684
- atob(formattedValue).split("").map(function(c) {
685
- return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
686
- }).join("")
687
- );
688
- return decodedValue;
689
- } catch (err) {
690
- return atob(b64value);
1628
+ this._cachedImmutable = void 0;
1629
+ this._cachedTreeNode = void 0;
1630
+ if (this.parent.type === "HasParent") {
1631
+ this.parent.node.invalidate();
1632
+ }
1633
+ }
691
1634
  }
692
- }
693
- function compact(items) {
694
- return items.filter(
695
- (item) => item !== null && item !== void 0
696
- );
697
- }
698
- function compactObject(obj) {
699
- const newObj = __spreadValues({}, obj);
700
- Object.keys(obj).forEach((k) => {
701
- const key = k;
702
- if (newObj[key] === void 0) {
703
- delete newObj[key];
1635
+ /**
1636
+ * @internal
1637
+ *
1638
+ * Return an snapshot of this Live tree for use in DevTools.
1639
+ */
1640
+ toTreeNode(key) {
1641
+ if (this._cachedTreeNode === void 0 || this._cachedTreeNodeKey !== key) {
1642
+ this._cachedTreeNodeKey = key;
1643
+ this._cachedTreeNode = this._toTreeNode(key);
704
1644
  }
705
- });
706
- return newObj;
707
- }
1645
+ return this._cachedTreeNode;
1646
+ }
1647
+ /**
1648
+ * Return an immutable snapshot of this Live node and its children.
1649
+ */
1650
+ toImmutable() {
1651
+ if (this._cachedImmutable === void 0) {
1652
+ this._cachedImmutable = this._toImmutable();
1653
+ }
1654
+ return this._cachedImmutable;
1655
+ }
1656
+ };
708
1657
 
709
1658
  // src/protocol/SerializedCrdt.ts
710
1659
  var CrdtType = /* @__PURE__ */ ((CrdtType2) => {
@@ -3239,35 +4188,12 @@ var DerivedRef = class extends ImmutableRef {
3239
4188
  }
3240
4189
  };
3241
4190
 
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
4191
  // 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
4192
  var MAX_MESSAGE_SIZE = 1024 * 1024 - 128;
3261
4193
  function makeIdFactory(connectionId) {
3262
4194
  let count = 0;
3263
4195
  return () => `${connectionId}:${count++}`;
3264
4196
  }
3265
- function log(..._params) {
3266
- return;
3267
- }
3268
- function isConnectionSelfAware(connection) {
3269
- return connection.status === "open" || connection.status === "connecting";
3270
- }
3271
4197
  function userToTreeNode(key, user) {
3272
4198
  return {
3273
4199
  type: "User",
@@ -3277,21 +4203,27 @@ function userToTreeNode(key, user) {
3277
4203
  };
3278
4204
  }
3279
4205
  function createRoom(options, config) {
3280
- var _a;
4206
+ var _a, _b, _c, _d;
3281
4207
  const initialPresence = typeof options.initialPresence === "function" ? options.initialPresence(config.roomId) : options.initialPresence;
3282
4208
  const initialStorage = typeof options.initialStorage === "function" ? options.initialStorage(config.roomId) : options.initialStorage;
4209
+ const delegates = (_c = config.delegates) != null ? _c : {
4210
+ authenticate: makeAuthDelegateForRoom(
4211
+ config.roomId,
4212
+ config.authentication,
4213
+ (_a = config.polyfills) == null ? void 0 : _a.fetch
4214
+ ),
4215
+ createSocket: makeCreateSocketDelegateForRoom(
4216
+ config.liveblocksServer,
4217
+ (_b = config.polyfills) == null ? void 0 : _b.WebSocket
4218
+ )
4219
+ };
4220
+ const managedSocket = new ManagedSocket(
4221
+ delegates,
4222
+ config.enableDebugLogging
4223
+ );
3283
4224
  const context = {
3284
- token: null,
3285
- lastConnectionId: null,
3286
- socket: null,
3287
- numRetries: 0,
3288
- timers: {
3289
- flush: void 0,
3290
- reconnect: void 0,
3291
- heartbeat: void 0,
3292
- pongTimeout: void 0
3293
- },
3294
4225
  buffer: {
4226
+ flushTimerID: void 0,
3295
4227
  lastFlushedAt: 0,
3296
4228
  me: (
3297
4229
  // Queue up the initial presence message as a Full Presence™ update
@@ -3303,7 +4235,7 @@ function createRoom(options, config) {
3303
4235
  messages: [],
3304
4236
  storageOperations: []
3305
4237
  },
3306
- connection: new ValueRef({ status: "closed" }),
4238
+ sessionInfo: new ValueRef(null),
3307
4239
  me: new MeRef(initialPresence),
3308
4240
  others: new OthersRef(),
3309
4241
  initialStorage,
@@ -3322,7 +4254,91 @@ function createRoom(options, config) {
3322
4254
  opStackTraces: process.env.NODE_ENV !== "production" ? /* @__PURE__ */ new Map() : void 0
3323
4255
  };
3324
4256
  const doNotBatchUpdates = (cb) => cb();
3325
- const batchUpdates = (_a = config.unstable_batchedUpdates) != null ? _a : doNotBatchUpdates;
4257
+ const batchUpdates = (_d = config.unstable_batchedUpdates) != null ? _d : doNotBatchUpdates;
4258
+ let lastToken;
4259
+ function onStatusDidChange(newStatus) {
4260
+ var _a2;
4261
+ const token = (_a2 = managedSocket.token) == null ? void 0 : _a2.parsed;
4262
+ if (token !== void 0 && token !== lastToken) {
4263
+ context.sessionInfo.set({
4264
+ id: token.actor,
4265
+ userInfo: token.info,
4266
+ userId: token.id,
4267
+ isReadOnly: isStorageReadOnly(token.scopes)
4268
+ });
4269
+ lastToken = token;
4270
+ }
4271
+ batchUpdates(() => {
4272
+ eventHub.status.notify(newStatus);
4273
+ eventHub.connection.notify(newToLegacyStatus(newStatus));
4274
+ });
4275
+ }
4276
+ let _connectionLossTimerId;
4277
+ let _hasLostConnection = false;
4278
+ function handleConnectionLossEvent(newStatus) {
4279
+ if (newStatus === "reconnecting") {
4280
+ _connectionLossTimerId = setTimeout(() => {
4281
+ batchUpdates(() => {
4282
+ eventHub.lostConnection.notify("lost");
4283
+ _hasLostConnection = true;
4284
+ context.others.clearOthers();
4285
+ notify({ others: [{ type: "reset" }] }, doNotBatchUpdates);
4286
+ });
4287
+ }, config.lostConnectionTimeout);
4288
+ } else {
4289
+ clearTimeout(_connectionLossTimerId);
4290
+ if (_hasLostConnection) {
4291
+ if (newStatus === "disconnected") {
4292
+ batchUpdates(() => {
4293
+ eventHub.lostConnection.notify("failed");
4294
+ });
4295
+ } else {
4296
+ batchUpdates(() => {
4297
+ eventHub.lostConnection.notify("restored");
4298
+ });
4299
+ }
4300
+ _hasLostConnection = false;
4301
+ }
4302
+ }
4303
+ }
4304
+ function onDidConnect() {
4305
+ const sessionInfo = context.sessionInfo.current;
4306
+ if (sessionInfo === null) {
4307
+ throw new Error("Unexpected missing session info");
4308
+ }
4309
+ context.buffer.me = {
4310
+ type: "full",
4311
+ data: (
4312
+ // Because context.me.current is a readonly object, we'll have to
4313
+ // make a copy here. Otherwise, type errors happen later when
4314
+ // "patching" my presence.
4315
+ __spreadValues({}, context.me.current)
4316
+ )
4317
+ };
4318
+ context.idFactory = makeIdFactory(sessionInfo.id);
4319
+ if (context.root) {
4320
+ context.buffer.messages.push({ type: 200 /* FETCH_STORAGE */ });
4321
+ }
4322
+ tryFlushing();
4323
+ }
4324
+ function onDidDisconnect() {
4325
+ clearTimeout(context.buffer.flushTimerID);
4326
+ }
4327
+ managedSocket.events.onMessage.subscribe(handleServerMessage);
4328
+ managedSocket.events.statusDidChange.subscribe(onStatusDidChange);
4329
+ managedSocket.events.statusDidChange.subscribe(handleConnectionLossEvent);
4330
+ managedSocket.events.didConnect.subscribe(onDidConnect);
4331
+ managedSocket.events.didDisconnect.subscribe(onDidDisconnect);
4332
+ managedSocket.events.onLiveblocksError.subscribe((err) => {
4333
+ batchUpdates(() => {
4334
+ if (process.env.NODE_ENV !== "production") {
4335
+ error(
4336
+ `Connection to websocket server closed. Reason: ${err.message} (code: ${err.code}).`
4337
+ );
4338
+ }
4339
+ eventHub.error.notify(err);
4340
+ });
4341
+ });
3326
4342
  const pool = {
3327
4343
  roomId: config.roomId,
3328
4344
  getNode: (id) => context.nodes.get(id),
@@ -3364,7 +4380,8 @@ function createRoom(options, config) {
3364
4380
  }
3365
4381
  },
3366
4382
  assertStorageIsWritable: () => {
3367
- if (isConnectionSelfAware(context.connection.current) && context.connection.current.isReadOnly) {
4383
+ var _a2;
4384
+ if ((_a2 = context.sessionInfo.current) == null ? void 0 : _a2.isReadOnly) {
3368
4385
  throw new Error(
3369
4386
  "Cannot write to storage with a read only user, please ensure the user has write permissions"
3370
4387
  );
@@ -3372,81 +4389,55 @@ function createRoom(options, config) {
3372
4389
  }
3373
4390
  };
3374
4391
  const eventHub = {
4392
+ connection: makeEventSource(),
4393
+ // Old/deprecated API
4394
+ status: makeEventSource(),
4395
+ // New/recommended API
4396
+ lostConnection: makeEventSource(),
3375
4397
  customEvent: makeEventSource(),
3376
4398
  me: makeEventSource(),
3377
4399
  others: makeEventSource(),
3378
4400
  error: makeEventSource(),
3379
- connection: makeEventSource(),
3380
4401
  storage: makeEventSource(),
3381
4402
  history: makeEventSource(),
3382
4403
  storageDidLoad: makeEventSource(),
3383
4404
  storageStatus: makeEventSource()
3384
4405
  };
3385
- const effects = config.mockedEffects || {
3386
- authenticateAndConnect(auth, createWebSocket) {
3387
- const prevToken = context.token;
3388
- if (prevToken !== null && !isTokenExpired(prevToken.parsed)) {
3389
- const socket = createWebSocket(prevToken);
3390
- handleAuthSuccess(prevToken.parsed, socket);
3391
- return void 0;
3392
- } else {
3393
- void auth().then((token) => {
3394
- if (context.connection.current.status !== "authenticating") {
3395
- return;
3396
- }
3397
- const socket = createWebSocket(token);
3398
- handleAuthSuccess(token.parsed, socket);
3399
- context.token = token;
3400
- }).catch(
3401
- (er) => authenticationFailure(
3402
- er instanceof Error ? er : new Error(String(er))
3403
- )
3404
- );
3405
- return void 0;
3406
- }
3407
- },
3408
- send(messageOrMessages) {
3409
- var _a2, _b;
3410
- if (context.socket === null) {
3411
- throw new Error("Can't send message if socket is null");
3412
- }
3413
- if (context.socket.readyState === context.socket.OPEN) {
3414
- const message = JSON.stringify(messageOrMessages);
3415
- if (config.unstable_fallbackToHTTP) {
3416
- const size = new TextEncoder().encode(message).length;
3417
- if (size > MAX_MESSAGE_SIZE && ((_a2 = context.token) == null ? void 0 : _a2.raw) && config.httpSendEndpoint) {
3418
- if (isTokenExpired(context.token.parsed)) {
3419
- return reconnect();
3420
- }
3421
- void httpSend(
3422
- message,
3423
- context.token.raw,
3424
- config.httpSendEndpoint,
3425
- (_b = config.polyfills) == null ? void 0 : _b.fetch
3426
- );
3427
- warn(
3428
- "Message was too large for websockets and sent over HTTP instead"
3429
- );
3430
- return;
3431
- }
4406
+ function sendMessages(messageOrMessages) {
4407
+ var _a2, _b2;
4408
+ const message = JSON.stringify(messageOrMessages);
4409
+ if (config.unstable_fallbackToHTTP) {
4410
+ const size = new TextEncoder().encode(message).length;
4411
+ if (size > MAX_MESSAGE_SIZE && ((_a2 = managedSocket.token) == null ? void 0 : _a2.raw) && config.httpSendEndpoint) {
4412
+ if (isTokenExpired(managedSocket.token.parsed)) {
4413
+ return managedSocket.reconnect();
3432
4414
  }
3433
- context.socket.send(message);
4415
+ void httpSend(
4416
+ message,
4417
+ managedSocket.token.raw,
4418
+ config.httpSendEndpoint,
4419
+ (_b2 = config.polyfills) == null ? void 0 : _b2.fetch
4420
+ );
4421
+ warn(
4422
+ "Message was too large for websockets and sent over HTTP instead"
4423
+ );
4424
+ return;
3434
4425
  }
3435
- },
3436
- scheduleReconnect: (delay) => setTimeout(handleConnect, delay),
3437
- startHeartbeatInterval: () => setInterval(heartbeat, HEARTBEAT_INTERVAL),
3438
- schedulePongTimeout: () => setTimeout(pongTimeout, PONG_TIMEOUT)
3439
- };
4426
+ }
4427
+ managedSocket.send(message);
4428
+ }
3440
4429
  const self = new DerivedRef(
3441
- context.connection,
4430
+ context.sessionInfo,
3442
4431
  context.me,
3443
- (conn, me) => isConnectionSelfAware(conn) ? {
3444
- connectionId: conn.id,
3445
- id: conn.userId,
3446
- info: conn.userInfo,
3447
- presence: me,
3448
- isReadOnly: conn.isReadOnly
3449
- } : null
4432
+ (info, me) => {
4433
+ return info !== null ? {
4434
+ connectionId: info.id,
4435
+ id: info.userId,
4436
+ info: info.userInfo,
4437
+ presence: me,
4438
+ isReadOnly: info.isReadOnly
4439
+ } : null;
4440
+ }
3450
4441
  );
3451
4442
  const selfAsTreeNode = new DerivedRef(
3452
4443
  self,
@@ -3515,11 +4506,9 @@ function createRoom(options, config) {
3515
4506
  });
3516
4507
  }
3517
4508
  function getConnectionId() {
3518
- const conn = context.connection.current;
3519
- if (isConnectionSelfAware(conn)) {
3520
- return conn.id;
3521
- } else if (context.lastConnectionId !== null) {
3522
- return context.lastConnectionId;
4509
+ const info = context.sessionInfo.current;
4510
+ if (info) {
4511
+ return info.id;
3523
4512
  }
3524
4513
  throw new Error(
3525
4514
  "Internal. Tried to get connection id but connection was never open"
@@ -3642,23 +4631,6 @@ function createRoom(options, config) {
3642
4631
  }
3643
4632
  }
3644
4633
  }
3645
- function handleConnect() {
3646
- var _a2, _b;
3647
- if (context.connection.current.status !== "closed" && context.connection.current.status !== "unavailable") {
3648
- return;
3649
- }
3650
- const auth = prepareAuthEndpoint(
3651
- config.roomId,
3652
- config.authentication,
3653
- (_a2 = config.polyfills) == null ? void 0 : _a2.fetch
3654
- );
3655
- const createWebSocket = prepareCreateWebSocket(
3656
- config.liveblocksServer,
3657
- (_b = config.polyfills) == null ? void 0 : _b.WebSocket
3658
- );
3659
- updateConnection({ status: "authenticating" }, batchUpdates);
3660
- effects.authenticateAndConnect(auth, createWebSocket);
3661
- }
3662
4634
  function updatePresence(patch, options2) {
3663
4635
  const oldValues = {};
3664
4636
  if (context.buffer.me === null) {
@@ -3700,40 +4672,6 @@ function createRoom(options, config) {
3700
4672
  function isStorageReadOnly(scopes) {
3701
4673
  return scopes.includes("room:read" /* Read */) && scopes.includes("room:presence:write" /* PresenceWrite */) && !scopes.includes("room:write" /* Write */);
3702
4674
  }
3703
- function handleAuthSuccess(token, socket) {
3704
- socket.addEventListener("message", handleRawSocketMessage);
3705
- socket.addEventListener("open", handleSocketOpen);
3706
- socket.addEventListener("close", handleExplicitClose);
3707
- socket.addEventListener("error", handleSocketError);
3708
- updateConnection(
3709
- {
3710
- status: "connecting",
3711
- id: token.actor,
3712
- userInfo: token.info,
3713
- userId: token.id,
3714
- isReadOnly: isStorageReadOnly(token.scopes)
3715
- },
3716
- batchUpdates
3717
- );
3718
- context.idFactory = makeIdFactory(token.actor);
3719
- context.socket = socket;
3720
- }
3721
- function authenticationFailure(error2) {
3722
- if (process.env.NODE_ENV !== "production") {
3723
- error("Call to authentication endpoint failed", error2);
3724
- }
3725
- context.token = null;
3726
- updateConnection({ status: "unavailable" }, batchUpdates);
3727
- context.numRetries++;
3728
- clearTimeout(context.timers.reconnect);
3729
- context.timers.reconnect = effects.scheduleReconnect(getRetryDelay());
3730
- }
3731
- function handleWindowGotFocus() {
3732
- if (context.connection.current.status === "open") {
3733
- log("Heartbeat after visibility change");
3734
- heartbeat();
3735
- }
3736
- }
3737
4675
  function onUpdatePresenceMessage(message) {
3738
4676
  if (message.targetActor !== void 0) {
3739
4677
  const oldUser = context.others.getUser(message.actor);
@@ -3783,12 +4721,6 @@ function createRoom(options, config) {
3783
4721
  }
3784
4722
  return { type: "reset" };
3785
4723
  }
3786
- function handleNavigatorBackOnline() {
3787
- if (context.connection.current.status === "unavailable") {
3788
- log("Try to reconnect after connectivity change");
3789
- reconnect();
3790
- }
3791
- }
3792
4724
  function canUndo() {
3793
4725
  return context.undoStack.length > 0;
3794
4726
  }
@@ -3844,17 +4776,7 @@ function createRoom(options, config) {
3844
4776
  ops: result.ops
3845
4777
  });
3846
4778
  notify(result.updates, batchedUpdatesWrapper);
3847
- effects.send(messages);
3848
- }
3849
- function handleRawSocketMessage(event) {
3850
- if (event.data === "pong") {
3851
- transition({ type: "RECEIVE_PONG" });
3852
- } else {
3853
- handleServerMessage(event);
3854
- }
3855
- }
3856
- function handlePong() {
3857
- clearTimeout(context.timers.pongTimeout);
4779
+ sendMessages(messages);
3858
4780
  }
3859
4781
  function handleServerMessage(event) {
3860
4782
  if (typeof event.data !== "string") {
@@ -3908,8 +4830,8 @@ function createRoom(options, config) {
3908
4830
  const unacknowledgedOps = new Map(context.unacknowledgedOps);
3909
4831
  createOrUpdateRootFromMessage(message, doNotBatchUpdates);
3910
4832
  applyAndSendOps(unacknowledgedOps, doNotBatchUpdates);
3911
- if (_getInitialStateResolver !== null) {
3912
- _getInitialStateResolver();
4833
+ if (_resolveInitialStatePromise !== null) {
4834
+ _resolveInitialStatePromise();
3913
4835
  }
3914
4836
  notifyStorageStatus();
3915
4837
  eventHub.storageDidLoad.notify();
@@ -3957,140 +4879,6 @@ ${Array.from(traces).join("\n\n")}`
3957
4879
  notify(updates, doNotBatchUpdates);
3958
4880
  });
3959
4881
  }
3960
- function handleExplicitClose(event) {
3961
- context.socket = null;
3962
- clearTimeout(context.timers.flush);
3963
- clearTimeout(context.timers.reconnect);
3964
- clearInterval(context.timers.heartbeat);
3965
- clearTimeout(context.timers.pongTimeout);
3966
- context.others.clearOthers();
3967
- batchUpdates(() => {
3968
- notify({ others: [{ type: "reset" }] }, doNotBatchUpdates);
3969
- if (event.code >= 4e3 && event.code <= 4100) {
3970
- updateConnection({ status: "failed" }, doNotBatchUpdates);
3971
- const error2 = new LiveblocksError(event.reason, event.code);
3972
- eventHub.error.notify(error2);
3973
- const delay = getRetryDelay(true);
3974
- context.numRetries++;
3975
- if (process.env.NODE_ENV !== "production") {
3976
- error(
3977
- `Connection to websocket server closed. Reason: ${error2.message} (code: ${error2.code}). Retrying in ${delay}ms.`
3978
- );
3979
- }
3980
- updateConnection({ status: "unavailable" }, doNotBatchUpdates);
3981
- clearTimeout(context.timers.reconnect);
3982
- context.timers.reconnect = effects.scheduleReconnect(delay);
3983
- } else if (event.code === 4999 /* CLOSE_WITHOUT_RETRY */) {
3984
- updateConnection({ status: "closed" }, doNotBatchUpdates);
3985
- } else {
3986
- const delay = getRetryDelay();
3987
- context.numRetries++;
3988
- if (process.env.NODE_ENV !== "production") {
3989
- warn(
3990
- `Connection to Liveblocks websocket server closed (code: ${event.code}). Retrying in ${delay}ms.`
3991
- );
3992
- }
3993
- updateConnection({ status: "unavailable" }, doNotBatchUpdates);
3994
- clearTimeout(context.timers.reconnect);
3995
- context.timers.reconnect = effects.scheduleReconnect(delay);
3996
- }
3997
- });
3998
- }
3999
- function updateConnection(connection, batchedUpdatesWrapper) {
4000
- context.connection.set(connection);
4001
- batchedUpdatesWrapper(() => {
4002
- eventHub.connection.notify(connection.status);
4003
- });
4004
- }
4005
- function getRetryDelay(slow = false) {
4006
- if (slow) {
4007
- return BACKOFF_RETRY_DELAYS_SLOW[context.numRetries < BACKOFF_RETRY_DELAYS_SLOW.length ? context.numRetries : BACKOFF_RETRY_DELAYS_SLOW.length - 1];
4008
- }
4009
- return BACKOFF_RETRY_DELAYS[context.numRetries < BACKOFF_RETRY_DELAYS.length ? context.numRetries : BACKOFF_RETRY_DELAYS.length - 1];
4010
- }
4011
- function handleSocketError() {
4012
- }
4013
- function handleSocketOpen() {
4014
- clearInterval(context.timers.heartbeat);
4015
- context.timers.heartbeat = effects.startHeartbeatInterval();
4016
- if (context.connection.current.status === "connecting") {
4017
- updateConnection(
4018
- __spreadProps(__spreadValues({}, context.connection.current), { status: "open" }),
4019
- batchUpdates
4020
- );
4021
- context.numRetries = 0;
4022
- if (context.lastConnectionId !== void 0) {
4023
- context.buffer.me = {
4024
- type: "full",
4025
- data: (
4026
- // Because state.me.current is a readonly object, we'll have to
4027
- // make a copy here. Otherwise, type errors happen later when
4028
- // "patching" my presence.
4029
- __spreadValues({}, context.me.current)
4030
- )
4031
- };
4032
- tryFlushing();
4033
- }
4034
- context.lastConnectionId = context.connection.current.id;
4035
- if (context.root) {
4036
- context.buffer.messages.push({ type: 200 /* FETCH_STORAGE */ });
4037
- }
4038
- tryFlushing();
4039
- } else {
4040
- }
4041
- }
4042
- function heartbeat() {
4043
- if (context.socket === null) {
4044
- return;
4045
- }
4046
- clearTimeout(context.timers.pongTimeout);
4047
- context.timers.pongTimeout = effects.schedulePongTimeout();
4048
- if (context.socket.readyState === context.socket.OPEN) {
4049
- context.socket.send("ping");
4050
- }
4051
- }
4052
- function pongTimeout() {
4053
- log("Pong timeout. Trying to reconnect.");
4054
- reconnect();
4055
- }
4056
- function handleDisconnect() {
4057
- if (context.socket) {
4058
- context.socket.removeEventListener("open", handleSocketOpen);
4059
- context.socket.removeEventListener("message", handleRawSocketMessage);
4060
- context.socket.removeEventListener("close", handleExplicitClose);
4061
- context.socket.removeEventListener("error", handleSocketError);
4062
- context.socket.close();
4063
- context.socket = null;
4064
- }
4065
- clearTimeout(context.timers.flush);
4066
- clearTimeout(context.timers.reconnect);
4067
- clearInterval(context.timers.heartbeat);
4068
- clearTimeout(context.timers.pongTimeout);
4069
- batchUpdates(() => {
4070
- updateConnection({ status: "closed" }, doNotBatchUpdates);
4071
- context.others.clearOthers();
4072
- notify({ others: [{ type: "reset" }] }, doNotBatchUpdates);
4073
- });
4074
- for (const eventSource2 of Object.values(eventHub)) {
4075
- eventSource2.clear();
4076
- }
4077
- }
4078
- function reconnect() {
4079
- if (context.socket) {
4080
- context.socket.removeEventListener("open", handleSocketOpen);
4081
- context.socket.removeEventListener("message", handleRawSocketMessage);
4082
- context.socket.removeEventListener("close", handleExplicitClose);
4083
- context.socket.removeEventListener("error", handleSocketError);
4084
- context.socket.close();
4085
- context.socket = null;
4086
- }
4087
- clearTimeout(context.timers.flush);
4088
- clearTimeout(context.timers.reconnect);
4089
- clearInterval(context.timers.heartbeat);
4090
- clearTimeout(context.timers.pongTimeout);
4091
- updateConnection({ status: "unavailable" }, batchUpdates);
4092
- handleConnect();
4093
- }
4094
4882
  function tryFlushing() {
4095
4883
  const storageOps = context.buffer.storageOperations;
4096
4884
  if (storageOps.length > 0) {
@@ -4099,7 +4887,7 @@ ${Array.from(traces).join("\n\n")}`
4099
4887
  }
4100
4888
  notifyStorageStatus();
4101
4889
  }
4102
- if (context.socket === null || context.socket.readyState !== context.socket.OPEN) {
4890
+ if (managedSocket.getStatus() !== "connected") {
4103
4891
  context.buffer.storageOperations = [];
4104
4892
  return;
4105
4893
  }
@@ -4110,16 +4898,17 @@ ${Array.from(traces).join("\n\n")}`
4110
4898
  if (messagesToFlush.length === 0) {
4111
4899
  return;
4112
4900
  }
4113
- effects.send(messagesToFlush);
4901
+ sendMessages(messagesToFlush);
4114
4902
  context.buffer = {
4903
+ flushTimerID: void 0,
4115
4904
  lastFlushedAt: now,
4116
4905
  messages: [],
4117
4906
  storageOperations: [],
4118
4907
  me: null
4119
4908
  };
4120
4909
  } else {
4121
- clearTimeout(context.timers.flush);
4122
- context.timers.flush = setTimeout(
4910
+ clearTimeout(context.buffer.flushTimerID);
4911
+ context.buffer.flushTimerID = setTimeout(
4123
4912
  tryFlushing,
4124
4913
  config.throttleDelay - elapsedMillis
4125
4914
  );
@@ -4156,7 +4945,7 @@ ${Array.from(traces).join("\n\n")}`
4156
4945
  function broadcastEvent(event, options2 = {
4157
4946
  shouldQueueEventIfNotReady: false
4158
4947
  }) {
4159
- if (context.socket === null && !options2.shouldQueueEventIfNotReady) {
4948
+ if (managedSocket.getStatus() !== "connected" && !options2.shouldQueueEventIfNotReady) {
4160
4949
  return;
4161
4950
  }
4162
4951
  context.buffer.messages.push({
@@ -4170,13 +4959,13 @@ ${Array.from(traces).join("\n\n")}`
4170
4959
  tryFlushing();
4171
4960
  }
4172
4961
  let _getInitialStatePromise = null;
4173
- let _getInitialStateResolver = null;
4962
+ let _resolveInitialStatePromise = null;
4174
4963
  function startLoadingStorage() {
4175
4964
  if (_getInitialStatePromise === null) {
4176
4965
  context.buffer.messages.push({ type: 200 /* FETCH_STORAGE */ });
4177
4966
  tryFlushing();
4178
4967
  _getInitialStatePromise = new Promise(
4179
- (resolve) => _getInitialStateResolver = resolve
4968
+ (resolve) => _resolveInitialStatePromise = resolve
4180
4969
  );
4181
4970
  notifyStorageStatus();
4182
4971
  }
@@ -4293,11 +5082,6 @@ ${Array.from(traces).join("\n\n")}`
4293
5082
  _addToRealUndoStack(historyOps, batchUpdates);
4294
5083
  }
4295
5084
  }
4296
- function handleImplicitClose() {
4297
- if (context.socket) {
4298
- context.socket = null;
4299
- }
4300
- }
4301
5085
  function getStorageStatus() {
4302
5086
  if (_getInitialStatePromise === null) {
4303
5087
  return "not-loaded";
@@ -4320,38 +5104,20 @@ ${Array.from(traces).join("\n\n")}`
4320
5104
  (others) => others.map((other, index) => userToTreeNode(`Other ${index}`, other))
4321
5105
  );
4322
5106
  const events = {
5107
+ connection: eventHub.connection.observable,
5108
+ // Old/deprecated API
5109
+ status: eventHub.status.observable,
5110
+ // New/recommended API
5111
+ lostConnection: eventHub.lostConnection.observable,
4323
5112
  customEvent: eventHub.customEvent.observable,
4324
5113
  others: eventHub.others.observable,
4325
5114
  me: eventHub.me.observable,
4326
5115
  error: eventHub.error.observable,
4327
- connection: eventHub.connection.observable,
4328
5116
  storage: eventHub.storage.observable,
4329
5117
  history: eventHub.history.observable,
4330
5118
  storageDidLoad: eventHub.storageDidLoad.observable,
4331
5119
  storageStatus: eventHub.storageStatus.observable
4332
5120
  };
4333
- function transition(event) {
4334
- switch (event.type) {
4335
- case "CONNECT":
4336
- return handleConnect();
4337
- case "DISCONNECT":
4338
- return handleDisconnect();
4339
- case "RECEIVE_PONG":
4340
- return handlePong();
4341
- case "AUTH_SUCCESS":
4342
- return handleAuthSuccess(event.token, event.socket);
4343
- case "WINDOW_GOT_FOCUS":
4344
- return handleWindowGotFocus();
4345
- case "NAVIGATOR_ONLINE":
4346
- return handleNavigatorBackOnline();
4347
- case "IMPLICIT_CLOSE":
4348
- return handleImplicitClose();
4349
- case "EXPLICIT_CLOSE":
4350
- return handleExplicitClose(event.closeEvent);
4351
- default:
4352
- return assertNever(event, "Invalid event");
4353
- }
4354
- }
4355
5121
  return {
4356
5122
  /* NOTE: Exposing __internal here only to allow testing implementation details in unit tests */
4357
5123
  __internal: {
@@ -4359,10 +5125,6 @@ ${Array.from(traces).join("\n\n")}`
4359
5125
  return context.buffer;
4360
5126
  },
4361
5127
  // prettier-ignore
4362
- get numRetries() {
4363
- return context.numRetries;
4364
- },
4365
- // prettier-ignore
4366
5128
  get undoStack() {
4367
5129
  return context.undoStack;
4368
5130
  },
@@ -4376,27 +5138,17 @@ ${Array.from(traces).join("\n\n")}`
4376
5138
  getOthers_forDevTools: () => others_forDevTools.current,
4377
5139
  // prettier-ignore
4378
5140
  send: {
4379
- explicitClose: (closeEvent) => transition({ type: "EXPLICIT_CLOSE", closeEvent }),
4380
- implicitClose: () => transition({ type: "IMPLICIT_CLOSE" }),
4381
- authSuccess: (token, socket) => transition({ type: "AUTH_SUCCESS", token, socket }),
4382
- navigatorOnline: () => transition({ type: "NAVIGATOR_ONLINE" }),
4383
- windowGotFocus: () => transition({ type: "WINDOW_GOT_FOCUS" }),
4384
- pong: () => transition({ type: "RECEIVE_PONG" }),
4385
- connect: () => transition({ type: "CONNECT" }),
4386
- disconnect: () => transition({ type: "DISCONNECT" }),
4387
- /**
4388
- * This one looks differently from the rest, because receiving messages
4389
- * is handled orthorgonally from all other possible events above,
4390
- * because it does not matter what the connectivity state of the
4391
- * machine is, so there won't be an explicit state machine transition
4392
- * needed for this event.
4393
- */
4394
- incomingMessage: handleServerMessage
5141
+ // These exist only for our E2E testing app
5142
+ explicitClose: (event) => managedSocket._privateSendMachineEvent({ type: "EXPLICIT_SOCKET_CLOSE", event }),
5143
+ implicitClose: () => managedSocket._privateSendMachineEvent({ type: "PONG_TIMEOUT" })
4395
5144
  }
4396
5145
  },
4397
5146
  id: config.roomId,
4398
5147
  subscribe: makeClassicSubscribeFn(events),
4399
- reconnect,
5148
+ connect: () => managedSocket.connect(),
5149
+ reconnect: () => managedSocket.reconnect(),
5150
+ disconnect: () => managedSocket.disconnect(),
5151
+ destroy: () => managedSocket.destroy(),
4400
5152
  // Presence
4401
5153
  updatePresence,
4402
5154
  broadcastEvent,
@@ -4415,8 +5167,9 @@ ${Array.from(traces).join("\n\n")}`
4415
5167
  getStorageStatus,
4416
5168
  events,
4417
5169
  // Core
4418
- getConnectionState: () => context.connection.current.status,
4419
- isSelfAware: () => isConnectionSelfAware(context.connection.current),
5170
+ getStatus: () => managedSocket.getStatus(),
5171
+ getConnectionState: () => managedSocket.getLegacyStatus(),
5172
+ isSelfAware: () => context.sessionInfo.current !== null,
4420
5173
  getSelf: () => self.current,
4421
5174
  // Presence
4422
5175
  getPresence: () => context.me.current,
@@ -4468,6 +5221,12 @@ function makeClassicSubscribeFn(events) {
4468
5221
  return events.connection.subscribe(
4469
5222
  callback
4470
5223
  );
5224
+ case "status":
5225
+ return events.status.subscribe(callback);
5226
+ case "lost-connection":
5227
+ return events.lostConnection.subscribe(
5228
+ callback
5229
+ );
4471
5230
  case "history":
4472
5231
  return events.history.subscribe(callback);
4473
5232
  case "storage-status":
@@ -4503,20 +5262,14 @@ function makeClassicSubscribeFn(events) {
4503
5262
  function isRoomEventName(value) {
4504
5263
  return value === "my-presence" || value === "others" || value === "event" || value === "error" || value === "connection" || value === "history" || value === "storage-status";
4505
5264
  }
4506
- var LiveblocksError = class extends Error {
4507
- constructor(message, code) {
4508
- super(message);
4509
- this.code = code;
4510
- }
4511
- };
4512
- function prepareCreateWebSocket(liveblocksServer, WebSocketPolyfill) {
4513
- if (typeof window === "undefined" && WebSocketPolyfill === void 0) {
4514
- throw new Error(
4515
- "To use Liveblocks client in a non-dom environment, you need to provide a WebSocket polyfill."
4516
- );
4517
- }
4518
- const ws = WebSocketPolyfill || WebSocket;
5265
+ function makeCreateSocketDelegateForRoom(liveblocksServer, WebSocketPolyfill) {
4519
5266
  return (richToken) => {
5267
+ const ws = WebSocketPolyfill != null ? WebSocketPolyfill : typeof WebSocket === "undefined" ? void 0 : WebSocket;
5268
+ if (ws === void 0) {
5269
+ throw new StopRetrying(
5270
+ "To use Liveblocks client in a non-dom environment, you need to provide a WebSocket polyfill."
5271
+ );
5272
+ }
4520
5273
  const token = richToken.raw;
4521
5274
  return new ws(
4522
5275
  `${liveblocksServer}/?token=${token}&version=${// prettier-ignore
@@ -4524,7 +5277,7 @@ function prepareCreateWebSocket(liveblocksServer, WebSocketPolyfill) {
4524
5277
  // @ts-ignore (__PACKAGE_VERSION__ will be injected by the build script)
4525
5278
  true ? (
4526
5279
  /* istanbul ignore next */
4527
- "1.0.12"
5280
+ "1.1.0-beta1"
4528
5281
  ) : "dev"}`
4529
5282
  );
4530
5283
  };
@@ -4543,45 +5296,44 @@ function httpSend(message, token, endpoint, fetchPolyfill) {
4543
5296
  });
4544
5297
  });
4545
5298
  }
4546
- function prepareAuthEndpoint(roomId, authentication, fetchPolyfill) {
5299
+ function makeAuthDelegateForRoom(roomId, authentication, fetchPolyfill) {
5300
+ const fetcher = fetchPolyfill != null ? fetchPolyfill : typeof window === "undefined" ? void 0 : window.fetch;
4547
5301
  if (authentication.type === "public") {
4548
- if (typeof window === "undefined" && fetchPolyfill === void 0) {
4549
- throw new Error(
4550
- "To use Liveblocks client in a non-dom environment with a publicApiKey, you need to provide a fetch polyfill."
4551
- );
4552
- }
4553
- return () => fetchAuthEndpoint(
4554
- fetchPolyfill || /* istanbul ignore next */
4555
- fetch,
4556
- authentication.url,
4557
- {
5302
+ return () => __async(this, null, function* () {
5303
+ if (fetcher === void 0) {
5304
+ throw new StopRetrying(
5305
+ "To use Liveblocks client in a non-dom environment with a publicApiKey, you need to provide a fetch polyfill."
5306
+ );
5307
+ }
5308
+ return fetchAuthEndpoint(fetcher, authentication.url, {
4558
5309
  room: roomId,
4559
5310
  publicApiKey: authentication.publicApiKey
5311
+ }).then(({ token }) => parseRoomAuthToken(token));
5312
+ });
5313
+ } else if (authentication.type === "private") {
5314
+ return () => __async(this, null, function* () {
5315
+ if (fetcher === void 0) {
5316
+ throw new StopRetrying(
5317
+ "To use Liveblocks client in a non-dom environment with a url as auth endpoint, you need to provide a fetch polyfill."
5318
+ );
4560
5319
  }
4561
- ).then(({ token }) => parseRoomAuthToken(token));
4562
- }
4563
- if (authentication.type === "private") {
4564
- if (typeof window === "undefined" && fetchPolyfill === void 0) {
4565
- throw new Error(
4566
- "To use Liveblocks client in a non-dom environment with a url as auth endpoint, you need to provide a fetch polyfill."
4567
- );
4568
- }
4569
- return () => fetchAuthEndpoint(fetchPolyfill || fetch, authentication.url, {
4570
- room: roomId
4571
- }).then(({ token }) => parseRoomAuthToken(token));
4572
- }
4573
- if (authentication.type === "custom") {
5320
+ return fetchAuthEndpoint(fetcher, authentication.url, {
5321
+ room: roomId
5322
+ }).then(({ token }) => parseRoomAuthToken(token));
5323
+ });
5324
+ } else if (authentication.type === "custom") {
4574
5325
  return () => __async(this, null, function* () {
4575
5326
  const response = yield authentication.callback(roomId);
4576
5327
  if (!response || !response.token) {
4577
5328
  throw new Error(
4578
- 'Authentication error. We expect the authentication callback to return a token, but it does not. Hint: the return value should look like: { token: "..." }'
5329
+ 'We expect the authentication callback to return a token, but it does not. Hint: the return value should look like: { token: "..." }'
4579
5330
  );
4580
5331
  }
4581
5332
  return parseRoomAuthToken(response.token);
4582
5333
  });
5334
+ } else {
5335
+ throw new Error("Internal error. Unexpected authentication type");
4583
5336
  }
4584
- throw new Error("Internal error. Unexpected authentication type");
4585
5337
  }
4586
5338
  function fetchAuthEndpoint(fetch2, endpoint, body) {
4587
5339
  return __async(this, null, function* () {
@@ -4595,22 +5347,25 @@ function fetchAuthEndpoint(fetch2, endpoint, body) {
4595
5347
  body: JSON.stringify(body)
4596
5348
  });
4597
5349
  if (!res.ok) {
4598
- throw new AuthenticationError(
4599
- `Expected a status 200 but got ${res.status} when doing a POST request on "${endpoint}"`
4600
- );
5350
+ const reason = `${(yield res.text()).trim() || "reason not provided in auth response"} (${res.status} returned by POST ${endpoint})`;
5351
+ if (res.status === 401 || res.status === 403) {
5352
+ throw new StopRetrying(`Unauthorized: ${reason}`);
5353
+ } else {
5354
+ throw new Error(`Failed to authenticate: ${reason}`);
5355
+ }
4601
5356
  }
4602
5357
  let data;
4603
5358
  try {
4604
5359
  data = yield res.json();
4605
5360
  } catch (er) {
4606
- throw new AuthenticationError(
5361
+ throw new Error(
4607
5362
  `Expected a JSON response when doing a POST request on "${endpoint}". ${String(
4608
5363
  er
4609
5364
  )}`
4610
5365
  );
4611
5366
  }
4612
5367
  if (!isPlainObject(data) || typeof data.token !== "string") {
4613
- throw new AuthenticationError(
5368
+ throw new Error(
4614
5369
  `Expected a JSON response of the form \`{ token: "..." }\` when doing a POST request on "${endpoint}", but got ${JSON.stringify(
4615
5370
  data
4616
5371
  )}`
@@ -4620,11 +5375,6 @@ function fetchAuthEndpoint(fetch2, endpoint, body) {
4620
5375
  return { token };
4621
5376
  });
4622
5377
  }
4623
- var AuthenticationError = class extends Error {
4624
- constructor(message) {
4625
- super(message);
4626
- }
4627
- };
4628
5378
 
4629
5379
  // src/client.ts
4630
5380
  var MIN_THROTTLE = 16;
@@ -4643,7 +5393,7 @@ function createClient(options) {
4643
5393
  return room ? room : null;
4644
5394
  }
4645
5395
  function enter(roomId, options2) {
4646
- var _a, _b, _c;
5396
+ var _a, _b, _c, _d;
4647
5397
  const existingRoom = rooms.get(roomId);
4648
5398
  if (existingRoom !== void 0) {
4649
5399
  return existingRoom;
@@ -4660,7 +5410,10 @@ function createClient(options) {
4660
5410
  {
4661
5411
  roomId,
4662
5412
  throttleDelay,
5413
+ lostConnectionTimeout: (_b = clientOptions.lostConnectionTimeout) != null ? _b : 5e3,
4663
5414
  polyfills: clientOptions.polyfills,
5415
+ delegates: clientOptions.mockedDelegates,
5416
+ enableDebugLogging: clientOptions.enableDebugLogging,
4664
5417
  unstable_batchedUpdates: options2 == null ? void 0 : options2.unstable_batchedUpdates,
4665
5418
  liveblocksServer: getServerFromClientOptions(clientOptions),
4666
5419
  authentication: prepareAuthentication(clientOptions, roomId),
@@ -4674,17 +5427,17 @@ function createClient(options) {
4674
5427
  rooms.set(roomId, newRoom);
4675
5428
  setupDevTools(() => Array.from(rooms.keys()));
4676
5429
  linkDevTools(roomId, newRoom);
4677
- const shouldConnect = (_b = options2.shouldInitiallyConnect) != null ? _b : true;
5430
+ const shouldConnect = (_c = options2.shouldInitiallyConnect) != null ? _c : true;
4678
5431
  if (shouldConnect) {
4679
5432
  if (typeof atob === "undefined") {
4680
- if (((_c = clientOptions.polyfills) == null ? void 0 : _c.atob) === void 0) {
5433
+ if (((_d = clientOptions.polyfills) == null ? void 0 : _d.atob) === void 0) {
4681
5434
  throw new Error(
4682
5435
  "You need to polyfill atob to use the client in your environment. Please follow the instructions at https://liveblocks.io/docs/errors/liveblocks-client/atob-polyfill"
4683
5436
  );
4684
5437
  }
4685
5438
  global.atob = clientOptions.polyfills.atob;
4686
5439
  }
4687
- newRoom.__internal.send.connect();
5440
+ newRoom.connect();
4688
5441
  }
4689
5442
  return newRoom;
4690
5443
  }
@@ -4692,27 +5445,10 @@ function createClient(options) {
4692
5445
  unlinkDevTools(roomId);
4693
5446
  const room = rooms.get(roomId);
4694
5447
  if (room !== void 0) {
4695
- room.__internal.send.disconnect();
5448
+ room.destroy();
4696
5449
  rooms.delete(roomId);
4697
5450
  }
4698
5451
  }
4699
- if (typeof window !== "undefined" && // istanbul ignore next: React Native environment doesn't implement window.addEventListener
4700
- typeof window.addEventListener !== "undefined") {
4701
- window.addEventListener("online", () => {
4702
- for (const [, room] of rooms) {
4703
- room.__internal.send.navigatorOnline();
4704
- }
4705
- });
4706
- }
4707
- if (typeof document !== "undefined") {
4708
- document.addEventListener("visibilitychange", () => {
4709
- if (document.visibilityState === "visible") {
4710
- for (const [, room] of rooms) {
4711
- room.__internal.send.windowGotFocus();
4712
- }
4713
- }
4714
- });
4715
- }
4716
5452
  return {
4717
5453
  getRoom,
4718
5454
  enter,
@@ -5131,6 +5867,20 @@ function shallow(a, b) {
5131
5867
  return shallowObj(a, b);
5132
5868
  }
5133
5869
 
5870
+ // src/types/IWebSocket.ts
5871
+ var WebsocketCloseCodes = /* @__PURE__ */ ((WebsocketCloseCodes2) => {
5872
+ WebsocketCloseCodes2[WebsocketCloseCodes2["CLOSE_ABNORMAL"] = 1006] = "CLOSE_ABNORMAL";
5873
+ WebsocketCloseCodes2[WebsocketCloseCodes2["INVALID_MESSAGE_FORMAT"] = 4e3] = "INVALID_MESSAGE_FORMAT";
5874
+ WebsocketCloseCodes2[WebsocketCloseCodes2["NOT_ALLOWED"] = 4001] = "NOT_ALLOWED";
5875
+ WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_MESSAGES_PER_SECONDS"] = 4002] = "MAX_NUMBER_OF_MESSAGES_PER_SECONDS";
5876
+ WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_CONCURRENT_CONNECTIONS"] = 4003] = "MAX_NUMBER_OF_CONCURRENT_CONNECTIONS";
5877
+ WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP"] = 4004] = "MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP";
5878
+ WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_CONCURRENT_CONNECTIONS_PER_ROOM"] = 4005] = "MAX_NUMBER_OF_CONCURRENT_CONNECTIONS_PER_ROOM";
5879
+ WebsocketCloseCodes2[WebsocketCloseCodes2["CLOSE_WITHOUT_RETRY"] = 4999] = "CLOSE_WITHOUT_RETRY";
5880
+ return WebsocketCloseCodes2;
5881
+ })(WebsocketCloseCodes || {});
5882
+
5883
+
5134
5884
 
5135
5885
 
5136
5886
 
@@ -5166,4 +5916,4 @@ function shallow(a, b) {
5166
5916
 
5167
5917
 
5168
5918
 
5169
- exports.ClientMsgCode = ClientMsgCode; exports.CrdtType = CrdtType; exports.LiveList = LiveList; exports.LiveMap = LiveMap; exports.LiveObject = LiveObject; exports.OpCode = OpCode; exports.ServerMsgCode = ServerMsgCode; exports.WebsocketCloseCodes = WebsocketCloseCodes; exports.asArrayWithLegacyMethods = asArrayWithLegacyMethods; exports.asPos = asPos; exports.assert = assert; exports.assertNever = assertNever; exports.b64decode = b64decode; exports.createClient = createClient; exports.deprecate = deprecate; exports.deprecateIf = deprecateIf; exports.errorIf = errorIf; exports.freeze = freeze; exports.isAppOnlyAuthToken = isAppOnlyAuthToken; exports.isAuthToken = isAuthToken; exports.isChildCrdt = isChildCrdt; exports.isJsonArray = isJsonArray; exports.isJsonObject = isJsonObject; exports.isJsonScalar = isJsonScalar; exports.isPlainObject = isPlainObject; exports.isRoomAuthToken = isRoomAuthToken; exports.isRootCrdt = isRootCrdt; exports.legacy_patchImmutableObject = legacy_patchImmutableObject; exports.lsonToJson = lsonToJson; exports.makePosition = makePosition; exports.nn = nn; exports.patchLiveObjectKey = patchLiveObjectKey; exports.shallow = shallow; exports.throwUsageError = throwUsageError; exports.tryParseJson = tryParseJson;
5919
+ exports.ClientMsgCode = ClientMsgCode; exports.CrdtType = CrdtType; exports.LiveList = LiveList; exports.LiveMap = LiveMap; exports.LiveObject = LiveObject; exports.OpCode = OpCode; exports.ServerMsgCode = ServerMsgCode; exports.WebsocketCloseCodes = WebsocketCloseCodes; exports.asArrayWithLegacyMethods = asArrayWithLegacyMethods; exports.asPos = asPos; exports.assert = assert; exports.assertNever = assertNever; exports.b64decode = b64decode; exports.createClient = createClient; exports.deprecate = deprecate; exports.deprecateIf = deprecateIf; exports.errorIf = errorIf; exports.freeze = freeze; exports.isAppOnlyAuthToken = isAppOnlyAuthToken; exports.isAuthToken = isAuthToken; exports.isChildCrdt = isChildCrdt; exports.isJsonArray = isJsonArray; exports.isJsonObject = isJsonObject; exports.isJsonScalar = isJsonScalar; exports.isPlainObject = isPlainObject; exports.isRoomAuthToken = isRoomAuthToken; exports.isRootCrdt = isRootCrdt; exports.legacy_patchImmutableObject = legacy_patchImmutableObject; exports.lsonToJson = lsonToJson; exports.makePosition = makePosition; exports.nn = nn; exports.patchLiveObjectKey = patchLiveObjectKey; exports.shallow = shallow; exports.throwUsageError = throwUsageError; exports.tryParseJson = tryParseJson; exports.withTimeout = withTimeout;