Release rolling / release (push) Successful in 11m16s
Bump published pins (compact-encoding 3, bare-fetch/tls/https/ws 3, bare-subprocess 6, bare-signals 5, corestore 7.12, protomux 3.11, hypercore-crypto 3.7, bare-runtime 1.31) and regenerate catalogs, manifests, and kernel/seeder bundles. Adapt call sites to the new APIs: - Corestore: explicit session flush before suspend(); treeCache ctor opts - bare-crypto: KeyObject.export() instead of removed ._key - Protomux 3.11: wait for fullyOpened()/fullyClosed() on chat channels - bare-fetch: surface response.type and Headers.getSetCookie - host snapshots: bare-os 3.9 / bare-posix / bare-fs.statfs frsize - bare-subprocess 6: optional IPC channel + json serialization Keep catalog sync from wiping curated pearEntries. Teach the Node test shim to stub bare-thread/bare-worker (ESM absolute paths) and chain Bare.on so bare-timers can load. Booter 479, protocol 34, seeder 14.
920 lines
29 KiB
JavaScript
920 lines
29 KiB
JavaScript
import b4a from 'b4a'
|
||
import c from 'compact-encoding'
|
||
import {
|
||
PROTOCOL_NAME,
|
||
PROTOCOL_APP_CHANNEL_NAME,
|
||
PROTOCOL_CAP_CHANNEL_NAME,
|
||
PROTOCOL_CHAT_CHANNEL_NAME,
|
||
PROTOCOL_MESHDROP_CHANNEL_NAME
|
||
} from 'bare-os-protocol/constants.js'
|
||
import { bareOsChatMuxEnabled } from './bare-os-chat-service.js'
|
||
import { bareOsMeshdropMuxEnabled } from './bare-os-meshdrop-service.js'
|
||
import {
|
||
bareOsHostBooterInfo,
|
||
bareOsHostBooterWarn
|
||
} from './bare-os-host-booter-log.js'
|
||
import { getKernelCapabilityWords } from 'bare-os-protocol'
|
||
|
||
/**
|
||
* Host-side booter log for swarm disk (stderr JSON when BARE_OS_BOOT_TRACE=json|ndjson; else stderr or console.warn).
|
||
* @param {'info'|'warn'} level
|
||
* @param {string} message
|
||
* @param {Record<string, unknown> | null} [detail]
|
||
*/
|
||
function emitSwarmDiskHostLog(level, message, detail = null) {
|
||
const env = globalThis.process?.env
|
||
const trace =
|
||
env &&
|
||
(env.BARE_OS_BOOT_TRACE === 'json' || env.BARE_OS_BOOT_TRACE === 'ndjson')
|
||
const err = globalThis.process?.stderr
|
||
if (trace && err && typeof err.write === 'function') {
|
||
err.write(
|
||
`${JSON.stringify({
|
||
type: 'booterHost',
|
||
bootTraceSchemaVersion: 2,
|
||
component: 'swarm_disk',
|
||
level,
|
||
message,
|
||
detail,
|
||
ts: Date.now()
|
||
})}\n`
|
||
)
|
||
}
|
||
if (level === 'warn') {
|
||
bareOsHostBooterWarn(
|
||
'swarm_disk',
|
||
message,
|
||
detail ? JSON.stringify(detail) : ''
|
||
)
|
||
return
|
||
}
|
||
bareOsHostBooterInfo(
|
||
'swarm_disk',
|
||
message,
|
||
detail ? JSON.stringify(detail) : ''
|
||
)
|
||
}
|
||
|
||
/** Default max wait for block 0 (MBR) before failing boot (override **`BARE_OS_MBR_READ_TIMEOUT_MS`**). */
|
||
const MBR_READ_TIMEOUT_MS_DEFAULT = 60_000
|
||
|
||
/** Cap-channel (`bare-os-cap-v1`) message size bound when **`BARE_OS_PROTOMUX_CAP_CHANNEL`** is enabled. */
|
||
export const BARE_OS_PROTOMUX_CAP_CHANNEL_MESSAGE_MAX_BYTES = 65536
|
||
|
||
/**
|
||
* Max wait (ms) for block 0 / MBR replication before boot failure.
|
||
* Exported for microbench / unit introspection (same rules as booter).
|
||
* @param {SwarmDisk} disk
|
||
*/
|
||
export function mbrReadTimeoutMsForDisk(disk) {
|
||
const env = globalThis.process?.env
|
||
const raw = String(env?.BARE_OS_MBR_READ_TIMEOUT_MS ?? '').trim()
|
||
if (raw) {
|
||
const n = Math.floor(Number(raw))
|
||
if (Number.isFinite(n) && n >= 3000) return Math.min(n, 600_000)
|
||
}
|
||
const adapt =
|
||
env?.BARE_OS_MBR_READ_TIMEOUT_ADAPTIVE === '1' ||
|
||
env?.BARE_OS_MBR_READ_TIMEOUT_ADAPTIVE === 'true'
|
||
if (adapt && disk && disk.peers && typeof disk.peers.size === 'number') {
|
||
const n = disk.peers.size
|
||
if (n <= 1) return Math.min(120_000, MBR_READ_TIMEOUT_MS_DEFAULT + 30_000)
|
||
if (n >= 4) return Math.max(45_000, MBR_READ_TIMEOUT_MS_DEFAULT - 15_000)
|
||
}
|
||
return MBR_READ_TIMEOUT_MS_DEFAULT
|
||
}
|
||
|
||
/**
|
||
* Interval (ms) to re-send pending block read requests to all peers while waiting for MBR / block data.
|
||
* **`0`** disables periodic rebroadcast (initial + per-connect sends only).
|
||
* Override **`BARE_OS_MBR_READ_REBROADCAST_MS`** (defaults **5000**, clamp **2000**–**60000**).
|
||
* @param {Record<string, string | undefined> | null | undefined} env
|
||
*/
|
||
export function mbrReadRebroadcastMsFromEnv(env) {
|
||
const raw = String(env?.BARE_OS_MBR_READ_REBROADCAST_MS ?? '').trim()
|
||
if (raw === '0' || raw.toLowerCase() === 'false') return 0
|
||
const n = Math.floor(Number(raw))
|
||
if (Number.isFinite(n) && n >= 0) {
|
||
if (n === 0) return 0
|
||
return Math.min(60_000, Math.max(2_000, n))
|
||
}
|
||
return 5_000
|
||
}
|
||
|
||
export class SwarmDisk {
|
||
constructor() {
|
||
this.localRAM = new Map()
|
||
this.peers = new Set()
|
||
this.pendingReads = new Map()
|
||
this.pendingSearches = new Map()
|
||
this.pendingRpc = new Map()
|
||
this.searchIdCounter = 0
|
||
this.rpcIdCounter = 0
|
||
this.drive = null
|
||
this.personalDrive = null
|
||
this.os = null
|
||
/** @type {import('hyperdrive').default[]} */
|
||
this.auxiliaryDrives = []
|
||
/** @type {Record<string, unknown> | null} Last `bare_os.capabilities` RPC result (or error object). */
|
||
this.seedCapabilityInfo = null
|
||
/** @type {Record<string, unknown> | null} Last `bare_os.replication_status` RPC result when available. */
|
||
this.seedReplicationStatus = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedManifestHints = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedPeerHealth = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedStagingSlot = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedReplicationQueue = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedCapabilityAttestation = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedMbrLayout = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedSnapshotHints = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedPeerFirewallStats = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedReplicationPlan = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedDhtBootstrapHint = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedSnapshotChain = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedMirrorCompactionHint = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedUpdaterState = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedBlindPeerTopologyV2 = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedCompactPing = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedCorestoreStats = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedSnapshotManifestSlice = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedMirrorDriveHintV2 = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedHrpcRegistrySummary = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedProtomuxCapabilityAd = null
|
||
/** Cumulative count of buffers received on optional Protomux app-channel pairs (operator observability). */
|
||
this.protomuxAppChannelRxTotal = 0
|
||
/** Cumulative count of buffers received on optional Protomux cap-channel pairs (`bare-os-cap-v1`). */
|
||
this.protomuxCapChannelRxTotal = 0
|
||
/** Cumulative chat `event` messages received on `bare-os-chat-v1` (when enabled). */
|
||
this.protomuxChatChannelRxTotal = 0
|
||
/** Cumulative meshdrop envelopes received on `bare-os-meshdrop-v1` (when enabled). */
|
||
this.protomuxMeshdropChannelRxTotal = 0
|
||
/** @type {ReturnType<import('./bare-os-chat-service.js').createBareOsChatService> | null} */
|
||
this.bareOsChatService = null
|
||
/** @type {ReturnType<import('./bare-os-meshdrop-service.js').createBareOsMeshdropService> | null} */
|
||
this.bareOsMeshdropService = null
|
||
/** @type {Record<string, unknown> | null} Host-requested Hyperswarm caps (from env); surfaced on disk.os RPC. */
|
||
this.swarmConnectionBudget = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedDhtAddressBook = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedReplicationThrottleHint = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedBundlebeeStage = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedHttpDhtProxyHint = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedProtomuxRpcPoolHint = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedHyperblobStoreHint = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedSigningRequestQueueHint = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedCoreStorageLayoutHint = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedMirrorDriveCompactionV3 = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedBundlebeeCliStage = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedReadyGuardV2 = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedBlindRelayCircuitHint = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
this.seedHttpDhtProxyRoutes = null
|
||
/** @type {string[]} MBR-derived drive key hex list (primary + failovers). */
|
||
this.mbrKeysHex = []
|
||
/**
|
||
* 512-byte MBR block used at boot (copy); enables peer block-0 service when mirrored to {@link SwarmDisk#localRAM}.
|
||
* @type {Uint8Array | null}
|
||
*/
|
||
this.bootMbr512 = null
|
||
/** True after peer system seed eligibility passed and MBR was published to `localRAM`. */
|
||
this.peerSystemSeedActive = false
|
||
/**
|
||
* Local Noise wire static key (32 bytes), last seen from `Hyperswarm` socket `publicKey`.
|
||
* Peers validate chat `senderPk` against `socket.remotePublicKey`, which matches **this** — not `swarm.keyPair.publicKey`.
|
||
*/
|
||
this.localNoiseWirePk = /** @type {Uint8Array | null} */ (null)
|
||
}
|
||
|
||
/**
|
||
* One JSON-RPC-style round-trip to a specific peer.
|
||
* @param {unknown} peer
|
||
* @param {string} module
|
||
* @param {string} method
|
||
* @param {string[]} args
|
||
* @param {number} timeoutMs
|
||
* @returns {Promise<unknown>}
|
||
*/
|
||
_rpcOne(peer, module, method, args, timeoutMs) {
|
||
const id = this.rpcIdCounter++
|
||
return new Promise((resolve, reject) => {
|
||
const to = setTimeout(() => {
|
||
this.pendingRpc.delete(id)
|
||
reject(new Error('swarm-disk rpc: timeout'))
|
||
}, timeoutMs)
|
||
this.pendingRpc.set(id, (m) => {
|
||
clearTimeout(to)
|
||
if (m.success) {
|
||
const r = m.result || ''
|
||
try {
|
||
resolve(r ? JSON.parse(r) : null)
|
||
} catch {
|
||
resolve(r)
|
||
}
|
||
} else {
|
||
reject(new Error(m.error || 'swarm-disk rpc failed'))
|
||
}
|
||
})
|
||
const chan =
|
||
/** @type {{ chan: { messages: { send: (m: unknown) => void }[] } }} */ (
|
||
peer
|
||
).chan
|
||
void chan.fullyOpened().then((opened) => {
|
||
if (!opened) return
|
||
if (!this.pendingRpc.has(id)) return
|
||
try {
|
||
chan.messages[5].send({ id, module, method, args })
|
||
} catch (err) {
|
||
this.pendingRpc.delete(id)
|
||
clearTimeout(to)
|
||
reject(
|
||
err instanceof Error
|
||
? err
|
||
: new Error((err && err.message) || String(err))
|
||
)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
/**
|
||
* JSON-RPC-style call to swarm peers. For **`bare_os`**, tries each peer in turn when
|
||
* **`BARE_OS_SWARM_RPC_TRY_PEERS`** is not **`0`/`false`** (default: try all).
|
||
* **`capabilities`** responses must include **`kernelCapabilityWords`** (wire v2).
|
||
* @param {string} module
|
||
* @param {string} method
|
||
* @param {string[]} [args]
|
||
* @param {number} [timeoutMs]
|
||
* @returns {Promise<unknown>}
|
||
*/
|
||
async rpc(module, method, args = [], timeoutMs = 8000) {
|
||
if (!this.peers.size) throw new Error('swarm-disk rpc: no peers')
|
||
const peers = [...this.peers]
|
||
const mod = String(module || '')
|
||
const meth = String(method || '')
|
||
const env = globalThis.process?.env
|
||
const tryAll =
|
||
String(env?.BARE_OS_SWARM_RPC_TRY_PEERS ?? '1').toLowerCase() !== '0' &&
|
||
String(env?.BARE_OS_SWARM_RPC_TRY_PEERS ?? '1').toLowerCase() !==
|
||
'false' &&
|
||
mod === 'bare_os' &&
|
||
peers.length > 1
|
||
const perPeerMs = tryAll
|
||
? Math.min(
|
||
2500,
|
||
Math.max(800, Math.floor(timeoutMs / Math.min(peers.length, 4)))
|
||
)
|
||
: timeoutMs
|
||
if (tryAll) {
|
||
/** @type {Error | null} */
|
||
let lastErr = null
|
||
for (const peer of peers) {
|
||
try {
|
||
const r = await this._rpcOne(peer, mod, meth, args, perPeerMs)
|
||
if (meth === 'capabilities') {
|
||
const words = getKernelCapabilityWords(r)
|
||
if (!words) {
|
||
lastErr = new Error(
|
||
'swarm-disk rpc: capabilities missing kernelCapabilityWords'
|
||
)
|
||
continue
|
||
}
|
||
}
|
||
return r
|
||
} catch (e) {
|
||
lastErr =
|
||
e instanceof Error ? e : new Error((e && e.message) || String(e))
|
||
}
|
||
}
|
||
throw lastErr || new Error('swarm-disk rpc: all peers failed')
|
||
}
|
||
return this._rpcOne(peers[0], mod, meth, args, timeoutMs)
|
||
}
|
||
|
||
/**
|
||
* @param {import('corestore').default} store
|
||
* @param {import('hyperswarm').default} swarm
|
||
* @param {import('hyperdrive').default} Hyperdrive
|
||
*/
|
||
async initPersonalDrive(store, swarm, Hyperdrive) {
|
||
const localStore = store.namespace('bare-os-personal-v1', {
|
||
writable: true
|
||
})
|
||
this.personalDrive = new Hyperdrive(localStore)
|
||
await this.personalDrive.ready()
|
||
if (!this.personalDrive.writable) {
|
||
try {
|
||
await this.personalDrive.close()
|
||
} catch (_) {}
|
||
this.personalDrive = new Hyperdrive(localStore)
|
||
await this.personalDrive.ready()
|
||
}
|
||
if (!this.personalDrive.writable) {
|
||
emitSwarmDiskHostLog(
|
||
'warn',
|
||
'[bare-os-booter] Personal Hyperdrive is not writable — identity and $HOME writes will fail. Check Corestore path permissions and that no other process holds the store read-only.',
|
||
{
|
||
writable: false,
|
||
idPrefix: b4a.toString(this.personalDrive.id, 'hex').slice(0, 16)
|
||
}
|
||
)
|
||
}
|
||
emitSwarmDiskHostLog(
|
||
'info',
|
||
'Personal Hyperdrive mounted; joining swarm discovery',
|
||
{
|
||
idPrefix: b4a.toString(this.personalDrive.id, 'hex').slice(0, 16),
|
||
writable: this.personalDrive.writable
|
||
}
|
||
)
|
||
swarm.join(this.personalDrive.discoveryKey)
|
||
}
|
||
|
||
addPeer(mux, socket) {
|
||
const disk = this
|
||
/** @type {any} */
|
||
let chan
|
||
|
||
const context = {
|
||
onread(index) {
|
||
const data = disk.localRAM.get(index)
|
||
if (data) chan.messages[1].send({ index, data })
|
||
},
|
||
ondata(m) {
|
||
const cb = disk.pendingReads.get(m.index)
|
||
if (cb) {
|
||
disk.pendingReads.delete(m.index)
|
||
cb(m.data)
|
||
}
|
||
},
|
||
ongossip() {},
|
||
async onsearchreq(m) {
|
||
const matches = disk.os ? await disk.os.searchLocal?.(m.query) : []
|
||
chan.messages[4].send({ id: m.id, matches: matches || [] })
|
||
},
|
||
onsearchres(m) {
|
||
const st = disk.pendingSearches.get(m.id)
|
||
if (!st || st.settled) return
|
||
st.results.push(m.matches || [])
|
||
st.remaining--
|
||
if (st.remaining <= 0) st.finish()
|
||
},
|
||
async onrpcreq(m) {
|
||
if (!disk.os) {
|
||
chan.messages[6].send({
|
||
id: m.id,
|
||
success: false,
|
||
result: '',
|
||
error: 'OS not initialized'
|
||
})
|
||
return
|
||
}
|
||
try {
|
||
const result = await disk.os.execRpc?.(m.module, m.method, m.args)
|
||
chan.messages[6].send({
|
||
id: m.id,
|
||
success: true,
|
||
result: String(result ?? ''),
|
||
error: ''
|
||
})
|
||
} catch (err) {
|
||
chan.messages[6].send({
|
||
id: m.id,
|
||
success: false,
|
||
result: '',
|
||
error: err.message
|
||
})
|
||
}
|
||
},
|
||
onrpcres(m) {
|
||
const cb = disk.pendingRpc.get(m.id)
|
||
if (cb) {
|
||
disk.pendingRpc.delete(m.id)
|
||
cb(m)
|
||
}
|
||
}
|
||
}
|
||
|
||
chan = mux.createChannel({ protocol: PROTOCOL_NAME, userData: context })
|
||
|
||
chan.addMessage({
|
||
encoding: c.uint32,
|
||
onmessage: (index, ch) => ch.userData.onread(index)
|
||
})
|
||
chan.addMessage({
|
||
encoding: {
|
||
preencode(state, m) {
|
||
c.uint32.preencode(state, m.index)
|
||
c.buffer.preencode(state, m.data)
|
||
},
|
||
encode(state, m) {
|
||
c.uint32.encode(state, m.index)
|
||
c.buffer.encode(state, m.data)
|
||
},
|
||
decode(state) {
|
||
return { index: c.uint32.decode(state), data: c.buffer.decode(state) }
|
||
}
|
||
},
|
||
onmessage: (m, ch) => ch.userData.ondata(m)
|
||
})
|
||
chan.addMessage({
|
||
encoding: c.buffer,
|
||
onmessage: (bitfield, ch) => ch.userData.ongossip(bitfield)
|
||
})
|
||
chan.addMessage({
|
||
encoding: {
|
||
preencode(state, m) {
|
||
c.uint32.preencode(state, m.id)
|
||
c.string.preencode(state, m.query)
|
||
},
|
||
encode(state, m) {
|
||
c.uint32.encode(state, m.id)
|
||
c.string.encode(state, m.query)
|
||
},
|
||
decode(state) {
|
||
return { id: c.uint32.decode(state), query: c.string.decode(state) }
|
||
}
|
||
},
|
||
onmessage: (m, ch) => ch.userData.onsearchreq(m)
|
||
})
|
||
chan.addMessage({
|
||
encoding: {
|
||
preencode(state, m) {
|
||
c.uint32.preencode(state, m.id)
|
||
c.array(c.string).preencode(state, m.matches)
|
||
},
|
||
encode(state, m) {
|
||
c.uint32.encode(state, m.id)
|
||
c.array(c.string).encode(state, m.matches)
|
||
},
|
||
decode(state) {
|
||
return {
|
||
id: c.uint32.decode(state),
|
||
matches: c.array(c.string).decode(state)
|
||
}
|
||
}
|
||
},
|
||
onmessage: (m, ch) => ch.userData.onsearchres(m)
|
||
})
|
||
chan.addMessage({
|
||
encoding: {
|
||
preencode(state, m) {
|
||
c.uint32.preencode(state, m.id)
|
||
c.string.preencode(state, m.module)
|
||
c.string.preencode(state, m.method)
|
||
c.array(c.string).preencode(state, m.args)
|
||
},
|
||
encode(state, m) {
|
||
c.uint32.encode(state, m.id)
|
||
c.string.encode(state, m.module)
|
||
c.string.encode(state, m.method)
|
||
c.array(c.string).encode(state, m.args)
|
||
},
|
||
decode(state) {
|
||
return {
|
||
id: c.uint32.decode(state),
|
||
module: c.string.decode(state),
|
||
method: c.string.decode(state),
|
||
args: c.array(c.string).decode(state)
|
||
}
|
||
}
|
||
},
|
||
onmessage: (m, ch) => ch.userData.onrpcreq(m)
|
||
})
|
||
chan.addMessage({
|
||
encoding: {
|
||
preencode(state, m) {
|
||
c.uint32.preencode(state, m.id)
|
||
c.bool.preencode(state, m.success)
|
||
c.string.preencode(state, m.result)
|
||
c.string.preencode(state, m.error)
|
||
},
|
||
encode(state, m) {
|
||
c.uint32.encode(state, m.id)
|
||
c.bool.encode(state, m.success)
|
||
c.string.encode(state, m.result || '')
|
||
c.string.encode(state, m.error || '')
|
||
},
|
||
decode(state) {
|
||
return {
|
||
id: c.uint32.decode(state),
|
||
success: c.bool.decode(state),
|
||
result: c.string.decode(state),
|
||
error: c.string.decode(state)
|
||
}
|
||
}
|
||
},
|
||
onmessage: (m, ch) => ch.userData.onrpcres(m)
|
||
})
|
||
|
||
chan.open()
|
||
|
||
const appChOn =
|
||
globalThis.process &&
|
||
globalThis.process.env &&
|
||
(globalThis.process.env.BARE_OS_PROTOMUX_APP_CHANNEL === '1' ||
|
||
globalThis.process.env.BARE_OS_PROTOMUX_APP_CHANNEL === 'true')
|
||
if (appChOn) {
|
||
mux.pair({ protocol: PROTOCOL_APP_CHANNEL_NAME }, (achan) => {
|
||
achan.addMessage({
|
||
encoding: c.buffer,
|
||
onmessage: () => {
|
||
try {
|
||
this.protomuxAppChannelRxTotal++
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
})
|
||
achan.open()
|
||
})
|
||
}
|
||
|
||
const capChOn =
|
||
globalThis.process &&
|
||
globalThis.process.env &&
|
||
(globalThis.process.env.BARE_OS_PROTOMUX_CAP_CHANNEL === '1' ||
|
||
globalThis.process.env.BARE_OS_PROTOMUX_CAP_CHANNEL === 'true')
|
||
if (capChOn) {
|
||
mux.pair({ protocol: PROTOCOL_CAP_CHANNEL_NAME }, (cchan) => {
|
||
cchan.addMessage({
|
||
encoding: c.buffer,
|
||
onmessage: (buf) => {
|
||
try {
|
||
const n = buf ? buf.byteLength : 0
|
||
if (n > BARE_OS_PROTOMUX_CAP_CHANNEL_MESSAGE_MAX_BYTES) {
|
||
emitSwarmDiskHostLog('warn', 'protomux_cap_payload_oversized', {
|
||
byteLength: n,
|
||
maxBytes: BARE_OS_PROTOMUX_CAP_CHANNEL_MESSAGE_MAX_BYTES
|
||
})
|
||
return
|
||
}
|
||
this.protomuxCapChannelRxTotal++
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
})
|
||
cchan.open()
|
||
})
|
||
}
|
||
|
||
const peer = {
|
||
chan,
|
||
mux,
|
||
socket,
|
||
id: null,
|
||
chatChan: null,
|
||
meshdropChan: null
|
||
}
|
||
if (
|
||
bareOsChatMuxEnabled(globalThis.process?.env) &&
|
||
this.bareOsChatService &&
|
||
mux.stream &&
|
||
!mux.stream.destroyed
|
||
) {
|
||
try {
|
||
this.bareOsChatService.pairOnMux(this, mux, socket, peer)
|
||
} catch (e) {
|
||
emitSwarmDiskHostLog('warn', 'bare_os_chat_pair_on_connect_failed', {
|
||
message:
|
||
(e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||
})
|
||
}
|
||
}
|
||
if (
|
||
bareOsMeshdropMuxEnabled(globalThis.process?.env) &&
|
||
this.bareOsMeshdropService &&
|
||
mux.stream &&
|
||
!mux.stream.destroyed
|
||
) {
|
||
try {
|
||
this.bareOsMeshdropService.pairOnMux(this, mux, socket, peer)
|
||
} catch (e) {
|
||
emitSwarmDiskHostLog(
|
||
'warn',
|
||
'bare_os_meshdrop_pair_on_connect_failed',
|
||
{
|
||
message:
|
||
(e && /** @type {{ message?: string }} */ (e).message) ||
|
||
String(e)
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
/** Sync local Noise static key once the secret stream exposes `publicKey` (may follow handshake). */
|
||
const cacheLocalNoiseWirePk = () => {
|
||
if (socket.publicKey && socket.publicKey.byteLength === 32) {
|
||
disk.localNoiseWirePk = b4a.from(socket.publicKey)
|
||
}
|
||
}
|
||
cacheLocalNoiseWirePk()
|
||
|
||
this.peers.add(peer)
|
||
|
||
const pendingIndices = [...this.pendingReads.keys()]
|
||
for (const idx of pendingIndices) {
|
||
this._sendReadIndexToPeer(peer, idx)
|
||
}
|
||
|
||
const collabNd =
|
||
globalThis.process &&
|
||
globalThis.process.env &&
|
||
(globalThis.process.env.BARE_OS_COLLAB_SESSION_NDJSON === '1' ||
|
||
globalThis.process.env.BARE_OS_COLLAB_SESSION_NDJSON === 'true')
|
||
if (collabNd) {
|
||
emitSwarmDiskHostLog('info', 'collab_session_peer', {
|
||
peerCount: this.peers.size,
|
||
capChannelEnabled: capChOn,
|
||
appChannelEnabled: appChOn,
|
||
note: 'Host NDJSON preview; mirror to personal-drive tooling off-guest if policy allows.'
|
||
})
|
||
}
|
||
|
||
const setPeerId = () => {
|
||
if (socket.remotePublicKey && !peer.id) {
|
||
peer.id = b4a.toString(socket.remotePublicKey, 'hex')
|
||
return true
|
||
}
|
||
if (socket.handshakeHash && !peer.id) {
|
||
peer.id = b4a.toString(socket.handshakeHash, 'hex')
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
socket.on('handshake', () => {
|
||
setPeerId()
|
||
cacheLocalNoiseWirePk()
|
||
})
|
||
if (!setPeerId()) {
|
||
let attempts = 0
|
||
const tryAgain = () => {
|
||
attempts++
|
||
if (attempts > 10) {
|
||
if (!peer.id) peer.id = 'peer-' + Date.now().toString(36)
|
||
return
|
||
}
|
||
if (!setPeerId()) setTimeout(tryAgain, attempts * 50)
|
||
}
|
||
setTimeout(tryAgain, 50)
|
||
}
|
||
|
||
mux.stream.on('close', () => {
|
||
this.peers.delete(peer)
|
||
})
|
||
|
||
if (this.drive)
|
||
this.drive.replicate(mux.stream, { live: true, download: true })
|
||
if (this.personalDrive) this.personalDrive.replicate(mux.stream)
|
||
for (const d of this.auxiliaryDrives || []) {
|
||
try {
|
||
d.replicate(mux.stream, { live: true, download: true })
|
||
} catch (_) {}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Protomux message 0: request block index (MBR). Only send after channel pairing.
|
||
* @param {unknown} peer
|
||
* @param {number} index
|
||
*/
|
||
_sendReadIndexToPeer(peer, index) {
|
||
const chan =
|
||
/** @type {{ chan: { messages: { send: (idx: number) => void }[] } }} */ (
|
||
peer
|
||
).chan
|
||
void chan.fullyOpened().then((opened) => {
|
||
if (!opened) return
|
||
if (!this.pendingReads.has(index)) return
|
||
try {
|
||
chan.messages[0].send(index)
|
||
} catch {
|
||
/* ignore — channel closed or not ready */
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Re-broadcast a pending block read to every connected peer (lossy links, slow pairing).
|
||
* @param {number} index
|
||
*/
|
||
_broadcastPendingReadIndex(index) {
|
||
for (const p of this.peers) {
|
||
this._sendReadIndexToPeer(p, index)
|
||
}
|
||
}
|
||
|
||
async read(index) {
|
||
if (this.localRAM.has(index)) return this.localRAM.get(index)
|
||
return new Promise((resolve, reject) => {
|
||
const timeoutMs = mbrReadTimeoutMsForDisk(this)
|
||
/** @type {ReturnType<typeof setInterval> | null} */
|
||
let rebroadcastIv = null
|
||
const timeout = setTimeout(() => {
|
||
if (rebroadcastIv) clearInterval(rebroadcastIv)
|
||
this.pendingReads.delete(index)
|
||
reject(new Error('MBR read timeout'))
|
||
}, timeoutMs)
|
||
this.pendingReads.set(index, (data) => {
|
||
if (rebroadcastIv) clearInterval(rebroadcastIv)
|
||
clearTimeout(timeout)
|
||
resolve(data)
|
||
})
|
||
const rbMs = mbrReadRebroadcastMsFromEnv(globalThis.process?.env)
|
||
this._broadcastPendingReadIndex(index)
|
||
if (rbMs > 0) {
|
||
rebroadcastIv = setInterval(() => {
|
||
if (!this.pendingReads.has(index)) {
|
||
if (rebroadcastIv) clearInterval(rebroadcastIv)
|
||
return
|
||
}
|
||
this._broadcastPendingReadIndex(index)
|
||
}, rbMs)
|
||
}
|
||
})
|
||
}
|
||
|
||
async search(query) {
|
||
const id = this.searchIdCounter++
|
||
const peerList = [...this.peers]
|
||
const n = peerList.length
|
||
if (!n) return []
|
||
const self = this
|
||
return new Promise((resolve) => {
|
||
/** @type {unknown[][]} */
|
||
const results = []
|
||
const state = {
|
||
remaining: n,
|
||
results,
|
||
settled: false,
|
||
timeout: /** @type {ReturnType<typeof setTimeout> | null} */ (null),
|
||
finish() {
|
||
if (state.settled) return
|
||
state.settled = true
|
||
if (state.timeout) clearTimeout(state.timeout)
|
||
self.pendingSearches.delete(id)
|
||
resolve(results.flat())
|
||
}
|
||
}
|
||
state.timeout = setTimeout(() => state.finish(), 3000)
|
||
self.pendingSearches.set(id, state)
|
||
for (const peer of peerList) {
|
||
void peer.chan.fullyOpened().then((opened) => {
|
||
if (!opened) return
|
||
if (state.settled) return
|
||
try {
|
||
peer.chan.messages[3].send({ id, query })
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* (Re)pair `bare-os-chat-v1` on every live mux — used after `ensureDiskBareOsChatTransport` replaces
|
||
* the service (guest → user, logout → guest) so `peer.chatChan` and handlers match the active instance.
|
||
*/
|
||
async pairBareOsChatExistingPeers() {
|
||
if (!bareOsChatMuxEnabled(globalThis.process?.env)) return
|
||
const svc = this.bareOsChatService
|
||
if (!svc || typeof svc.pairOnMux !== 'function') return
|
||
for (const peer of [...this.peers]) {
|
||
const st = peer.mux && peer.mux.stream
|
||
if (!st || st.destroyed) continue
|
||
try {
|
||
const mux = peer.mux
|
||
const wired = peer.chatChan
|
||
/** @type {Promise<unknown>[]} */
|
||
const closing = []
|
||
if (wired && typeof wired.close === 'function') {
|
||
try {
|
||
wired.close()
|
||
if (typeof wired.fullyClosed === 'function')
|
||
closing.push(wired.fullyClosed())
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
peer.chatChan = null
|
||
const prevCh =
|
||
mux &&
|
||
typeof mux.getLastChannel === 'function' &&
|
||
mux.getLastChannel({ protocol: PROTOCOL_CHAT_CHANNEL_NAME })
|
||
if (prevCh && typeof prevCh.close === 'function' && prevCh !== wired) {
|
||
try {
|
||
prevCh.close()
|
||
if (typeof prevCh.fullyClosed === 'function')
|
||
closing.push(prevCh.fullyClosed())
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (closing.length) await Promise.all(closing)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
if (typeof peer.mux.unpair === 'function') {
|
||
try {
|
||
peer.mux.unpair({ protocol: PROTOCOL_CHAT_CHANNEL_NAME })
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
svc.pairOnMux(this, peer.mux, peer.socket, peer)
|
||
} catch (e) {
|
||
emitSwarmDiskHostLog('warn', 'bare_os_chat_late_pair_failed', {
|
||
message:
|
||
(e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* (Re)pair `bare-os-meshdrop-v1` on every live mux — used after meshdrop service env/user transitions.
|
||
*/
|
||
pairBareOsMeshdropExistingPeers() {
|
||
if (!bareOsMeshdropMuxEnabled(globalThis.process?.env)) return
|
||
const svc = this.bareOsMeshdropService
|
||
if (!svc || typeof svc.pairOnMux !== 'function') return
|
||
for (const peer of [...this.peers]) {
|
||
const st = peer.mux && peer.mux.stream
|
||
if (!st || st.destroyed) continue
|
||
try {
|
||
const mux = peer.mux
|
||
const wired = peer.meshdropChan
|
||
if (wired && typeof wired.close === 'function') {
|
||
try {
|
||
wired.close()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
peer.meshdropChan = null
|
||
const prevCh =
|
||
mux &&
|
||
typeof mux.getLastChannel === 'function' &&
|
||
mux.getLastChannel({ protocol: PROTOCOL_MESHDROP_CHANNEL_NAME })
|
||
if (prevCh && typeof prevCh.close === 'function' && prevCh !== wired) {
|
||
try {
|
||
prevCh.close()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
svc.pairOnMux(this, peer.mux, peer.socket, peer)
|
||
if (typeof peer.mux.unpair === 'function') {
|
||
try {
|
||
peer.mux.unpair({ protocol: PROTOCOL_MESHDROP_CHANNEL_NAME })
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
} catch (e) {
|
||
emitSwarmDiskHostLog('warn', 'bare_os_meshdrop_late_pair_failed', {
|
||
message:
|
||
(e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|