@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.
- package/dist/index.js +913 -368
- 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
|
|
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 = (
|
|
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
|
-
|
|
3409
|
-
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
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
|
-
|
|
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
|
|
5047
|
+
"1.1.0-fsm1"
|
|
4507
5048
|
) : "dev"}`
|
|
4508
5049
|
);
|
|
4509
5050
|
};
|
|
4510
5051
|
}
|
|
4511
|
-
function
|
|
5052
|
+
function makeAuthDelegateForRoom(roomId, authentication, fetchPolyfill) {
|
|
4512
5053
|
if (authentication.type === "public") {
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
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
|
-
|
|
4564
|
-
|
|
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.
|
|
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
|
|