@liveblocks/core 1.0.8 → 1.1.0-fsm1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +66 -59
- package/dist/index.js +949 -362
- 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
|
}
|
|
@@ -2983,20 +3746,22 @@ function parseJwtToken(token) {
|
|
|
2983
3746
|
}
|
|
2984
3747
|
function parseRoomAuthToken(tokenString) {
|
|
2985
3748
|
const data = parseJwtToken(tokenString);
|
|
2986
|
-
if (data && isRoomAuthToken(data)) {
|
|
2987
|
-
const _a = data, {
|
|
2988
|
-
maxConnections: _legacyField
|
|
2989
|
-
} = _a, token = __objRest(_a, [
|
|
2990
|
-
// If this legacy field is found on the token, pretend it wasn't there,
|
|
2991
|
-
// to make all internally used token payloads uniform
|
|
2992
|
-
"maxConnections"
|
|
2993
|
-
]);
|
|
2994
|
-
return token;
|
|
2995
|
-
} else {
|
|
3749
|
+
if (!(data && isRoomAuthToken(data))) {
|
|
2996
3750
|
throw new Error(
|
|
2997
3751
|
"Authentication error: we expected a room token but did not get one. Hint: if you are using a callback, ensure the room is passed when creating the token. For more information: https://liveblocks.io/docs/api-reference/liveblocks-client#createClientCallback"
|
|
2998
3752
|
);
|
|
2999
3753
|
}
|
|
3754
|
+
const _a = data, {
|
|
3755
|
+
maxConnections: _legacyField
|
|
3756
|
+
} = _a, parsedToken = __objRest(_a, [
|
|
3757
|
+
// If this legacy field is found on the token, pretend it wasn't there,
|
|
3758
|
+
// to make all internally used token payloads uniform
|
|
3759
|
+
"maxConnections"
|
|
3760
|
+
]);
|
|
3761
|
+
return {
|
|
3762
|
+
raw: tokenString,
|
|
3763
|
+
parsed: parsedToken
|
|
3764
|
+
};
|
|
3000
3765
|
}
|
|
3001
3766
|
|
|
3002
3767
|
// src/protocol/ClientMsg.ts
|
|
@@ -3237,31 +4002,11 @@ var DerivedRef = class extends ImmutableRef {
|
|
|
3237
4002
|
}
|
|
3238
4003
|
};
|
|
3239
4004
|
|
|
3240
|
-
// src/types/IWebSocket.ts
|
|
3241
|
-
var WebsocketCloseCodes = /* @__PURE__ */ ((WebsocketCloseCodes2) => {
|
|
3242
|
-
WebsocketCloseCodes2[WebsocketCloseCodes2["CLOSE_ABNORMAL"] = 1006] = "CLOSE_ABNORMAL";
|
|
3243
|
-
WebsocketCloseCodes2[WebsocketCloseCodes2["INVALID_MESSAGE_FORMAT"] = 4e3] = "INVALID_MESSAGE_FORMAT";
|
|
3244
|
-
WebsocketCloseCodes2[WebsocketCloseCodes2["NOT_ALLOWED"] = 4001] = "NOT_ALLOWED";
|
|
3245
|
-
WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_MESSAGES_PER_SECONDS"] = 4002] = "MAX_NUMBER_OF_MESSAGES_PER_SECONDS";
|
|
3246
|
-
WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_CONCURRENT_CONNECTIONS"] = 4003] = "MAX_NUMBER_OF_CONCURRENT_CONNECTIONS";
|
|
3247
|
-
WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP"] = 4004] = "MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP";
|
|
3248
|
-
WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_CONCURRENT_CONNECTIONS_PER_ROOM"] = 4005] = "MAX_NUMBER_OF_CONCURRENT_CONNECTIONS_PER_ROOM";
|
|
3249
|
-
WebsocketCloseCodes2[WebsocketCloseCodes2["CLOSE_WITHOUT_RETRY"] = 4999] = "CLOSE_WITHOUT_RETRY";
|
|
3250
|
-
return WebsocketCloseCodes2;
|
|
3251
|
-
})(WebsocketCloseCodes || {});
|
|
3252
|
-
|
|
3253
4005
|
// src/room.ts
|
|
3254
|
-
var BACKOFF_RETRY_DELAYS = [250, 500, 1e3, 2e3, 4e3, 8e3, 1e4];
|
|
3255
|
-
var BACKOFF_RETRY_DELAYS_SLOW = [2e3, 3e4, 6e4, 3e5];
|
|
3256
|
-
var HEARTBEAT_INTERVAL = 3e4;
|
|
3257
|
-
var PONG_TIMEOUT = 2e3;
|
|
3258
4006
|
function makeIdFactory(connectionId) {
|
|
3259
4007
|
let count = 0;
|
|
3260
4008
|
return () => `${connectionId}:${count++}`;
|
|
3261
4009
|
}
|
|
3262
|
-
function log(..._params) {
|
|
3263
|
-
return;
|
|
3264
|
-
}
|
|
3265
4010
|
function isConnectionSelfAware(connection) {
|
|
3266
4011
|
return connection.status === "open" || connection.status === "connecting";
|
|
3267
4012
|
}
|
|
@@ -3274,19 +4019,25 @@ function userToTreeNode(key, user) {
|
|
|
3274
4019
|
};
|
|
3275
4020
|
}
|
|
3276
4021
|
function createRoom(options, config) {
|
|
3277
|
-
var _a;
|
|
4022
|
+
var _a, _b, _c;
|
|
3278
4023
|
const initialPresence = typeof options.initialPresence === "function" ? options.initialPresence(config.roomId) : options.initialPresence;
|
|
3279
4024
|
const initialStorage = typeof options.initialStorage === "function" ? options.initialStorage(config.roomId) : options.initialStorage;
|
|
4025
|
+
const delegates = {
|
|
4026
|
+
authenticate: makeAuthDelegateForRoom(
|
|
4027
|
+
config.roomId,
|
|
4028
|
+
config.authentication,
|
|
4029
|
+
(_a = config.polyfills) == null ? void 0 : _a.fetch
|
|
4030
|
+
),
|
|
4031
|
+
createSocket: makeCreateSocketDelegateForRoom(
|
|
4032
|
+
config.liveblocksServer,
|
|
4033
|
+
(_b = config.polyfills) == null ? void 0 : _b.WebSocket
|
|
4034
|
+
)
|
|
4035
|
+
};
|
|
4036
|
+
const managedSocket = new ManagedSocket(delegates);
|
|
3280
4037
|
const context = {
|
|
3281
|
-
token: null,
|
|
3282
4038
|
lastConnectionId: null,
|
|
3283
|
-
socket: null,
|
|
3284
|
-
numRetries: 0,
|
|
3285
4039
|
timers: {
|
|
3286
|
-
flush: void 0
|
|
3287
|
-
reconnect: void 0,
|
|
3288
|
-
heartbeat: void 0,
|
|
3289
|
-
pongTimeout: void 0
|
|
4040
|
+
flush: void 0
|
|
3290
4041
|
},
|
|
3291
4042
|
buffer: {
|
|
3292
4043
|
lastFlushedAt: 0,
|
|
@@ -3319,7 +4070,68 @@ function createRoom(options, config) {
|
|
|
3319
4070
|
opStackTraces: process.env.NODE_ENV !== "production" ? /* @__PURE__ */ new Map() : void 0
|
|
3320
4071
|
};
|
|
3321
4072
|
const doNotBatchUpdates = (cb) => cb();
|
|
3322
|
-
const batchUpdates = (
|
|
4073
|
+
const batchUpdates = (_c = config.unstable_batchedUpdates) != null ? _c : doNotBatchUpdates;
|
|
4074
|
+
function onStatusDidChange(newStatus) {
|
|
4075
|
+
if (newStatus !== "open" && newStatus !== "connecting") {
|
|
4076
|
+
context.connection.set({ status: newStatus });
|
|
4077
|
+
} else {
|
|
4078
|
+
context.connection.set({
|
|
4079
|
+
status: newStatus,
|
|
4080
|
+
id: managedSocket.token.parsed.actor,
|
|
4081
|
+
userInfo: managedSocket.token.parsed.info,
|
|
4082
|
+
userId: managedSocket.token.parsed.id,
|
|
4083
|
+
isReadOnly: isStorageReadOnly(managedSocket.token.parsed.scopes)
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
4086
|
+
batchUpdates(() => {
|
|
4087
|
+
eventHub.connection.notify(newStatus);
|
|
4088
|
+
});
|
|
4089
|
+
}
|
|
4090
|
+
function onDidConnect() {
|
|
4091
|
+
const conn = context.connection.current;
|
|
4092
|
+
if (conn.status !== "open") {
|
|
4093
|
+
throw new Error("Unexpected non-open state here");
|
|
4094
|
+
}
|
|
4095
|
+
if (context.lastConnectionId !== void 0) {
|
|
4096
|
+
context.buffer.me = {
|
|
4097
|
+
type: "full",
|
|
4098
|
+
data: (
|
|
4099
|
+
// Because state.me.current is a readonly object, we'll have to
|
|
4100
|
+
// make a copy here. Otherwise, type errors happen later when
|
|
4101
|
+
// "patching" my presence.
|
|
4102
|
+
__spreadValues({}, context.me.current)
|
|
4103
|
+
)
|
|
4104
|
+
};
|
|
4105
|
+
tryFlushing();
|
|
4106
|
+
}
|
|
4107
|
+
context.lastConnectionId = conn.id;
|
|
4108
|
+
context.idFactory = makeIdFactory(conn.id);
|
|
4109
|
+
if (context.root) {
|
|
4110
|
+
context.buffer.messages.push({ type: 200 /* FETCH_STORAGE */ });
|
|
4111
|
+
}
|
|
4112
|
+
tryFlushing();
|
|
4113
|
+
}
|
|
4114
|
+
function onDidDisconnect() {
|
|
4115
|
+
clearTimeout(context.timers.flush);
|
|
4116
|
+
batchUpdates(() => {
|
|
4117
|
+
context.others.clearOthers();
|
|
4118
|
+
notify({ others: [{ type: "reset" }] }, doNotBatchUpdates);
|
|
4119
|
+
});
|
|
4120
|
+
}
|
|
4121
|
+
managedSocket.events.onMessage.subscribe(handleServerMessage);
|
|
4122
|
+
managedSocket.events.statusDidChange.subscribe(onStatusDidChange);
|
|
4123
|
+
managedSocket.events.didConnect.subscribe(onDidConnect);
|
|
4124
|
+
managedSocket.events.didDisconnect.subscribe(onDidDisconnect);
|
|
4125
|
+
managedSocket.events.onLiveblocksError.subscribe((err) => {
|
|
4126
|
+
batchUpdates(() => {
|
|
4127
|
+
if (process.env.NODE_ENV !== "production") {
|
|
4128
|
+
error(
|
|
4129
|
+
`Connection to websocket server closed. Reason: ${err.message} (code: ${err.code}).`
|
|
4130
|
+
);
|
|
4131
|
+
}
|
|
4132
|
+
eventHub.error.notify(err);
|
|
4133
|
+
});
|
|
4134
|
+
});
|
|
3323
4135
|
const pool = {
|
|
3324
4136
|
roomId: config.roomId,
|
|
3325
4137
|
getNode: (id) => context.nodes.get(id),
|
|
@@ -3380,41 +4192,9 @@ function createRoom(options, config) {
|
|
|
3380
4192
|
storageStatus: makeEventSource()
|
|
3381
4193
|
};
|
|
3382
4194
|
const effects = config.mockedEffects || {
|
|
3383
|
-
authenticate(auth, createWebSocket) {
|
|
3384
|
-
const prevToken = context.token;
|
|
3385
|
-
if (prevToken !== null && !isTokenExpired(prevToken.parsed)) {
|
|
3386
|
-
const socket = createWebSocket(prevToken.raw);
|
|
3387
|
-
authenticationSuccess(prevToken.parsed, socket);
|
|
3388
|
-
return void 0;
|
|
3389
|
-
} else {
|
|
3390
|
-
void auth(config.roomId).then(({ token }) => {
|
|
3391
|
-
if (context.connection.current.status !== "authenticating") {
|
|
3392
|
-
return;
|
|
3393
|
-
}
|
|
3394
|
-
const parsedToken = parseRoomAuthToken(token);
|
|
3395
|
-
const socket = createWebSocket(token);
|
|
3396
|
-
authenticationSuccess(parsedToken, socket);
|
|
3397
|
-
context.token = { raw: token, parsed: parsedToken };
|
|
3398
|
-
}).catch(
|
|
3399
|
-
(er) => authenticationFailure(
|
|
3400
|
-
er instanceof Error ? er : new Error(String(er))
|
|
3401
|
-
)
|
|
3402
|
-
);
|
|
3403
|
-
return void 0;
|
|
3404
|
-
}
|
|
3405
|
-
},
|
|
3406
4195
|
send(messageOrMessages) {
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
}
|
|
3410
|
-
if (context.socket.readyState === context.socket.OPEN) {
|
|
3411
|
-
context.socket.send(JSON.stringify(messageOrMessages));
|
|
3412
|
-
}
|
|
3413
|
-
},
|
|
3414
|
-
scheduleFlush: (delay) => setTimeout(tryFlushing, delay),
|
|
3415
|
-
scheduleReconnect: (delay) => setTimeout(connect, delay),
|
|
3416
|
-
startHeartbeatInterval: () => setInterval(heartbeat, HEARTBEAT_INTERVAL),
|
|
3417
|
-
schedulePongTimeout: () => setTimeout(pongTimeout, PONG_TIMEOUT)
|
|
4196
|
+
managedSocket.send(JSON.stringify(messageOrMessages));
|
|
4197
|
+
}
|
|
3418
4198
|
};
|
|
3419
4199
|
const self = new DerivedRef(
|
|
3420
4200
|
context.connection,
|
|
@@ -3621,22 +4401,6 @@ function createRoom(options, config) {
|
|
|
3621
4401
|
}
|
|
3622
4402
|
}
|
|
3623
4403
|
}
|
|
3624
|
-
function connect() {
|
|
3625
|
-
var _a2, _b;
|
|
3626
|
-
if (context.connection.current.status !== "closed" && context.connection.current.status !== "unavailable") {
|
|
3627
|
-
return;
|
|
3628
|
-
}
|
|
3629
|
-
const auth = prepareAuthEndpoint(
|
|
3630
|
-
config.authentication,
|
|
3631
|
-
(_a2 = config.polyfills) == null ? void 0 : _a2.fetch
|
|
3632
|
-
);
|
|
3633
|
-
const createWebSocket = prepareCreateWebSocket(
|
|
3634
|
-
config.liveblocksServer,
|
|
3635
|
-
(_b = config.polyfills) == null ? void 0 : _b.WebSocket
|
|
3636
|
-
);
|
|
3637
|
-
updateConnection({ status: "authenticating" }, batchUpdates);
|
|
3638
|
-
effects.authenticate(auth, createWebSocket);
|
|
3639
|
-
}
|
|
3640
4404
|
function updatePresence(patch, options2) {
|
|
3641
4405
|
const oldValues = {};
|
|
3642
4406
|
if (context.buffer.me === null) {
|
|
@@ -3678,40 +4442,6 @@ function createRoom(options, config) {
|
|
|
3678
4442
|
function isStorageReadOnly(scopes) {
|
|
3679
4443
|
return scopes.includes("room:read" /* Read */) && scopes.includes("room:presence:write" /* PresenceWrite */) && !scopes.includes("room:write" /* Write */);
|
|
3680
4444
|
}
|
|
3681
|
-
function authenticationSuccess(token, socket) {
|
|
3682
|
-
socket.addEventListener("message", onMessage);
|
|
3683
|
-
socket.addEventListener("open", onOpen);
|
|
3684
|
-
socket.addEventListener("close", onClose);
|
|
3685
|
-
socket.addEventListener("error", onError);
|
|
3686
|
-
updateConnection(
|
|
3687
|
-
{
|
|
3688
|
-
status: "connecting",
|
|
3689
|
-
id: token.actor,
|
|
3690
|
-
userInfo: token.info,
|
|
3691
|
-
userId: token.id,
|
|
3692
|
-
isReadOnly: isStorageReadOnly(token.scopes)
|
|
3693
|
-
},
|
|
3694
|
-
batchUpdates
|
|
3695
|
-
);
|
|
3696
|
-
context.idFactory = makeIdFactory(token.actor);
|
|
3697
|
-
context.socket = socket;
|
|
3698
|
-
}
|
|
3699
|
-
function authenticationFailure(error2) {
|
|
3700
|
-
if (process.env.NODE_ENV !== "production") {
|
|
3701
|
-
error("Call to authentication endpoint failed", error2);
|
|
3702
|
-
}
|
|
3703
|
-
context.token = null;
|
|
3704
|
-
updateConnection({ status: "unavailable" }, batchUpdates);
|
|
3705
|
-
context.numRetries++;
|
|
3706
|
-
clearTimeout(context.timers.reconnect);
|
|
3707
|
-
context.timers.reconnect = effects.scheduleReconnect(getRetryDelay());
|
|
3708
|
-
}
|
|
3709
|
-
function onVisibilityChange(visibilityState) {
|
|
3710
|
-
if (visibilityState === "visible" && context.connection.current.status === "open") {
|
|
3711
|
-
log("Heartbeat after visibility change");
|
|
3712
|
-
heartbeat();
|
|
3713
|
-
}
|
|
3714
|
-
}
|
|
3715
4445
|
function onUpdatePresenceMessage(message) {
|
|
3716
4446
|
if (message.targetActor !== void 0) {
|
|
3717
4447
|
const oldUser = context.others.getUser(message.actor);
|
|
@@ -3761,12 +4491,6 @@ function createRoom(options, config) {
|
|
|
3761
4491
|
}
|
|
3762
4492
|
return { type: "reset" };
|
|
3763
4493
|
}
|
|
3764
|
-
function onNavigatorOnline() {
|
|
3765
|
-
if (context.connection.current.status === "unavailable") {
|
|
3766
|
-
log("Try to reconnect after connectivity change");
|
|
3767
|
-
reconnect();
|
|
3768
|
-
}
|
|
3769
|
-
}
|
|
3770
4494
|
function canUndo() {
|
|
3771
4495
|
return context.undoStack.length > 0;
|
|
3772
4496
|
}
|
|
@@ -3824,11 +4548,7 @@ function createRoom(options, config) {
|
|
|
3824
4548
|
notify(result.updates, batchedUpdatesWrapper);
|
|
3825
4549
|
effects.send(messages);
|
|
3826
4550
|
}
|
|
3827
|
-
function
|
|
3828
|
-
if (event.data === "pong") {
|
|
3829
|
-
clearTimeout(context.timers.pongTimeout);
|
|
3830
|
-
return;
|
|
3831
|
-
}
|
|
4551
|
+
function handleServerMessage(event) {
|
|
3832
4552
|
if (typeof event.data !== "string") {
|
|
3833
4553
|
return;
|
|
3834
4554
|
}
|
|
@@ -3929,140 +4649,6 @@ ${Array.from(traces).join("\n\n")}`
|
|
|
3929
4649
|
notify(updates, doNotBatchUpdates);
|
|
3930
4650
|
});
|
|
3931
4651
|
}
|
|
3932
|
-
function onClose(event) {
|
|
3933
|
-
context.socket = null;
|
|
3934
|
-
clearTimeout(context.timers.flush);
|
|
3935
|
-
clearTimeout(context.timers.reconnect);
|
|
3936
|
-
clearInterval(context.timers.heartbeat);
|
|
3937
|
-
clearTimeout(context.timers.pongTimeout);
|
|
3938
|
-
context.others.clearOthers();
|
|
3939
|
-
batchUpdates(() => {
|
|
3940
|
-
notify({ others: [{ type: "reset" }] }, doNotBatchUpdates);
|
|
3941
|
-
if (event.code >= 4e3 && event.code <= 4100) {
|
|
3942
|
-
updateConnection({ status: "failed" }, doNotBatchUpdates);
|
|
3943
|
-
const error2 = new LiveblocksError(event.reason, event.code);
|
|
3944
|
-
eventHub.error.notify(error2);
|
|
3945
|
-
const delay = getRetryDelay(true);
|
|
3946
|
-
context.numRetries++;
|
|
3947
|
-
if (process.env.NODE_ENV !== "production") {
|
|
3948
|
-
error(
|
|
3949
|
-
`Connection to websocket server closed. Reason: ${error2.message} (code: ${error2.code}). Retrying in ${delay}ms.`
|
|
3950
|
-
);
|
|
3951
|
-
}
|
|
3952
|
-
updateConnection({ status: "unavailable" }, doNotBatchUpdates);
|
|
3953
|
-
clearTimeout(context.timers.reconnect);
|
|
3954
|
-
context.timers.reconnect = effects.scheduleReconnect(delay);
|
|
3955
|
-
} else if (event.code === 4999 /* CLOSE_WITHOUT_RETRY */) {
|
|
3956
|
-
updateConnection({ status: "closed" }, doNotBatchUpdates);
|
|
3957
|
-
} else {
|
|
3958
|
-
const delay = getRetryDelay();
|
|
3959
|
-
context.numRetries++;
|
|
3960
|
-
if (process.env.NODE_ENV !== "production") {
|
|
3961
|
-
warn(
|
|
3962
|
-
`Connection to Liveblocks websocket server closed (code: ${event.code}). Retrying in ${delay}ms.`
|
|
3963
|
-
);
|
|
3964
|
-
}
|
|
3965
|
-
updateConnection({ status: "unavailable" }, doNotBatchUpdates);
|
|
3966
|
-
clearTimeout(context.timers.reconnect);
|
|
3967
|
-
context.timers.reconnect = effects.scheduleReconnect(delay);
|
|
3968
|
-
}
|
|
3969
|
-
});
|
|
3970
|
-
}
|
|
3971
|
-
function updateConnection(connection, batchedUpdatesWrapper) {
|
|
3972
|
-
context.connection.set(connection);
|
|
3973
|
-
batchedUpdatesWrapper(() => {
|
|
3974
|
-
eventHub.connection.notify(connection.status);
|
|
3975
|
-
});
|
|
3976
|
-
}
|
|
3977
|
-
function getRetryDelay(slow = false) {
|
|
3978
|
-
if (slow) {
|
|
3979
|
-
return BACKOFF_RETRY_DELAYS_SLOW[context.numRetries < BACKOFF_RETRY_DELAYS_SLOW.length ? context.numRetries : BACKOFF_RETRY_DELAYS_SLOW.length - 1];
|
|
3980
|
-
}
|
|
3981
|
-
return BACKOFF_RETRY_DELAYS[context.numRetries < BACKOFF_RETRY_DELAYS.length ? context.numRetries : BACKOFF_RETRY_DELAYS.length - 1];
|
|
3982
|
-
}
|
|
3983
|
-
function onError() {
|
|
3984
|
-
}
|
|
3985
|
-
function onOpen() {
|
|
3986
|
-
clearInterval(context.timers.heartbeat);
|
|
3987
|
-
context.timers.heartbeat = effects.startHeartbeatInterval();
|
|
3988
|
-
if (context.connection.current.status === "connecting") {
|
|
3989
|
-
updateConnection(
|
|
3990
|
-
__spreadProps(__spreadValues({}, context.connection.current), { status: "open" }),
|
|
3991
|
-
batchUpdates
|
|
3992
|
-
);
|
|
3993
|
-
context.numRetries = 0;
|
|
3994
|
-
if (context.lastConnectionId !== void 0) {
|
|
3995
|
-
context.buffer.me = {
|
|
3996
|
-
type: "full",
|
|
3997
|
-
data: (
|
|
3998
|
-
// Because state.me.current is a readonly object, we'll have to
|
|
3999
|
-
// make a copy here. Otherwise, type errors happen later when
|
|
4000
|
-
// "patching" my presence.
|
|
4001
|
-
__spreadValues({}, context.me.current)
|
|
4002
|
-
)
|
|
4003
|
-
};
|
|
4004
|
-
tryFlushing();
|
|
4005
|
-
}
|
|
4006
|
-
context.lastConnectionId = context.connection.current.id;
|
|
4007
|
-
if (context.root) {
|
|
4008
|
-
context.buffer.messages.push({ type: 200 /* FETCH_STORAGE */ });
|
|
4009
|
-
}
|
|
4010
|
-
tryFlushing();
|
|
4011
|
-
} else {
|
|
4012
|
-
}
|
|
4013
|
-
}
|
|
4014
|
-
function heartbeat() {
|
|
4015
|
-
if (context.socket === null) {
|
|
4016
|
-
return;
|
|
4017
|
-
}
|
|
4018
|
-
clearTimeout(context.timers.pongTimeout);
|
|
4019
|
-
context.timers.pongTimeout = effects.schedulePongTimeout();
|
|
4020
|
-
if (context.socket.readyState === context.socket.OPEN) {
|
|
4021
|
-
context.socket.send("ping");
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
|
-
function pongTimeout() {
|
|
4025
|
-
log("Pong timeout. Trying to reconnect.");
|
|
4026
|
-
reconnect();
|
|
4027
|
-
}
|
|
4028
|
-
function disconnect() {
|
|
4029
|
-
if (context.socket) {
|
|
4030
|
-
context.socket.removeEventListener("open", onOpen);
|
|
4031
|
-
context.socket.removeEventListener("message", onMessage);
|
|
4032
|
-
context.socket.removeEventListener("close", onClose);
|
|
4033
|
-
context.socket.removeEventListener("error", onError);
|
|
4034
|
-
context.socket.close();
|
|
4035
|
-
context.socket = null;
|
|
4036
|
-
}
|
|
4037
|
-
clearTimeout(context.timers.flush);
|
|
4038
|
-
clearTimeout(context.timers.reconnect);
|
|
4039
|
-
clearInterval(context.timers.heartbeat);
|
|
4040
|
-
clearTimeout(context.timers.pongTimeout);
|
|
4041
|
-
batchUpdates(() => {
|
|
4042
|
-
updateConnection({ status: "closed" }, doNotBatchUpdates);
|
|
4043
|
-
context.others.clearOthers();
|
|
4044
|
-
notify({ others: [{ type: "reset" }] }, doNotBatchUpdates);
|
|
4045
|
-
});
|
|
4046
|
-
for (const eventSource2 of Object.values(eventHub)) {
|
|
4047
|
-
eventSource2.clear();
|
|
4048
|
-
}
|
|
4049
|
-
}
|
|
4050
|
-
function reconnect() {
|
|
4051
|
-
if (context.socket) {
|
|
4052
|
-
context.socket.removeEventListener("open", onOpen);
|
|
4053
|
-
context.socket.removeEventListener("message", onMessage);
|
|
4054
|
-
context.socket.removeEventListener("close", onClose);
|
|
4055
|
-
context.socket.removeEventListener("error", onError);
|
|
4056
|
-
context.socket.close();
|
|
4057
|
-
context.socket = null;
|
|
4058
|
-
}
|
|
4059
|
-
clearTimeout(context.timers.flush);
|
|
4060
|
-
clearTimeout(context.timers.reconnect);
|
|
4061
|
-
clearInterval(context.timers.heartbeat);
|
|
4062
|
-
clearTimeout(context.timers.pongTimeout);
|
|
4063
|
-
updateConnection({ status: "unavailable" }, batchUpdates);
|
|
4064
|
-
connect();
|
|
4065
|
-
}
|
|
4066
4652
|
function tryFlushing() {
|
|
4067
4653
|
const storageOps = context.buffer.storageOperations;
|
|
4068
4654
|
if (storageOps.length > 0) {
|
|
@@ -4071,7 +4657,7 @@ ${Array.from(traces).join("\n\n")}`
|
|
|
4071
4657
|
}
|
|
4072
4658
|
notifyStorageStatus();
|
|
4073
4659
|
}
|
|
4074
|
-
if (
|
|
4660
|
+
if (managedSocket.status !== "open") {
|
|
4075
4661
|
context.buffer.storageOperations = [];
|
|
4076
4662
|
return;
|
|
4077
4663
|
}
|
|
@@ -4091,7 +4677,8 @@ ${Array.from(traces).join("\n\n")}`
|
|
|
4091
4677
|
};
|
|
4092
4678
|
} else {
|
|
4093
4679
|
clearTimeout(context.timers.flush);
|
|
4094
|
-
context.timers.flush =
|
|
4680
|
+
context.timers.flush = setTimeout(
|
|
4681
|
+
tryFlushing,
|
|
4095
4682
|
config.throttleDelay - elapsedMillis
|
|
4096
4683
|
);
|
|
4097
4684
|
}
|
|
@@ -4127,7 +4714,7 @@ ${Array.from(traces).join("\n\n")}`
|
|
|
4127
4714
|
function broadcastEvent(event, options2 = {
|
|
4128
4715
|
shouldQueueEventIfNotReady: false
|
|
4129
4716
|
}) {
|
|
4130
|
-
if (
|
|
4717
|
+
if (managedSocket.status !== "open" && !options2.shouldQueueEventIfNotReady) {
|
|
4131
4718
|
return;
|
|
4132
4719
|
}
|
|
4133
4720
|
context.buffer.messages.push({
|
|
@@ -4264,14 +4851,6 @@ ${Array.from(traces).join("\n\n")}`
|
|
|
4264
4851
|
_addToRealUndoStack(historyOps, batchUpdates);
|
|
4265
4852
|
}
|
|
4266
4853
|
}
|
|
4267
|
-
function simulateCloseWebsocket() {
|
|
4268
|
-
if (context.socket) {
|
|
4269
|
-
context.socket = null;
|
|
4270
|
-
}
|
|
4271
|
-
}
|
|
4272
|
-
function simulateSendCloseEvent(event) {
|
|
4273
|
-
onClose(event);
|
|
4274
|
-
}
|
|
4275
4854
|
function getStorageStatus() {
|
|
4276
4855
|
if (_getInitialStatePromise === null) {
|
|
4277
4856
|
return "not-loaded";
|
|
@@ -4311,28 +4890,39 @@ ${Array.from(traces).join("\n\n")}`
|
|
|
4311
4890
|
return context.buffer;
|
|
4312
4891
|
},
|
|
4313
4892
|
// prettier-ignore
|
|
4314
|
-
get
|
|
4315
|
-
return context.
|
|
4893
|
+
get undoStack() {
|
|
4894
|
+
return context.undoStack;
|
|
4895
|
+
},
|
|
4896
|
+
// prettier-ignore
|
|
4897
|
+
get nodeCount() {
|
|
4898
|
+
return context.nodes.size;
|
|
4316
4899
|
},
|
|
4317
4900
|
// prettier-ignore
|
|
4318
|
-
onClose,
|
|
4319
|
-
onMessage,
|
|
4320
|
-
authenticationSuccess,
|
|
4321
|
-
onNavigatorOnline,
|
|
4322
|
-
simulateCloseWebsocket,
|
|
4323
|
-
simulateSendCloseEvent,
|
|
4324
|
-
onVisibilityChange,
|
|
4325
|
-
getUndoStack: () => context.undoStack,
|
|
4326
|
-
getItemsCount: () => context.nodes.size,
|
|
4327
|
-
connect,
|
|
4328
|
-
disconnect,
|
|
4329
4901
|
// Support for the Liveblocks browser extension
|
|
4330
4902
|
getSelf_forDevTools: () => selfAsTreeNode.current,
|
|
4331
|
-
getOthers_forDevTools: () => others_forDevTools.current
|
|
4903
|
+
getOthers_forDevTools: () => others_forDevTools.current,
|
|
4904
|
+
// prettier-ignore
|
|
4905
|
+
send: {
|
|
4906
|
+
// These exist only for our E2E testing app
|
|
4907
|
+
explicitClose: (event) => managedSocket._privateSend({ type: "EXPLICIT_SOCKET_CLOSE", event }),
|
|
4908
|
+
implicitClose: () => managedSocket._privateSend({ type: "PONG_TIMEOUT" }),
|
|
4909
|
+
connect: () => managedSocket.connect(),
|
|
4910
|
+
disconnect: () => managedSocket.disconnect(),
|
|
4911
|
+
// XXX Do we really need disconnect() now that we have destroy()? Seems still useful to reset the machine, but also... YAGNI?
|
|
4912
|
+
destroy: () => managedSocket.destroy(),
|
|
4913
|
+
/**
|
|
4914
|
+
* This one looks differently from the rest, because receiving messages
|
|
4915
|
+
* is handled orthorgonally from all other possible events above,
|
|
4916
|
+
* because it does not matter what the connectivity state of the
|
|
4917
|
+
* machine is, so there won't be an explicit state machine transition
|
|
4918
|
+
* needed for this event.
|
|
4919
|
+
*/
|
|
4920
|
+
incomingMessage: handleServerMessage
|
|
4921
|
+
}
|
|
4332
4922
|
},
|
|
4333
4923
|
id: config.roomId,
|
|
4334
4924
|
subscribe: makeClassicSubscribeFn(events),
|
|
4335
|
-
reconnect,
|
|
4925
|
+
reconnect: () => managedSocket.reconnect(),
|
|
4336
4926
|
// Presence
|
|
4337
4927
|
updatePresence,
|
|
4338
4928
|
broadcastEvent,
|
|
@@ -4404,10 +4994,6 @@ function makeClassicSubscribeFn(events) {
|
|
|
4404
4994
|
return events.connection.subscribe(
|
|
4405
4995
|
callback
|
|
4406
4996
|
);
|
|
4407
|
-
case "storage":
|
|
4408
|
-
return events.storage.subscribe(
|
|
4409
|
-
callback
|
|
4410
|
-
);
|
|
4411
4997
|
case "history":
|
|
4412
4998
|
return events.history.subscribe(callback);
|
|
4413
4999
|
case "storage-status":
|
|
@@ -4443,67 +5029,66 @@ function makeClassicSubscribeFn(events) {
|
|
|
4443
5029
|
function isRoomEventName(value) {
|
|
4444
5030
|
return value === "my-presence" || value === "others" || value === "event" || value === "error" || value === "connection" || value === "history" || value === "storage-status";
|
|
4445
5031
|
}
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
"To use Liveblocks client in a non-dom environment, you need to provide a WebSocket polyfill."
|
|
4456
|
-
);
|
|
4457
|
-
}
|
|
4458
|
-
const ws = WebSocketPolyfill || WebSocket;
|
|
4459
|
-
return (token) => {
|
|
5032
|
+
function makeCreateSocketDelegateForRoom(liveblocksServer, WebSocketPolyfill) {
|
|
5033
|
+
return (richToken) => {
|
|
5034
|
+
const ws = WebSocketPolyfill || WebSocket;
|
|
5035
|
+
if (typeof WebSocket === "undefined" && WebSocketPolyfill === void 0) {
|
|
5036
|
+
throw new Error(
|
|
5037
|
+
"To use Liveblocks client in a non-dom environment, you need to provide a WebSocket polyfill."
|
|
5038
|
+
);
|
|
5039
|
+
}
|
|
5040
|
+
const token = richToken.raw;
|
|
4460
5041
|
return new ws(
|
|
4461
5042
|
`${liveblocksServer}/?token=${token}&version=${// prettier-ignore
|
|
4462
5043
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
4463
5044
|
// @ts-ignore (__PACKAGE_VERSION__ will be injected by the build script)
|
|
4464
5045
|
true ? (
|
|
4465
5046
|
/* istanbul ignore next */
|
|
4466
|
-
"1.0
|
|
5047
|
+
"1.1.0-fsm1"
|
|
4467
5048
|
) : "dev"}`
|
|
4468
5049
|
);
|
|
4469
5050
|
};
|
|
4470
5051
|
}
|
|
4471
|
-
function
|
|
5052
|
+
function makeAuthDelegateForRoom(roomId, authentication, fetchPolyfill) {
|
|
4472
5053
|
if (authentication.type === "public") {
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
return (room) => fetchAuthEndpoint(
|
|
4479
|
-
fetchPolyfill || /* istanbul ignore next */
|
|
4480
|
-
fetch,
|
|
4481
|
-
authentication.url,
|
|
4482
|
-
{
|
|
4483
|
-
room,
|
|
4484
|
-
publicApiKey: authentication.publicApiKey
|
|
5054
|
+
return () => __async(this, null, function* () {
|
|
5055
|
+
if (typeof window === "undefined" && fetchPolyfill === void 0) {
|
|
5056
|
+
throw new Error(
|
|
5057
|
+
"To use Liveblocks client in a non-dom environment with a publicApiKey, you need to provide a fetch polyfill."
|
|
5058
|
+
);
|
|
4485
5059
|
}
|
|
4486
|
-
|
|
5060
|
+
return fetchAuthEndpoint(
|
|
5061
|
+
fetchPolyfill || /* istanbul ignore next */
|
|
5062
|
+
fetch,
|
|
5063
|
+
authentication.url,
|
|
5064
|
+
{
|
|
5065
|
+
room: roomId,
|
|
5066
|
+
publicApiKey: authentication.publicApiKey
|
|
5067
|
+
}
|
|
5068
|
+
).then(({ token }) => parseRoomAuthToken(token));
|
|
5069
|
+
});
|
|
4487
5070
|
}
|
|
4488
5071
|
if (authentication.type === "private") {
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
5072
|
+
return () => __async(this, null, function* () {
|
|
5073
|
+
if (typeof window === "undefined" && fetchPolyfill === void 0) {
|
|
5074
|
+
throw new Error(
|
|
5075
|
+
"To use Liveblocks client in a non-dom environment with a url as auth endpoint, you need to provide a fetch polyfill."
|
|
5076
|
+
);
|
|
5077
|
+
}
|
|
5078
|
+
return fetchAuthEndpoint(fetchPolyfill || fetch, authentication.url, {
|
|
5079
|
+
room: roomId
|
|
5080
|
+
}).then(({ token }) => parseRoomAuthToken(token));
|
|
4496
5081
|
});
|
|
4497
5082
|
}
|
|
4498
5083
|
if (authentication.type === "custom") {
|
|
4499
|
-
return (
|
|
4500
|
-
const response = yield authentication.callback(
|
|
5084
|
+
return () => __async(this, null, function* () {
|
|
5085
|
+
const response = yield authentication.callback(roomId);
|
|
4501
5086
|
if (!response || !response.token) {
|
|
4502
5087
|
throw new Error(
|
|
4503
5088
|
'Authentication error. We expect the authentication callback to return a token, but it does not. Hint: the return value should look like: { token: "..." }'
|
|
4504
5089
|
);
|
|
4505
5090
|
}
|
|
4506
|
-
return response;
|
|
5091
|
+
return parseRoomAuthToken(response.token);
|
|
4507
5092
|
});
|
|
4508
5093
|
}
|
|
4509
5094
|
throw new Error("Internal error. Unexpected authentication type");
|
|
@@ -4520,9 +5105,13 @@ function fetchAuthEndpoint(fetch2, endpoint, body) {
|
|
|
4520
5105
|
body: JSON.stringify(body)
|
|
4521
5106
|
});
|
|
4522
5107
|
if (!res.ok) {
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
5108
|
+
if (res.status === 401 || res.status === 403) {
|
|
5109
|
+
throw new UnauthorizedError(yield res.text());
|
|
5110
|
+
} else {
|
|
5111
|
+
throw new AuthenticationError(
|
|
5112
|
+
`Expected a status 200 but got ${res.status} when doing a POST request on "${endpoint}"`
|
|
5113
|
+
);
|
|
5114
|
+
}
|
|
4526
5115
|
}
|
|
4527
5116
|
let data;
|
|
4528
5117
|
try {
|
|
@@ -4604,7 +5193,7 @@ function createClient(options) {
|
|
|
4604
5193
|
}
|
|
4605
5194
|
global.atob = clientOptions.polyfills.atob;
|
|
4606
5195
|
}
|
|
4607
|
-
newRoom.__internal.connect();
|
|
5196
|
+
newRoom.__internal.send.connect();
|
|
4608
5197
|
}
|
|
4609
5198
|
return newRoom;
|
|
4610
5199
|
}
|
|
@@ -4612,25 +5201,10 @@ function createClient(options) {
|
|
|
4612
5201
|
unlinkDevTools(roomId);
|
|
4613
5202
|
const room = rooms.get(roomId);
|
|
4614
5203
|
if (room !== void 0) {
|
|
4615
|
-
room.__internal.
|
|
5204
|
+
room.__internal.send.destroy();
|
|
4616
5205
|
rooms.delete(roomId);
|
|
4617
5206
|
}
|
|
4618
5207
|
}
|
|
4619
|
-
if (typeof window !== "undefined" && // istanbul ignore next: React Native environment doesn't implement window.addEventListener
|
|
4620
|
-
typeof window.addEventListener !== "undefined") {
|
|
4621
|
-
window.addEventListener("online", () => {
|
|
4622
|
-
for (const [, room] of rooms) {
|
|
4623
|
-
room.__internal.onNavigatorOnline();
|
|
4624
|
-
}
|
|
4625
|
-
});
|
|
4626
|
-
}
|
|
4627
|
-
if (typeof document !== "undefined") {
|
|
4628
|
-
document.addEventListener("visibilitychange", () => {
|
|
4629
|
-
for (const [, room] of rooms) {
|
|
4630
|
-
room.__internal.onVisibilityChange(document.visibilityState);
|
|
4631
|
-
}
|
|
4632
|
-
});
|
|
4633
|
-
}
|
|
4634
5208
|
return {
|
|
4635
5209
|
getRoom,
|
|
4636
5210
|
enter,
|
|
@@ -5041,6 +5615,19 @@ function shallow(a, b) {
|
|
|
5041
5615
|
return shallowObj(a, b);
|
|
5042
5616
|
}
|
|
5043
5617
|
|
|
5618
|
+
// src/types/IWebSocket.ts
|
|
5619
|
+
var WebsocketCloseCodes = /* @__PURE__ */ ((WebsocketCloseCodes2) => {
|
|
5620
|
+
WebsocketCloseCodes2[WebsocketCloseCodes2["CLOSE_ABNORMAL"] = 1006] = "CLOSE_ABNORMAL";
|
|
5621
|
+
WebsocketCloseCodes2[WebsocketCloseCodes2["INVALID_MESSAGE_FORMAT"] = 4e3] = "INVALID_MESSAGE_FORMAT";
|
|
5622
|
+
WebsocketCloseCodes2[WebsocketCloseCodes2["NOT_ALLOWED"] = 4001] = "NOT_ALLOWED";
|
|
5623
|
+
WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_MESSAGES_PER_SECONDS"] = 4002] = "MAX_NUMBER_OF_MESSAGES_PER_SECONDS";
|
|
5624
|
+
WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_CONCURRENT_CONNECTIONS"] = 4003] = "MAX_NUMBER_OF_CONCURRENT_CONNECTIONS";
|
|
5625
|
+
WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP"] = 4004] = "MAX_NUMBER_OF_MESSAGES_PER_DAY_PER_APP";
|
|
5626
|
+
WebsocketCloseCodes2[WebsocketCloseCodes2["MAX_NUMBER_OF_CONCURRENT_CONNECTIONS_PER_ROOM"] = 4005] = "MAX_NUMBER_OF_CONCURRENT_CONNECTIONS_PER_ROOM";
|
|
5627
|
+
WebsocketCloseCodes2[WebsocketCloseCodes2["CLOSE_WITHOUT_RETRY"] = 4999] = "CLOSE_WITHOUT_RETRY";
|
|
5628
|
+
return WebsocketCloseCodes2;
|
|
5629
|
+
})(WebsocketCloseCodes || {});
|
|
5630
|
+
|
|
5044
5631
|
|
|
5045
5632
|
|
|
5046
5633
|
|