teletype-client-modified
Version:
I have modified the library developed by Atom to make it compatible with non-browser facility. I am using it only for experimentation purposes. I will remove this package post my experiment and raise a PR at Atom to finally merge this package so that this
730 lines (614 loc) • 25.1 kB
JavaScript
const assert = require('assert')
const {CompositeDisposable} = require('event-kit')
const Router = require('./router')
const BufferProxy = require('./buffer-proxy')
const EditorProxy = require('./editor-proxy')
const EditorProxyMetadata = require('./editor-proxy-metadata')
const StarOverlayNetwork = require('./star-overlay-network')
const Messages = require('./teletype-client_pb')
const NullPortalDelegate = require('./null-portal-delegate')
const FollowState = require('./follow-state')
module.exports =
class Portal {
constructor ({id, hostPeerId, siteId, peerPool, connectionTimeout, tetherDisconnectWindow}) {
this.id = id
this.hostPeerId = hostPeerId
this.siteId = siteId
this.tetherDisconnectWindow = tetherDisconnectWindow
this.isHost = isHostSiteId(this.siteId)
this.siteIdsByPeerId = new Map()
this.peerIdsBySiteId = new Map()
this.editorProxiesById = new Map()
this.bufferProxiesById = new Map()
this.activeEditorProxiesBySiteId = new Map()
this.activeEditorProxySubscriptions = new CompositeDisposable()
this.tethersByFollowerId = new Map()
this.disposables = new CompositeDisposable()
this.disposed = false
this.delegate = new NullPortalDelegate()
this.peerPool = peerPool
this.network = new StarOverlayNetwork({id, isHub: this.isHost, peerPool, connectionTimeout})
this.router = new Router(this.network)
if (this.isHost) {
this.bindPeerIdToSiteId(this.network.getPeerId(), this.siteId)
this.nextSiteId = 2
this.nextBufferId = 1
this.nextEditorId = 1
this.disposables.add(
this.router.onRequest(`/portals/${id}`, this.receiveSubscription.bind(this))
)
} else {
this.editorProxiesMetadataById = new Map()
}
this.disposables.add(
this.router.onNotification(`/portals/${id}`, this.receiveUpdate.bind(this)),
this.network.onMemberLeave(this.siteDidLeave.bind(this))
)
}
getLocalSiteId () {
return this.siteId
}
dispose () {
this.editorProxiesById.forEach((editorProxy) => {
editorProxy.dispose()
})
this.bufferProxiesById.forEach((bufferProxy) => {
bufferProxy.dispose()
})
this.disposables.dispose()
this.router.dispose()
this.network.dispose()
this.delegate.dispose()
this.disposed = true
}
async setDelegate (delegate) {
this.delegate = delegate || new NullPortalDelegate()
await this.delegate.updateTether(
this.resolveFollowState(),
this.getLocalActiveEditorProxy(),
this.resolveLeaderPosition()
)
}
async initialize () {
try {
this.disposables.add(await this.peerPool.listen())
} catch (error) {
this.dispose()
throw error
}
}
async join () {
try {
this.networkConnectionPromise = this.network.connectTo(this.hostPeerId)
await this.networkConnectionPromise
} catch (error) {
this.dispose()
throw error
}
const rawResponse = await this.router.request({recipientId: this.hostPeerId, channelId: `/portals/${this.id}`})
const response = Messages.PortalSubscriptionResponse.deserializeBinary(rawResponse.body)
response.getSiteIdsByPeerIdMap().forEach((siteId, peerId) => {
this.bindPeerIdToSiteId(peerId, siteId)
})
this.siteId = this.siteIdsByPeerId.get(this.network.getPeerId())
response.getEditorProxiesMetadataList().forEach((editorProxyMetadataMessage) => {
this.deserializeEditorProxyMetadata(editorProxyMetadataMessage)
})
const tethers = response.getTethersList()
for (let i = 0; i < tethers.length; i++) {
const tether = tethers[i]
this.tethersByFollowerId.set(tether.getFollowerSiteId(), {
leaderId: tether.getLeaderSiteId(),
state: tether.getState()
})
}
const activeBufferProxies = response.getActiveBufferProxiesList()
for (let i = 0; i < activeBufferProxies.length; i++) {
const bufferProxyMessage = activeBufferProxies[i]
this.deserializeBufferProxy(bufferProxyMessage)
}
const activeEditorProxies = response.getActiveEditorProxiesList()
for (let i = 0; i < activeEditorProxies.length; i++) {
const editorProxyMessage = activeEditorProxies[i]
this.deserializeEditorProxy(editorProxyMessage)
}
response.getActiveEditorProxyIdsBySiteIdMap().forEach((editorProxyId, siteId) => {
const editorProxy = this.editorProxiesById.get(editorProxyId)
this.activeEditorProxiesBySiteId.set(siteId, editorProxy)
})
this.follow(1)
}
createBufferProxy (props) {
const id = this.nextBufferId++
const bufferProxy = new BufferProxy(Object.assign({
id,
hostPeerId: this.hostPeerId,
siteId: this.siteId,
router: this.router,
didDispose: () => this.bufferProxiesById.delete(id)
}, props))
this.bufferProxiesById.set(id, bufferProxy)
return bufferProxy
}
async findOrFetchBufferProxy (id) {
if (id == null) return
let bufferProxy = this.bufferProxiesById.get(id)
if (!bufferProxy && !this.isHost) {
bufferProxy = await this.fetchBufferProxy(id)
}
return bufferProxy
}
async fetchBufferProxy (id) {
const response = await this.router.request({recipientId: this.hostPeerId, channelId: `/buffers/${id}`})
if (response.ok) {
const bufferProxyMessage = Messages.BufferProxy.deserializeBinary(response.body)
return this.deserializeBufferProxy(bufferProxyMessage)
}
}
deserializeBufferProxy (message) {
const bufferProxy = BufferProxy.deserialize(message, {
router: this.router,
hostPeerId: this.hostPeerId,
siteId: this.siteId,
didDispose: () => this.bufferProxiesById.delete(bufferProxy.id)
})
this.bufferProxiesById.set(bufferProxy.id, bufferProxy)
return bufferProxy
}
createEditorProxy (props) {
const id = this.nextEditorId++
const editorProxy = new EditorProxy(Object.assign({
id,
siteId: this.siteId,
router: this.router,
tetherDisconnectWindow: this.tetherDisconnectWindow,
didDispose: () => this.editorProxiesById.delete(id),
portal: this
}, props))
this.editorProxiesById.set(id, editorProxy)
this.broadcastEditorProxyCreation(editorProxy)
return editorProxy
}
async findOrFetchEditorProxy (id) {
let editorProxy = this.editorProxiesById.get(id)
if (!editorProxy && !this.isHost) {
editorProxy = await this.fetchEditorProxy(id)
}
return editorProxy
}
async fetchEditorProxy (id) {
if (id == null) return
const response = await this.router.request({recipientId: this.hostPeerId, channelId: `/editors/${id}`})
if (!response.ok) return
const editorProxyMessage = Messages.EditorProxy.deserializeBinary(response.body)
const bufferProxy = await this.findOrFetchBufferProxy(editorProxyMessage.getBufferProxyId())
if (bufferProxy) {
const editorProxy = this.deserializeEditorProxy(editorProxyMessage)
editorProxy.hideSelections()
return editorProxy
}
}
deserializeEditorProxy (message) {
const editorProxy = EditorProxy.deserialize(message, {
router: this.router,
siteId: this.siteId,
bufferProxiesById: this.bufferProxiesById,
didDispose: () => {
this.delegate.didChangeEditorProxies()
this.editorProxiesById.delete(editorProxy.id)
},
portal: this
})
this.editorProxiesById.set(editorProxy.id, editorProxy)
const editorProxyMetadata = this.editorProxiesMetadataById.get(editorProxy.id)
if (editorProxyMetadata) editorProxyMetadata.dispose()
return editorProxy
}
deserializeEditorProxyMetadata (message) {
const editorProxyMetadata = EditorProxyMetadata.deserialize(message, {
siteId: this.siteId,
router: this.router,
didDispose: () => {
if (!this.editorProxiesById.has(editorProxyMetadata.id)) {
this.delegate.didChangeEditorProxies()
}
this.editorProxiesMetadataById.delete(editorProxyMetadata.id)
}
})
this.editorProxiesMetadataById.set(editorProxyMetadata.id, editorProxyMetadata)
return editorProxyMetadata
}
activateEditorProxy (newEditorProxy) {
const oldEditorProxy = this.getLocalActiveEditorProxy()
if (newEditorProxy != oldEditorProxy) {
this.unfollow()
if (oldEditorProxy) oldEditorProxy.hideSelections()
if (newEditorProxy) newEditorProxy.showSelections()
this.activeEditorProxiesBySiteId.set(this.siteId, newEditorProxy)
this.subscribeToEditorProxyChanges(newEditorProxy)
this.broadcastEditorProxySwitch(newEditorProxy)
this.updateActivePositions()
}
}
getLocalActiveEditorProxy () {
return this.activeEditorProxyForSiteId(this.siteId)
}
activeEditorProxyForSiteId (siteId) {
const leaderId = this.resolveLeaderSiteId(siteId)
const followState = this.resolveFollowState(siteId)
if (followState === FollowState.RETRACTED) {
return this.activeEditorProxiesBySiteId.get(leaderId)
} else {
return this.activeEditorProxiesBySiteId.get(siteId)
}
}
getEditorProxiesMetadata () {
const editorProxiesMetadata = []
this.editorProxiesMetadataById.forEach((editorProxyMetadata) => {
editorProxiesMetadata.push(editorProxyMetadata)
})
this.editorProxiesById.forEach((editorProxy) => {
editorProxiesMetadata.push(editorProxy.getMetadata())
})
return editorProxiesMetadata
}
getEditorProxyMetadata (editorProxyId) {
const editorProxy = this.editorProxiesById.get(editorProxyId)
if (editorProxy) {
return editorProxy.getMetadata()
} else {
return this.editorProxiesMetadataById.get(editorProxyId)
}
}
getActiveSiteIds () {
const connectedMemberIds = this.network.getMemberIds()
const activeSiteIds = []
for (let i = 0; i < connectedMemberIds.length; i++) {
const memberId = connectedMemberIds[i]
const siteId = this.siteIdsByPeerId.get(memberId)
if (siteId != null) activeSiteIds.push(siteId)
}
return activeSiteIds
}
getSiteIdentity (siteId) {
const peerId = this.peerIdsBySiteId.get(siteId)
return this.network.getMemberIdentity(peerId)
}
siteDidLeave ({peerId, connectionLost}) {
const siteId = this.siteIdsByPeerId.get(peerId)
this.editorProxiesById.forEach((editorProxy) => {
if (isHostSiteId(siteId)) {
editorProxy.hostDidDisconnect()
} else {
editorProxy.siteDidDisconnect(siteId)
}
})
if (isHostSiteId(siteId)) {
if (connectionLost) {
this.delegate.hostDidLoseConnection()
} else {
this.delegate.hostDidClosePortal()
}
this.dispose()
} else {
this.delegate.siteDidLeave(siteId)
}
const tether = this.tethersByFollowerId.get(this.siteId)
if (tether && siteId === tether.leaderId) {
this.unfollow()
}
this.tethersByFollowerId.delete(siteId)
this.activeEditorProxiesBySiteId.delete(siteId)
this.updateActivePositions()
}
receiveSubscription ({senderId, requestId}) {
this.assignNewSiteId(senderId)
this.sendSubscriptionResponse(requestId)
this.delegate.siteDidJoin(this.siteIdsByPeerId.get(senderId))
this.updateActivePositions()
}
assignNewSiteId (peerId) {
const siteId = this.nextSiteId++
this.bindPeerIdToSiteId(peerId, siteId)
const siteAssignmentMessage = new Messages.PortalUpdate.SiteAssignment()
siteAssignmentMessage.setPeerId(peerId)
siteAssignmentMessage.setSiteId(siteId)
const updateMessage = new Messages.PortalUpdate()
updateMessage.setSiteAssignment(siteAssignmentMessage)
this.router.notify({channelId: `/portals/${this.id}`, body: updateMessage.serializeBinary()})
}
sendSubscriptionResponse (requestId) {
const response = new Messages.PortalSubscriptionResponse()
this.siteIdsByPeerId.forEach((siteId, peerId) => {
response.getSiteIdsByPeerIdMap().set(peerId, siteId)
})
const editorProxiesMetadata = []
this.editorProxiesById.forEach((editorProxy) => {
editorProxiesMetadata.push(editorProxy.getMetadata().serialize())
})
response.setEditorProxiesMetadataList(editorProxiesMetadata)
const activeBufferProxiesById = new Map()
const activeEditorProxiesById = new Map()
this.activeEditorProxiesBySiteId.forEach((editorProxy, siteId) => {
if (editorProxy) {
const {bufferProxy} = editorProxy
if (!activeBufferProxiesById.has(bufferProxy.id)) {
activeBufferProxiesById.set(bufferProxy.id, bufferProxy.serialize())
}
if (!activeEditorProxiesById.has(editorProxy.id)) {
activeEditorProxiesById.set(editorProxy.id, editorProxy.serialize())
}
response.getActiveEditorProxyIdsBySiteIdMap().set(siteId, editorProxy.id)
}
})
response.setActiveBufferProxiesList(Array.from(activeBufferProxiesById.values()))
response.setActiveEditorProxiesList(Array.from(activeEditorProxiesById.values()))
const tethers = []
this.tethersByFollowerId.forEach((tether, followerId) => {
const tetherMessage = new Messages.Tether()
tetherMessage.setFollowerSiteId(followerId)
tetherMessage.setLeaderSiteId(tether.leaderId)
tetherMessage.setState(tether.state)
tethers.push(tetherMessage)
})
response.setTethersList(tethers)
this.router.respond({requestId, body: response.serializeBinary()})
}
async receiveUpdate ({senderId, body}) {
const updateMessage = Messages.PortalUpdate.deserializeBinary(body)
if (updateMessage.hasEditorProxyCreation()) {
this.receiveEditorProxyCreation(updateMessage.getEditorProxyCreation())
} else if (updateMessage.hasEditorProxySwitch()) {
const senderSiteId = this.siteIdsByPeerId.get(senderId)
await this.receiveEditorProxySwitch(senderSiteId, updateMessage.getEditorProxySwitch())
} else if (updateMessage.hasSiteAssignment()) {
this.receiveSiteAssignment(updateMessage.getSiteAssignment())
} else if (updateMessage.hasTetherUpdate()) {
this.receiveTetherUpdate(updateMessage.getTetherUpdate())
} else {
throw new Error('Received unknown update message')
}
}
receiveEditorProxyCreation (editorProxyCreationMessage) {
this.deserializeEditorProxyMetadata(editorProxyCreationMessage.getEditorProxyMetadata())
this.delegate.didChangeEditorProxies()
}
async receiveEditorProxySwitch (senderSiteId, editorProxySwitch) {
const editorProxyId = editorProxySwitch.getEditorProxyId()
const editorProxy = await this.findOrFetchEditorProxy(editorProxyId)
this.activeEditorProxiesBySiteId.set(senderSiteId, editorProxy)
if (senderSiteId === this.resolveLeaderSiteId()) this.leaderDidUpdate()
this.updateActivePositions()
}
receiveSiteAssignment (siteAssignment) {
const siteId = siteAssignment.getSiteId()
const peerId = siteAssignment.getPeerId()
this.bindPeerIdToSiteId(peerId, siteId)
if (this.network.getPeerId() !== peerId) {
this.delegate.siteDidJoin(siteId)
}
}
receiveTetherUpdate (tetherUpdate) {
const oldResolvedLeaderId = this.resolveLeaderSiteId()
const oldResolvedState = this.resolveFollowState()
const followerSiteId = tetherUpdate.getFollowerSiteId()
const leaderSiteId = tetherUpdate.getLeaderSiteId()
const tetherState = tetherUpdate.getState()
this.tethersByFollowerId.set(followerSiteId, {leaderId: leaderSiteId, state: tetherState})
const newResolvedLeaderId = this.resolveLeaderSiteId()
const newResolvedState = this.resolveFollowState()
this.didChangeTetherState({oldResolvedState, oldResolvedLeaderId, newResolvedState, newResolvedLeaderId})
}
bindPeerIdToSiteId (peerId, siteId) {
this.siteIdsByPeerId.set(peerId, siteId)
this.peerIdsBySiteId.set(siteId, peerId)
}
subscribeToEditorProxyChanges (editorProxy) {
this.activeEditorProxySubscriptions.dispose()
this.activeEditorProxySubscriptions = new CompositeDisposable()
if (editorProxy) {
this.activeEditorProxySubscriptions.add(editorProxy.onDidScroll(this.activeEditorDidScroll.bind(this)))
this.activeEditorProxySubscriptions.add(editorProxy.onDidUpdateLocalSelections(this.activeEditorDidUpdateLocalSelections.bind(this)))
this.activeEditorProxySubscriptions.add(editorProxy.onDidUpdateRemoteSelections(this.activeEditorDidUpdateRemoteSelections.bind(this)))
this.activeEditorProxySubscriptions.add(editorProxy.bufferProxy.onDidUpdateText(this.activeEditorDidUpdateText.bind(this)))
}
}
activeEditorDidUpdateLocalSelections ({initialUpdate}) {
this.lastLocalUpdateAt = Date.now()
if (!initialUpdate && this.resolveFollowState() === FollowState.RETRACTED) {
const localCursorPosition = this.getLocalActiveEditorProxy().cursorPositionForSiteId(this.siteId)
const leaderPosition = this.resolveLeaderPosition()
if (localCursorPosition && leaderPosition && !pointsEqual(localCursorPosition, leaderPosition)) {
this.extendTether()
}
}
this.updateActivePositions()
}
activeEditorDidUpdateRemoteSelections ({selectionLayerIdsBySiteId, initialUpdate}) {
const leaderDidChangeSelections = selectionLayerIdsBySiteId.has(this.resolveLeaderSiteId())
if (!initialUpdate && leaderDidChangeSelections) {
this.leaderDidUpdate()
}
this.updateActivePositions()
}
activeEditorDidUpdateText ({remote}) {
if (this.resolveFollowState() === FollowState.RETRACTED) {
if (remote) {
this.delegate.updateTether(FollowState.RETRACTED, this.getLocalActiveEditorProxy(), this.resolveLeaderPosition())
} else {
this.lastLocalUpdateAt = Date.now()
this.extendTether()
}
}
this.updateActivePositions()
}
activeEditorDidScroll () {
const leaderPosition = this.resolveLeaderPosition()
if (leaderPosition && this.getLocalActiveEditorProxy().isScrollNeededToViewPosition(leaderPosition)) {
this.unfollow()
}
}
follow (leaderSiteId) {
this.setFollowState(FollowState.RETRACTED, leaderSiteId)
}
unfollow () {
this.setFollowState(FollowState.DISCONNECTED)
}
extendTether () {
this.setFollowState(FollowState.EXTENDED)
}
retractTether () {
this.setFollowState(FollowState.RETRACTED)
}
getFollowedSiteId () {
if (this.resolveFollowState() === FollowState.DISCONNECTED) {
return null
} else {
return this.tethersByFollowerId.get(this.siteId).leaderId
}
}
// Private
leaderDidUpdate () {
switch (this.resolveFollowState()) {
case FollowState.RETRACTED:
const editorProxy = this.getLocalActiveEditorProxy()
this.subscribeToEditorProxyChanges(editorProxy)
this.delegate.updateTether(FollowState.RETRACTED, editorProxy, this.resolveLeaderPosition())
break
case FollowState.EXTENDED:
this.retractOrDisconnectTether()
break
}
}
// Private
retractOrDisconnectTether () {
const leaderPosition = this.resolveLeaderPosition()
const leaderSiteId = this.resolveLeaderSiteId()
const localActiveEditorProxy = this.getLocalActiveEditorProxy()
const leaderActiveEditorProxy = this.activeEditorProxyForSiteId(leaderSiteId)
const leaderPositionIsInvisible = (
localActiveEditorProxy !== leaderActiveEditorProxy ||
(leaderPosition && localActiveEditorProxy.isScrollNeededToViewPosition(leaderPosition))
)
const hasRecentlyPerformedLocalUpdate = (Date.now() - this.lastLocalUpdateAt) <= this.tetherDisconnectWindow
if (leaderPositionIsInvisible) {
if (hasRecentlyPerformedLocalUpdate) {
this.unfollow()
} else {
this.retractTether()
}
}
}
// Private
setFollowState (newState, newLeaderId) {
const tether = this.tethersByFollowerId.get(this.siteId)
const oldState = tether ? tether.state : null
const oldResolvedState = this.resolveFollowState()
const oldResolvedLeaderId = this.resolveLeaderSiteId()
const oldLeaderId = tether ? tether.leaderId : null
newLeaderId = newLeaderId == null ? oldLeaderId : newLeaderId
if (newLeaderId == null) return
this.tethersByFollowerId.set(this.siteId, {leaderId: newLeaderId, state: newState})
const newResolvedState = this.resolveFollowState()
const newResolvedLeaderId = this.resolveLeaderSiteId()
this.didChangeTetherState({oldResolvedState, oldResolvedLeaderId, newResolvedState, newResolvedLeaderId})
if (oldState !== newState || oldLeaderId !== newLeaderId) {
const tetherMessage = new Messages.Tether()
tetherMessage.setFollowerSiteId(this.siteId)
tetherMessage.setLeaderSiteId(newLeaderId)
tetherMessage.setState(newState)
const updateMessage = new Messages.PortalUpdate()
updateMessage.setTetherUpdate(tetherMessage)
this.router.notify({channelId: `/portals/${this.id}`, body: updateMessage.serializeBinary()})
}
}
didChangeTetherState ({oldResolvedState, oldResolvedLeaderId, newResolvedState, newResolvedLeaderId}) {
const oldLeaderActiveEditorProxy = this.activeEditorProxiesBySiteId.get(oldResolvedLeaderId)
const newLeaderActiveEditorProxy = this.activeEditorProxiesBySiteId.get(newResolvedLeaderId)
if (newResolvedState === FollowState.RETRACTED) {
this.editorProxiesById.forEach((editorProxy) => {
editorProxy.hideSelections()
})
this.subscribeToEditorProxyChanges(newLeaderActiveEditorProxy)
} else if (oldResolvedState === FollowState.RETRACTED) {
this.activeEditorProxiesBySiteId.set(this.siteId, oldLeaderActiveEditorProxy)
this.broadcastEditorProxySwitch(oldLeaderActiveEditorProxy)
if (oldLeaderActiveEditorProxy) oldLeaderActiveEditorProxy.showSelections()
}
this.delegate.updateTether(newResolvedState, this.getLocalActiveEditorProxy(), this.resolveLeaderPosition())
this.updateActivePositions()
}
updateActivePositions () {
const activePositions = {}
const activeSiteIds = this.getActiveSiteIds()
for (let i = 0; i < activeSiteIds.length; i++) {
const siteId = activeSiteIds[i]
const editorProxy = this.activeEditorProxyForSiteId(siteId)
if (editorProxy) {
let position
const followState = this.resolveFollowState(siteId)
if (followState === FollowState.RETRACTED) {
const leaderId = this.resolveLeaderSiteId(siteId)
position = editorProxy.cursorPositionForSiteId(leaderId)
} else {
position = editorProxy.cursorPositionForSiteId(siteId)
}
activePositions[siteId] = {editorProxy, position, followState}
} else {
activePositions[siteId] = {editorProxy: null, position: null, followState: null}
}
}
this.delegate.updateActivePositions(activePositions)
}
broadcastEditorProxyCreation (editorProxy) {
const editorProxyCreationMessage = new Messages.PortalUpdate.EditorProxyCreation()
editorProxyCreationMessage.setEditorProxyMetadata(editorProxy.getMetadata().serialize())
const updateMessage = new Messages.PortalUpdate()
updateMessage.setEditorProxyCreation(editorProxyCreationMessage)
this.router.notify({channelId: `/portals/${this.id}`, body: updateMessage.serializeBinary()})
}
broadcastEditorProxySwitch (editorProxy) {
const editorProxySwitchMessage = new Messages.PortalUpdate.EditorProxySwitch()
if (editorProxy) editorProxySwitchMessage.setEditorProxyId(editorProxy.id)
const updateMessage = new Messages.PortalUpdate()
updateMessage.setEditorProxySwitch(editorProxySwitchMessage)
this.router.notify({channelId: `/portals/${this.id}`, body: updateMessage.serializeBinary()})
}
resolveLeaderPosition (followerId = this.siteId) {
const leaderId = this.resolveLeaderSiteId(followerId)
const editorProxy = this.getLocalActiveEditorProxy()
return editorProxy ? editorProxy.cursorPositionForSiteId(leaderId) : null
}
resolveFollowState (followerId = this.siteId) {
const leaderId = this.resolveLeaderSiteId(followerId)
if (followerId === leaderId) {
return FollowState.DISCONNECTED
} else {
return this.tethersByFollowerId.get(followerId).state
}
}
resolveLeaderSiteId (followerId = this.siteId) {
const tether = this.tethersByFollowerId.get(followerId)
if (!tether) return followerId
const visitedSiteIds = new Set([followerId])
let leaderId = tether.leaderId
let nextTether = this.tethersByFollowerId.get(leaderId)
while (nextTether && nextTether.state === FollowState.RETRACTED) {
if (visitedSiteIds.has(leaderId)) {
leaderId = Math.min(...Array.from(visitedSiteIds))
break
} else {
visitedSiteIds.add(leaderId)
leaderId = nextTether.leaderId
nextTether = this.tethersByFollowerId.get(leaderId)
}
}
return leaderId
}
}
function isHostSiteId (siteId) {
return siteId === 1
}
function pointsEqual (a, b) {
return a.row === b.row && a.column === b.column
}