654 lines
20 KiB
JavaScript
654 lines
20 KiB
JavaScript
'use strict'
|
|
|
|
const path = require('path')
|
|
const FramedStream = require('framed-stream')
|
|
const PearRuntime = require('pear-runtime')
|
|
const ReadyResource = require('ready-resource')
|
|
const { SquidManager } = require('./lib/squid-manager')
|
|
const { createWorld, listWorlds, resolveWorldPaths } = require('./lib/worlds')
|
|
const { worldsRoot } = require('./lib/paths')
|
|
const { ensureDir } = require('./lib/worlds')
|
|
const { WorldTunnelHost, WorldTunnelClient } = require('./lib/world-tunnel')
|
|
const { loadOrCreateTunnelKeys } = require('./lib/tunnel-keys')
|
|
const { encodeInvite, decodeInvite, decodeKey, encodeKey } = require('./lib/invite')
|
|
const { PeerSession } = require('./lib/peer-session')
|
|
const { MeshRegistry, listLocalMeshes } = require('./lib/mesh-registry')
|
|
const { borderAction } = require('./lib/border')
|
|
const { HandoffStore, applyHandoff } = require('./lib/player-handoff')
|
|
const crypto = require('hypercore-crypto')
|
|
|
|
/**
|
|
* Flying Jib Bare application shell.
|
|
* - Optional pear-runtime worker for OTA (hello-pear-bare pattern)
|
|
* - Squid + HyperDHT tunnels + Protomux peer session + mesh registry (ADR-0013)
|
|
*/
|
|
module.exports = class App extends ReadyResource {
|
|
constructor({ dir, appPath, updates, version, upgrade, name, displayName, mcUsername }) {
|
|
super()
|
|
this.dir = dir
|
|
this.appPath = appPath
|
|
this.updates = updates
|
|
this.version = version
|
|
this.upgrade = upgrade
|
|
this.name = name
|
|
this.displayName = displayName || 'player'
|
|
this.mcUsername = mcUsername || displayName || null
|
|
this.IPC = null
|
|
this.pipe = null
|
|
this.squid = null
|
|
this.tunnelHost = null
|
|
this.tunnelClient = null
|
|
this.peerSession = null
|
|
this.mesh = null
|
|
this.activeWorld = null
|
|
this._activeRegionId = null
|
|
this._migrating = false
|
|
this._shuttingDown = false
|
|
this.handoffs = new HandoffStore()
|
|
}
|
|
|
|
_open() {
|
|
ensureDir(worldsRoot(this.dir))
|
|
ensureDir(path.join(this.dir, 'corestore'))
|
|
|
|
const enableWorker =
|
|
this.updates !== false && this.upgrade && !String(this.upgrade).includes('<YOUR_KEY')
|
|
|
|
if (enableWorker) {
|
|
try {
|
|
this.IPC = PearRuntime.run(require.resolve('./workers/main.js'), [
|
|
String(this.updates),
|
|
this.version,
|
|
this.upgrade,
|
|
this.name,
|
|
this.dir,
|
|
this.appPath || ''
|
|
])
|
|
this.pipe = new FramedStream(this.IPC)
|
|
this.pipe.on('data', (data) => this._onWorkerMessage(data))
|
|
this.pipe.on('error', (err) => this.emit('error', err))
|
|
this.IPC.on('error', (err) => this.emit('error', err))
|
|
} catch (err) {
|
|
this.emit('message', `[worker] not started: ${err.message}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
async _close() {
|
|
await this._teardownSession()
|
|
const pipe = this.pipe
|
|
const IPC = this.IPC
|
|
this.pipe = null
|
|
this.IPC = null
|
|
pipe?.destroy()
|
|
IPC?.destroy()
|
|
}
|
|
|
|
async _teardownSession() {
|
|
if (this.peerSession) {
|
|
await this.peerSession.close().catch(() => {})
|
|
this.peerSession = null
|
|
}
|
|
if (this.tunnelClient) {
|
|
await this.tunnelClient.close().catch(() => {})
|
|
this.tunnelClient = null
|
|
}
|
|
if (this.tunnelHost) {
|
|
await this.tunnelHost.close().catch(() => {})
|
|
this.tunnelHost = null
|
|
}
|
|
if (this.squid) {
|
|
await this.squid.close().catch(() => {})
|
|
this.squid = null
|
|
}
|
|
// mesh stays open until exit unless stopMesh called
|
|
this.activeWorld = null
|
|
}
|
|
|
|
async stopMesh() {
|
|
if (this.mesh) {
|
|
await this.mesh.close().catch(() => {})
|
|
this.mesh = null
|
|
}
|
|
}
|
|
|
|
_onWorkerMessage(data) {
|
|
const message = data.toString()
|
|
if (message === 'updating') {
|
|
this.emit('updating')
|
|
return
|
|
}
|
|
if (message === 'updated') {
|
|
this.emit('updated')
|
|
this._sendWorker('pear:applyUpdate')
|
|
return
|
|
}
|
|
if (message === 'pear:updateApplied') {
|
|
this.emit('update-applied')
|
|
return
|
|
}
|
|
this.emit('message', message)
|
|
}
|
|
|
|
_sendWorker(message) {
|
|
if (this.pipe) this.pipe.write(message)
|
|
}
|
|
|
|
async _startPeerSession(worldPublicKey) {
|
|
if (this.peerSession) {
|
|
await this.peerSession.close().catch(() => {})
|
|
this.peerSession = null
|
|
}
|
|
this.peerSession = new PeerSession({
|
|
worldPublicKey,
|
|
displayName: this.displayName
|
|
})
|
|
this.peerSession.on('chat', (m) => this.emit('chat', m))
|
|
this.peerSession.on('presence', (m) => this.emit('presence', m))
|
|
this.peerSession.on('peers', (list) => this.emit('peers', list))
|
|
this.peerSession.on('peer-join', (m) => this.emit('peer-join', m))
|
|
this.peerSession.on('peer-leave', (m) => this.emit('peer-leave', m))
|
|
this.peerSession.on('error', (err) => this.emit('error', err))
|
|
this.peerSession.on('control', (m) => this._onPeerControl(m))
|
|
this.peerSession.on('migrate', (m) => {
|
|
// also handled in _wireGuestMigrate for tunnel switch
|
|
})
|
|
await this.peerSession.ready()
|
|
this.emit('message', `Peer session ready (id ${this.peerSession.peerId})`)
|
|
}
|
|
|
|
_onPeerControl(m) {
|
|
if (!m || m.local) return
|
|
if (m.type === 'handoff-apply' && m.username && m.handoff) {
|
|
this.handoffs.set(m.username, m.handoff)
|
|
this.emit(
|
|
'message',
|
|
`[mesh] pending handoff for ${m.username} (${(m.handoff.inventory || []).length} items)`
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When a player spawns on this Squid, apply any pending mesh handoff.
|
|
*/
|
|
_wireHandoffOnSpawn() {
|
|
if (!this.squid || !this.squid.serv) return
|
|
const serv = this.squid.serv
|
|
if (serv._fjHandoffWired) return
|
|
serv._fjHandoffWired = true
|
|
serv.on('newPlayer', (player) => {
|
|
player.on('spawned', () => {
|
|
const handoff = this.handoffs.take(player.username)
|
|
if (!handoff) return
|
|
applyHandoff(player, serv, handoff)
|
|
.then((r) => {
|
|
this.emit(
|
|
'message',
|
|
`[mesh] handoff applied for ${player.username}: slots=${r.slots || 0}`
|
|
)
|
|
this.emit('handoff-applied', { username: player.username, result: r })
|
|
})
|
|
.catch((err) => this.emit('error', err))
|
|
})
|
|
})
|
|
}
|
|
|
|
listWorlds() {
|
|
return listWorlds(this.dir)
|
|
}
|
|
|
|
createWorld(opts) {
|
|
return createWorld(this.dir, opts)
|
|
}
|
|
|
|
/**
|
|
* Start Squid for a world (loopback only).
|
|
* @param {{ world: string, port?: number, version?: string }} opts
|
|
*/
|
|
async startWorld(opts) {
|
|
if (this.squid) {
|
|
throw new Error('A world is already running; stop it first')
|
|
}
|
|
const { meta, anvil } = resolveWorldPaths(this.dir, opts.world)
|
|
const port = opts.port || 25565
|
|
const version = opts.version || meta.version
|
|
|
|
this.squid = new SquidManager({
|
|
worldFolder: anvil,
|
|
port,
|
|
version,
|
|
motd: meta.motd || `Flying Jib — ${meta.name}`,
|
|
logging: false
|
|
})
|
|
|
|
this.squid.on('error', (err) => this.emit('error', err))
|
|
this.squid.on('listening', (p) => {
|
|
this.emit('message', `Squid listening on 127.0.0.1:${p}`)
|
|
})
|
|
|
|
await this.squid.ready()
|
|
this._wireHandoffOnSpawn()
|
|
this.activeWorld = meta
|
|
return this.squid.status
|
|
}
|
|
|
|
/**
|
|
* Start Squid + HyperDHT host tunnel + peer chat/presence.
|
|
* @param {{ world: string, port?: number, version?: string, displayName?: string, meshInvite?: string, meshKey?: string, enroll?: object }} opts
|
|
*/
|
|
async hostWorld(opts) {
|
|
if (opts.displayName) this.displayName = opts.displayName
|
|
if (opts.mcUsername) this.mcUsername = opts.mcUsername
|
|
const squidStatus = await this.startWorld(opts)
|
|
const keys = loadOrCreateTunnelKeys(this.dir, this.activeWorld.id)
|
|
|
|
this.tunnelHost = new WorldTunnelHost({
|
|
localPort: squidStatus.port,
|
|
seed: keys.seed,
|
|
cap: keys.cap
|
|
})
|
|
this.tunnelHost.on('connection', () => {
|
|
this.emit('message', 'Remote peer connected via MC tunnel')
|
|
})
|
|
this.tunnelHost.on('error', (err) => this.emit('error', err))
|
|
await this.tunnelHost.ready()
|
|
|
|
this.squid.on('migrate', (evt) => this._onHostMigrate(evt))
|
|
this.squid.on('border', (evt) => this.emit('border', evt))
|
|
|
|
await this._startPeerSession(keys.publicKey)
|
|
|
|
// Optional: open mesh + enroll so border plugin activates
|
|
let region = null
|
|
if (opts.meshInvite || opts.meshKey || opts.createMesh) {
|
|
if (opts.createMesh) {
|
|
await this.createMesh({ name: opts.meshName || 'mesh' })
|
|
} else {
|
|
await this.openMesh({ invite: opts.meshInvite, key: opts.meshKey })
|
|
}
|
|
if (this.mesh && this.mesh.writable) {
|
|
region = await this.enrollWorld({
|
|
world: this.activeWorld.id,
|
|
...(opts.enroll || {})
|
|
})
|
|
}
|
|
}
|
|
|
|
const invite = encodeInvite({
|
|
type: 'private-world',
|
|
worldKey: keys.publicKeyZ32,
|
|
cap: keys.capZ32,
|
|
name: this.activeWorld.name,
|
|
mcVersion: this.activeWorld.version,
|
|
portHint: squidStatus.port
|
|
})
|
|
|
|
return {
|
|
squid: squidStatus,
|
|
tunnel: this.tunnelHost.status,
|
|
peers: this.peerSession ? this.peerSession.peerList : [],
|
|
invite,
|
|
region,
|
|
mesh: this.mesh
|
|
? { key: this.mesh.keyZ32, invite: this.mesh.keyZ32 && encodeInvite({ type: 'mesh', meshKey: this.mesh.keyZ32, name: this.mesh.name }) }
|
|
: null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Join a private world via fj1. invite (tunnel + peer session).
|
|
* @param {{ invite: string, localPort?: number, displayName?: string }} opts
|
|
*/
|
|
async joinWorld(opts) {
|
|
if (this.tunnelClient) {
|
|
throw new Error('Already joined a remote world; stop first')
|
|
}
|
|
if (opts.displayName) this.displayName = opts.displayName
|
|
if (opts.mcUsername) this.mcUsername = opts.mcUsername
|
|
const inv = decodeInvite(opts.invite)
|
|
if (inv.type !== 'private-world') {
|
|
throw new Error('join expects a private-world fj1. invite')
|
|
}
|
|
const publicKey = decodeKey(inv.worldKey)
|
|
const cap = decodeKey(inv.cap)
|
|
|
|
this.tunnelClient = new WorldTunnelClient({
|
|
publicKey,
|
|
cap,
|
|
localPort: opts.localPort != null ? Number(opts.localPort) : 0
|
|
})
|
|
this.tunnelClient.on('error', (err) => this.emit('error', err))
|
|
await this.tunnelClient.ready()
|
|
|
|
await this._startPeerSession(publicKey)
|
|
this._wireGuestMigrate()
|
|
|
|
return {
|
|
invite: inv,
|
|
tunnel: this.tunnelClient.status,
|
|
peers: this.peerSession ? this.peerSession.peerList : []
|
|
}
|
|
}
|
|
|
|
sendChat(text) {
|
|
if (!this.peerSession) throw new Error('No peer session (host or join first)')
|
|
this.peerSession.sendChat(text)
|
|
}
|
|
|
|
async stopWorld() {
|
|
await this._teardownSession()
|
|
return true
|
|
}
|
|
|
|
listMeshes() {
|
|
return listLocalMeshes(this.dir)
|
|
}
|
|
|
|
/**
|
|
* Create a new mesh (local becomes first writer).
|
|
* @param {{ name?: string }} opts
|
|
*/
|
|
async createMesh(opts = {}) {
|
|
if (this.mesh) await this.stopMesh()
|
|
this.mesh = new MeshRegistry({
|
|
storageDir: this.dir,
|
|
bootstrap: null,
|
|
name: opts.name || 'Flying Jib Mesh'
|
|
})
|
|
await this.mesh.ready()
|
|
const invite = encodeInvite({
|
|
type: 'mesh',
|
|
meshKey: this.mesh.keyZ32,
|
|
name: opts.name || 'Flying Jib Mesh'
|
|
})
|
|
return {
|
|
key: this.mesh.keyZ32,
|
|
meshId: this.mesh.meshId,
|
|
writable: this.mesh.writable,
|
|
localWriterKey: this.mesh.localWriterKey
|
|
? encodeKey(this.mesh.localWriterKey)
|
|
: null,
|
|
invite
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open/join mesh by key or fj1. mesh invite.
|
|
* @param {{ key?: string, invite?: string, name?: string }} opts
|
|
*/
|
|
async openMesh(opts) {
|
|
if (this.mesh) await this.stopMesh()
|
|
let key = opts.key
|
|
let name = opts.name
|
|
if (opts.invite) {
|
|
const inv = decodeInvite(opts.invite)
|
|
if (inv.type !== 'mesh') throw new Error('Invite is not a mesh invite')
|
|
key = inv.meshKey
|
|
name = inv.name || name
|
|
}
|
|
if (!key) throw new Error('openMesh requires key or mesh invite')
|
|
this.mesh = new MeshRegistry({
|
|
storageDir: this.dir,
|
|
bootstrap: key,
|
|
name: name || 'mesh'
|
|
})
|
|
await this.mesh.ready()
|
|
return {
|
|
key: this.mesh.keyZ32,
|
|
meshId: this.mesh.meshId,
|
|
writable: this.mesh.writable,
|
|
localWriterKey: this.mesh.localWriterKey
|
|
? encodeKey(this.mesh.localWriterKey)
|
|
: null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Admit a peer's writer key (must be mesh writer).
|
|
* @param {string} writerKeyZ32
|
|
*/
|
|
async meshAddWriter(writerKeyZ32) {
|
|
if (!this.mesh) throw new Error('No mesh open')
|
|
await this.mesh.addWriter(writerKeyZ32)
|
|
return { ok: true }
|
|
}
|
|
|
|
/**
|
|
* Enroll active (or named) world into open mesh.
|
|
* @param {{ world?: string, minX?: number, maxX?: number, minZ?: number, maxZ?: number, offsetX?: number, offsetZ?: number }} opts
|
|
*/
|
|
async enrollWorld(opts = {}) {
|
|
if (!this.mesh) throw new Error('No mesh open — create-mesh or open-mesh first')
|
|
const worldName = opts.world || (this.activeWorld && this.activeWorld.id)
|
|
if (!worldName) throw new Error('enroll requires world name or active hosted world')
|
|
const { meta } = resolveWorldPaths(this.dir, worldName)
|
|
const keys = loadOrCreateTunnelKeys(this.dir, meta.id)
|
|
const minX = opts.minX != null ? Number(opts.minX) : 0
|
|
const maxX = opts.maxX != null ? Number(opts.maxX) : 9999
|
|
const minZ = opts.minZ != null ? Number(opts.minZ) : 0
|
|
const maxZ = opts.maxZ != null ? Number(opts.maxZ) : 9999
|
|
const region = await this.mesh.enroll({
|
|
regionId: meta.id + '-' + crypto.randomBytes(4).toString('hex'),
|
|
worldKey: keys.publicKeyZ32,
|
|
// Mesh members share tunnel caps (mesh ACL); private fj1. invites still use same keys
|
|
cap: keys.capZ32,
|
|
ownerKey: this.mesh.localWriterKey ? encodeKey(this.mesh.localWriterKey) : null,
|
|
bounds: { minX, maxX, minZ, maxZ },
|
|
offset: {
|
|
x: opts.offsetX != null ? Number(opts.offsetX) : minX,
|
|
z: opts.offsetZ != null ? Number(opts.offsetZ) : minZ
|
|
},
|
|
mcVersion: meta.version,
|
|
name: meta.name,
|
|
portalPolicy: 'teleport'
|
|
})
|
|
this._activeRegionId = region.regionId
|
|
// If currently hosting this world, enable border plugin
|
|
if (this.squid && this.activeWorld && this.activeWorld.id === meta.id) {
|
|
await this._enableMeshBorder(region)
|
|
}
|
|
return region
|
|
}
|
|
|
|
/**
|
|
* Enable fj-mesh-border on the running Squid for an enrolled region.
|
|
* @param {object} region
|
|
*/
|
|
async _enableMeshBorder(region) {
|
|
if (!this.squid || !this.mesh) return
|
|
const self = this
|
|
const cfg = {
|
|
enabled: true,
|
|
regionId: region.regionId,
|
|
bounds: region.bounds,
|
|
offset: region.offset,
|
|
margin: 16,
|
|
intervalMs: 400,
|
|
kickOnMigrate: true,
|
|
getRegions: async () => self.mesh.listRegions(),
|
|
onBorder: (evt) => self.emit('border', evt),
|
|
onMigrate: (evt) => self._onHostMigrate(evt)
|
|
}
|
|
this.squid.setMeshBorder(cfg)
|
|
this.emit('message', `Mesh border active for region ${region.regionId}`)
|
|
}
|
|
|
|
/**
|
|
* Host-side: player crossed out of region — notify peers and log.
|
|
* @param {object} evt from fj-mesh-border
|
|
*/
|
|
_onHostMigrate(evt) {
|
|
this.emit('migrate', evt)
|
|
this.emit(
|
|
'message',
|
|
`[mesh] migrate ${evt.player.username} → ${evt.neighbor.name || evt.neighbor.regionId}` +
|
|
(evt.handoff ? ` (handoff ${evt.handoff.inventory?.length || 0} items)` : '')
|
|
)
|
|
if (this.peerSession) {
|
|
this.peerSession.sendControl({
|
|
type: 'migrate',
|
|
username: evt.player.username,
|
|
fromRegionId: evt.fromRegionId,
|
|
neighbor: evt.neighbor,
|
|
mapped: evt.mapped,
|
|
player: evt.player,
|
|
handoff: evt.handoff || null
|
|
})
|
|
}
|
|
// Local Java client (same machine as host) also needs to switch tunnel
|
|
if (
|
|
this.mcUsername &&
|
|
evt.player.username &&
|
|
evt.player.username.toLowerCase() === this.mcUsername.toLowerCase()
|
|
) {
|
|
this._switchToNeighborRegion(evt).catch((err) => this.emit('error', err))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Guest/local: stop current world tunnel and open tunnel to neighbor region.
|
|
* Squid host process is left running if we are the region host for others.
|
|
* @param {object} evt
|
|
*/
|
|
async _switchToNeighborRegion(evt) {
|
|
if (this._migrating) return
|
|
this._migrating = true
|
|
try {
|
|
const neighbor = evt.neighbor
|
|
if (!neighbor || !neighbor.worldKey) {
|
|
throw new Error('migrate missing neighbor.worldKey')
|
|
}
|
|
const publicKey = decodeKey(neighbor.worldKey)
|
|
const cap = neighbor.cap ? decodeKey(neighbor.cap) : null
|
|
const preferPort =
|
|
(this.tunnelClient && this.tunnelClient.status.localPort) ||
|
|
(this.squid && this.squid.status.port) ||
|
|
25565
|
|
|
|
// Tear down guest tunnel / peer session for old world (keep Squid if hosting)
|
|
if (this.peerSession) {
|
|
await this.peerSession.close().catch(() => {})
|
|
this.peerSession = null
|
|
}
|
|
if (this.tunnelClient) {
|
|
await this.tunnelClient.close().catch(() => {})
|
|
this.tunnelClient = null
|
|
}
|
|
|
|
// If we were hosting the old region, keep Squid+host tunnel for others;
|
|
// local player uses a client tunnel on preferPort+1 when port still taken by Squid
|
|
let localPort = preferPort
|
|
if (this.squid && this.squid.status.running) {
|
|
localPort = preferPort === 25565 ? 25566 : preferPort + 1
|
|
}
|
|
|
|
this.tunnelClient = new WorldTunnelClient({
|
|
publicKey,
|
|
cap,
|
|
localPort
|
|
})
|
|
this.tunnelClient.on('error', (err) => this.emit('error', err))
|
|
await this.tunnelClient.ready()
|
|
|
|
await this._startPeerSession(publicKey)
|
|
this._wireGuestMigrate()
|
|
|
|
// Deliver inventory handoff to neighbor host via control plane
|
|
if (evt.handoff && this.peerSession) {
|
|
this.peerSession.sendControl({
|
|
type: 'handoff-apply',
|
|
username: evt.player?.username || evt.username || evt.handoff.username,
|
|
handoff: evt.handoff
|
|
})
|
|
}
|
|
|
|
this._activeRegionId = neighbor.regionId
|
|
const port = this.tunnelClient.status.localPort
|
|
this.emit(
|
|
'message',
|
|
`[mesh] switched tunnel → ${neighbor.name || neighbor.regionId} @ 127.0.0.1:${port}`
|
|
)
|
|
this.emit(
|
|
'message',
|
|
`Connect / reconnect Java Edition to 127.0.0.1:${port} (spawn ~ ${JSON.stringify(evt.mapped)})`
|
|
)
|
|
this.emit('migrated', {
|
|
neighbor,
|
|
localPort: port,
|
|
mapped: evt.mapped,
|
|
handoff: evt.handoff || null
|
|
})
|
|
} finally {
|
|
this._migrating = false
|
|
}
|
|
}
|
|
|
|
_wireGuestMigrate() {
|
|
if (!this.peerSession) return
|
|
this.peerSession.removeAllListeners('migrate')
|
|
this.peerSession.on('migrate', (m) => {
|
|
if (m.local) return
|
|
if (
|
|
this.mcUsername &&
|
|
m.username &&
|
|
m.username.toLowerCase() === this.mcUsername.toLowerCase()
|
|
) {
|
|
this._switchToNeighborRegion(m).catch((err) => this.emit('error', err))
|
|
} else if (!this.mcUsername) {
|
|
// No filter configured — apply migrate (single-player guest)
|
|
this._switchToNeighborRegion(m).catch((err) => this.emit('error', err))
|
|
}
|
|
})
|
|
}
|
|
|
|
async listMeshRegions() {
|
|
if (!this.mesh) throw new Error('No mesh open')
|
|
return this.mesh.listRegions()
|
|
}
|
|
|
|
/**
|
|
* Border advisory for a position in the current mesh.
|
|
* @param {{ regionId: string, x: number, y?: number, z: number, margin?: number }} opts
|
|
*/
|
|
async checkBorder(opts) {
|
|
if (!this.mesh) throw new Error('No mesh open')
|
|
const regions = await this.mesh.listRegions()
|
|
const current = regions.find((r) => r.regionId === opts.regionId)
|
|
if (!current) throw new Error('region not found: ' + opts.regionId)
|
|
return borderAction(regions, current, opts.x, opts.z, opts.margin)
|
|
}
|
|
|
|
getStatus() {
|
|
return {
|
|
storage: this.dir,
|
|
world: this.activeWorld,
|
|
displayName: this.displayName,
|
|
squid: this.squid ? this.squid.status : { running: false },
|
|
tunnelHost: this.tunnelHost ? this.tunnelHost.status : null,
|
|
tunnelClient: this.tunnelClient ? this.tunnelClient.status : null,
|
|
peers: this.peerSession ? this.peerSession.peerList : [],
|
|
peerId: this.peerSession ? this.peerSession.peerId : null,
|
|
mesh: this.mesh
|
|
? {
|
|
key: this.mesh.keyZ32,
|
|
meshId: this.mesh.meshId,
|
|
writable: this.mesh.writable
|
|
}
|
|
: null
|
|
}
|
|
}
|
|
|
|
async exit(code = 0) {
|
|
if (this._shuttingDown) return
|
|
this._shuttingDown = true
|
|
if (typeof Bare !== 'undefined') Bare.exitCode = code
|
|
try {
|
|
await this.stopWorld()
|
|
await this.stopMesh()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
await this.close()
|
|
}
|
|
}
|