@croquet/microverse-library
Version:
An npm package version of Microverse
150 lines (121 loc) • 5.22 kB
JavaScript
// Copyright 2022 by Croquet Corporation, Inc. All Rights Reserved.
// https://croquet.io
// info@croquet.io
/*
It is useful to be able to "elect" one peer to take a certain action
on its view side. For example, the elected view may fetch an external
data stream from internet and feed data in the shared session for all
peers.
This module provides a generic way to elect one peer. It is expected be used with another behavior modules
that actually takes an action (such as fetching data) based on the
election result. See examples in behaviors/default/bitcoinTraker.js
and behaviors/default/flightTracker.js.
*/
/*
ElectedActor keeps track of the currently elected view. It picks the first
of all players currently in the world as the leader of the peers.
setup() may be called multiple times in its life cycle. But since
firstEligibleView prefers the currently elected view, it is safe to call again.
*/
// the following import statement is solely for the type checking and
// autocompletion features in IDE. A Behavior cannot inherit from
// another behavior or a base class but can use the methods and
// properties of the card to which it is installed.
// The prototype classes ActorBehavior and PawnBehavior provide
// the features defined at the card object.
import {ActorBehavior, PawnBehavior} from "../PrototypeBehavior";
class ElectedActor extends ActorBehavior {
setup() {
this.subscribe("playerManager", "create", "updateElectedView");
this.subscribe("playerManager", "destroy", "updateElectedView");
this.subscribe("playerManager", "enter", "updateElectedView");
this.subscribe("playerManager", "leave", "updateElectedView");
this.updateElectedView();
}
// previous versions of this behavior have used these methods
viewJoined() { this.unsubscribe(this.sessionId, "view-join"); }
viewExited() { this.unsubscribe(this.sessionId, "view-exit"); }
firstEligibleView() {
const manager = this.service("PlayerManager");
// prefer currently elected view, if player is in the world
const current = manager.player(this.electedViewId);
if (current && current.inWorld) return this.electedViewId;
// prefer in-world players (in particular, no spectators)
for (const [viewId, player] of manager.players) {
if (player.inWorld) return viewId;
}
// otherwise, pick the first player
for (const [viewId] of manager.players) {
return viewId;
}
}
updateElectedView() {
const electedBefore = this.electedViewId;
const electedAfter = this.firstEligibleView();
if (electedBefore !== electedAfter) {
this.electedViewId = electedAfter;
this.say("view-elected", electedAfter);
}
}
}
/*
ElectedPawn publishes an event when the elected peer changes. The
electionStatusRequested() method is called when the accompanying
behavior is being setup(), so that the view can check if the view is
already elected.
For other cases, the onViewElected() method is called when the model
chooses a new leader and checks if the peer was either newly unelected
or elected. It publishes the event handleElected and handleUnelected,
and the accompanying behavior attached to the same object is expected
to handle the event.
*/
class ElectedPawn extends PawnBehavior {
setup() {
if (this.electedViewId === undefined) {
this.electedViewId = "";
this.onViewElected(this.actorCall("ElectedActor", "electedViewId"));
}
this.listen({event: "view-elected", handling: "oncePerFrame"}, "onViewElected");
this.listen("handleElected", "handleElected");
this.listen("handleUnelected", "handleUnelected");
this.listen("electionStatusRequested", "electionStatusRequested");
}
isElected() { return this.viewId === this.electedViewId; }
electionStatusRequested() {
if (this.isElected()) {
this.say("handleElected");
}
}
onViewElected(viewId) {
const wasElected = this.isElected();
this.electedViewId = viewId;
if (wasElected !== this.isElected()) {
if (wasElected) {
this.say("handleUnelected", {from: this.electedViewId, to: viewId});
} else {
this.say("handleElected", {from: this.electedViewId, to: viewId});
}
} else {
console.log('%cView Elected: %s (this view %s %s)', 'color: #CC0', this.electedViewId || '<none>', this.viewId,
wasElected ? 'still elected ✅' : 'unaffected ❌');
}
}
handleElected() {
console.log('%cView Elected: %s (this view %s elected ✅)', 'color: #0C0', this.electedViewId || '<none>', this.viewId);
}
handleUnelected() {
console.log('%cView Elected: %s (this view %s unelected ❌)', 'color: #C00', this.electedViewId || '<none>', this.viewId);
}
teardown() {
this.onViewElected("");
}
}
export default {
modules: [
{
name: "Elected",
actorBehaviors: [ElectedActor],
pawnBehaviors: [ElectedPawn]
}
]
}