blob: 707cf29bfc42e13c213cc23c2342e65edd4637c4 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
authenticateSignedPeerReachabilityLease,
isPeerReachabilityLeaseCurrent,
isPeerReachabilityLeaseRecoverable,
PEER_REACHABILITY_MAX_CLOCK_SKEW_MS,
peerReachabilityLeaseReceipt,
verifySignedPeerReachabilityLease,
type PeerReachabilityLeaseReceipt,
type SignedPeerReachabilityLeaseV1,
} from '../peer-reachability/index.js';
import {
normalizePeerError,
RuntimeHostPeerError,
signRuntimeHostPeerIdentity,
startRuntimeHostPeerEndpoint,
verifyRuntimeHostPeerIdentity,
type RuntimeHostPeerIdentityProof,
type RuntimeHostPeerNativeEndpoint,
type RuntimeHostPeerNativeReachabilitySnapshot,
type RuntimeHostPeerNativeStream,
type RuntimeHostPeerTransitRelayCandidate,
type RuntimeHostPeerTransitSnapshot,
} from '../transport/peer-native.js';
import { RuntimeHostPermanentReconnectError } from './reconnect-lifecycle.js';
// One Desktop endpoint can retain 32 Host profiles and 128 guest mounts.
const AUTHENTICATED_REACHABILITY_MAX_ENTRIES = 160;
export interface RuntimeHostPeerConnectInput {
readonly peerId: string;
readonly routeHints: readonly string[];
readonly coordinationRelays?: readonly string[];
readonly transitRelayPeerIds?: readonly string[];
readonly directDeadlineMs: number;
readonly refreshRoutes?: boolean;
}
export type RuntimeHostPeerConnectionPhase = 'discovering' | 'connecting';
interface RuntimeHostPeerRouteCandidateSnapshot {
readonly routeHints: readonly string[];
readonly coordinationRelays: readonly string[];
readonly transitRelayPeerIds: readonly string[];
}
export type RuntimeHostPeerRouteResolution =
| (RuntimeHostPeerRouteCandidateSnapshot & { readonly state: 'available' })
| (RuntimeHostPeerRouteCandidateSnapshot & { readonly state: 'recovering' })
| (RuntimeHostPeerRouteCandidateSnapshot & { readonly state: 'exhausted' });
export interface RuntimeHostPeerRouteResolver {
peerConnected?(peerId: string): void;
resolveRoutes(peerId: string): RuntimeHostPeerRouteResolution;
prepareRoutes(peerId: string, signal: AbortSignal): Promise<void>;
subscribeRoutes(peerId: string, listener: () => void): () => void;
}
export class RuntimeHostPeerReachabilityUnavailableError extends Error {
readonly code = 'peer_reachability_needs_repair';
constructor(peerId: string) {
super(`Peer ${peerId} has no usable or recoverable route`);
this.name = 'RuntimeHostPeerReachabilityUnavailableError';
}
}
export interface RuntimeHostPeerClient {
reachability(): RuntimeHostPeerNativeReachabilitySnapshot;
watchReachability(afterGeneration: number, timeoutMs: number): Promise<number>;
identity(): Readonly<{
peerId: string;
}>;
signIdentity(payload: Buffer): Promise<RuntimeHostPeerIdentityProof>;
verifyIdentity(peerId: string, payload: Buffer, proof: RuntimeHostPeerIdentityProof): boolean;
isConnected(peerId: string): boolean;
transitSnapshot(): RuntimeHostPeerTransitSnapshot;
configureTransit(input: {
readonly allowedPeerIds: readonly string[];
readonly approvedRelayPeerIds: readonly string[];
readonly relayCandidates: readonly RuntimeHostPeerTransitRelayCandidate[];
}): Promise<void>;
attachRouteResolver(resolver: RuntimeHostPeerRouteResolver): () => void;
/** Notify reconnect owners when candidates change or a peer becomes connected. */
subscribeRoutes(peerId: string, listener: () => void): () => void;
observeAuthenticatedReachability(input: {
readonly expectedPeerId: string;
readonly value: unknown;
readonly allowHistorical?: boolean;
}): SignedPeerReachabilityLeaseV1;
connect(
input: RuntimeHostPeerConnectInput,
signal?: AbortSignal,
onPhase?: (phase: RuntimeHostPeerConnectionPhase) => void,
): Promise<RuntimeHostPeerNativeStream>;
connectMeshControl(
input: RuntimeHostPeerConnectInput,
signal?: AbortSignal,
): Promise<RuntimeHostPeerNativeStream>;
serveApplication(
onStream: (stream: RuntimeHostPeerNativeStream) => void,
signal: AbortSignal,
): Promise<void>;
serveMeshControl(
onStream: (stream: RuntimeHostPeerNativeStream) => void,
signal: AbortSignal,
): Promise<void>;
close(): Promise<void>;
}
export function createRuntimeHostPeerClientFromEnvironment(
environment: NodeJS.ProcessEnv = process.env,
options: {
readonly listenAddresses?: readonly string[];
readonly relayAnchorPath?: string;
readonly coordinationRelays?: readonly string[];
readonly automaticRelayDiscovery?: boolean;
readonly webRtcStunUrls?: readonly string[];
} = {},
): RuntimeHostPeerClient {
const nativePath = environment.MAKA_RUNTIME_HOST_PEER_NATIVE_PATH;
const keyPath = environment.MAKA_RUNTIME_HOST_PEER_KEY_PATH;
if (!nativePath || !keyPath) {
throw new RuntimeHostPeerError(
'peer_native_unavailable',
'Experimental direct peer requires MAKA_RUNTIME_HOST_PEER_NATIVE_PATH and MAKA_RUNTIME_HOST_PEER_KEY_PATH',
);
}
return createRuntimeHostPeerClient({ nativePath, keyPath, ...options });
}
export function createRuntimeHostPeerClient(input: {
readonly nativePath: string;
readonly keyPath: string;
readonly relayAnchorPath?: string;
readonly expectedPeerId?: string;
readonly listenAddresses?: readonly string[];
readonly coordinationRelays?: readonly string[];
readonly automaticRelayDiscovery?: boolean;
readonly webRtcStunUrls?: readonly string[];
}): RuntimeHostPeerClient {
return new RuntimeHostPeerClientImpl(input);
}
class RuntimeHostPeerClientImpl implements RuntimeHostPeerClient {
readonly #nativePath: string;
readonly #keyPath: string;
readonly #relayAnchorPath: string | undefined;
readonly #expectedPeerId: string | undefined;
readonly #listenAddresses: readonly string[] | undefined;
readonly #coordinationRelays: readonly string[] | undefined;
readonly #automaticRelayDiscovery: boolean;
readonly #webRtcStunUrls: readonly string[] | undefined;
#routeResolver: RuntimeHostPeerRouteResolver | undefined;
readonly #routeListeners = new Map<string, Set<() => void>>();
readonly #routeResolverSubscriptions = new Map<string, () => void>();
readonly #authenticatedReachability = new Map<string, AuthenticatedReachability>();
#endpoint: RuntimeHostPeerNativeEndpoint | undefined;
#draining: Promise<void> | undefined;
#meshDraining: Promise<void> | undefined;
#connectivityDraining: Promise<void> | undefined;
#applicationConsumer: InboundConsumer | undefined;
#meshConsumer: InboundConsumer | undefined;
#terminalError: Error | undefined;
readonly #connectTails = new Map<string, Promise<void>>();
#nextRequestId = 1;
#closed = false;
#closeTask: Promise<void> | undefined;
constructor(input: {
readonly nativePath: string;
readonly keyPath: string;
readonly relayAnchorPath?: string;
readonly expectedPeerId?: string;
readonly listenAddresses?: readonly string[];
readonly coordinationRelays?: readonly string[];
readonly automaticRelayDiscovery?: boolean;
readonly webRtcStunUrls?: readonly string[];
}) {
this.#nativePath = input.nativePath;
this.#keyPath = input.keyPath;
this.#relayAnchorPath = input.relayAnchorPath;
this.#expectedPeerId = input.expectedPeerId;
this.#listenAddresses = input.listenAddresses;
this.#coordinationRelays = input.coordinationRelays;
this.#automaticRelayDiscovery = input.automaticRelayDiscovery ?? false;
this.#webRtcStunUrls =
input.webRtcStunUrls === undefined ? undefined : [...input.webRtcStunUrls];
}
identity(): Readonly<{
peerId: string;
}> {
return Object.freeze({ peerId: this.#requireEndpoint().peerId });
}
reachability(): RuntimeHostPeerNativeReachabilitySnapshot {
return this.#requireEndpoint().reachabilitySnapshot;
}
async watchReachability(afterGeneration: number, timeoutMs: number): Promise<number> {
try {
return await this.#requireEndpoint().watchReachability(afterGeneration, timeoutMs);
} catch (error) {
throw normalizePeerError(error);
}
}
signIdentity(payload: Buffer): Promise<RuntimeHostPeerIdentityProof> {
const peerId = this.#requireEndpoint().peerId;
return signRuntimeHostPeerIdentity({
nativePath: this.#nativePath,
keyPath: this.#keyPath,
expectedPeerId: peerId,
payload,
});
}
verifyIdentity(peerId: string, payload: Buffer, proof: RuntimeHostPeerIdentityProof): boolean {
return verifyRuntimeHostPeerIdentity({
nativePath: this.#nativePath,
peerId,
payload,
publicKey: proof.publicKey,
signature: proof.signature,
});
}
isConnected(peerId: string): boolean {
return this.#endpoint?.connectivitySnapshot.connectedPeerIds.includes(peerId) ?? false;
}
transitSnapshot(): RuntimeHostPeerTransitSnapshot {
return Object.freeze({ ...this.#requireEndpoint().transitSnapshot });
}
configureTransit(input: {
readonly allowedPeerIds: readonly string[];
readonly approvedRelayPeerIds: readonly string[];
readonly relayCandidates: readonly RuntimeHostPeerTransitRelayCandidate[];
}): Promise<void> {
return this.#requireEndpoint()
.configureTransit(input)
.catch((error: unknown) => {
throw normalizePeerError(error);
});
}
attachRouteResolver(resolver: RuntimeHostPeerRouteResolver): () => void {
if (this.#routeResolver && this.#routeResolver !== resolver) {
throw new Error('Runtime Host peer client already has a reachability resolver');
}
if (this.#routeResolver === resolver) return () => undefined;
this.#routeResolver = resolver;
for (const peerId of this.#endpoint?.connectivitySnapshot.connectedPeerIds ?? []) {
this.#notifyPeerConnected(peerId);
}
for (const peerId of this.#routeListeners.keys()) {
this.#subscribeResolver(peerId);
this.#notifyRouteChange(peerId);
}
let attached = true;
return () => {
if (!attached) return;
attached = false;
if (this.#routeResolver !== resolver) return;
this.#routeResolver = undefined;
for (const unsubscribe of this.#routeResolverSubscriptions.values()) unsubscribe();
this.#routeResolverSubscriptions.clear();
for (const peerId of this.#routeListeners.keys()) this.#notifyRouteChange(peerId);
};
}
subscribeRoutes(peerId: string, listener: () => void): () => void {
const snapshot = () =>
this.#connectionResolution(
{ peerId, routeHints: [], directDeadlineMs: 0 },
'application',
false,
);
let previous = snapshot();
let connected = this.isConnected(peerId);
return this.#subscribeRouteResolution(peerId, () => {
const next = snapshot();
const nextConnected = this.isConnected(peerId);
const available =
next.state === 'available' &&
(!sameCandidates(previous, next) || (nextConnected && !connected));
previous = next;
connected = nextConnected;
// A dial's own recovery sweep toggles recovering/exhausted. Waking its
// reconnect owner for those transitions would bypass every backoff.
if (available) listener();
});
}
#subscribeRouteResolution(peerId: string, listener: () => void): () => void {
const listeners = this.#routeListeners.get(peerId) ?? new Set<() => void>();
const first = listeners.size === 0;
listeners.add(listener);
this.#routeListeners.set(peerId, listeners);
if (first) this.#subscribeResolver(peerId);
let subscribed = true;
return () => {
if (!subscribed) return;
subscribed = false;
listeners.delete(listener);
if (listeners.size > 0) return;
this.#routeListeners.delete(peerId);
this.#routeResolverSubscriptions.get(peerId)?.();
this.#routeResolverSubscriptions.delete(peerId);
};
}
observeAuthenticatedReachability(input: {
readonly expectedPeerId: string;
readonly value: unknown;
readonly allowHistorical?: boolean;
}): SignedPeerReachabilityLeaseV1 {
const now = Date.now();
const identity = {
value: input.value,
expectedPeerId: input.expectedPeerId,
verifyIdentity: this.verifyIdentity.bind(this),
};
const next = input.allowHistorical
? authenticateSignedPeerReachabilityLease(identity)
: verifySignedPeerReachabilityLease({ ...identity, now });
this.#pruneAuthenticatedReachability(now);
let current = this.#authenticatedReachability.get(input.expectedPeerId);
if (!isPeerReachabilityLeaseRecoverable(next.lease, now)) return current?.signed ?? next;
if (current && current.signed.lease.revision >= next.lease.revision) {
if (
current.signed.lease.revision === next.lease.revision &&
!sameReachability(current.signed, next)
) {
throw new Error('Peer reachability revision contains conflicting signed facts');
}
this.#rememberAuthenticatedReachability(input.expectedPeerId, current);
return current.signed;
}
this.#rememberAuthenticatedReachability(input.expectedPeerId, {
signed: next,
...(next.lease.issuedAt <= now + PEER_REACHABILITY_MAX_CLOCK_SKEW_MS
? {
receipt: peerReachabilityLeaseReceipt({
signed: next,
wallNow: now,
monotonicNow: performance.now(),
}),
}
: {}),
});
this.#notifyRouteChange(input.expectedPeerId);
return next;
}
#pruneAuthenticatedReachability(now: number): void {
for (const [peerId, authenticated] of this.#authenticatedReachability) {
if (!isPeerReachabilityLeaseRecoverable(authenticated.signed.lease, now)) {
this.#authenticatedReachability.delete(peerId);
this.#notifyRouteChange(peerId);
}
}
}
#rememberAuthenticatedReachability(
peerId: string,
authenticated: AuthenticatedReachability,
): void {
this.#authenticatedReachability.delete(peerId);
this.#authenticatedReachability.set(peerId, authenticated);
while (this.#authenticatedReachability.size > AUTHENTICATED_REACHABILITY_MAX_ENTRIES) {
let unobservedPeerId: string | undefined;
for (const candidatePeerId of this.#authenticatedReachability.keys()) {
if (this.#routeListeners.has(candidatePeerId)) continue;
unobservedPeerId = candidatePeerId;
break;
}
const evictedPeerId = unobservedPeerId ?? this.#authenticatedReachability.keys().next().value;
if (evictedPeerId === undefined) break;
this.#authenticatedReachability.delete(evictedPeerId);
this.#notifyRouteChange(evictedPeerId);
}
}
async connect(
input: RuntimeHostPeerConnectInput,
signal?: AbortSignal,
onPhase?: (phase: RuntimeHostPeerConnectionPhase) => void,
): Promise<RuntimeHostPeerNativeStream> {
if (input.refreshRoutes !== false && this.#routeResolver) {
notifyPhase(onPhase, 'discovering');
}
notifyPhase(onPhase, 'connecting');
return this.#connect(input, signal, 'application');
}
async #prepareRoutes(
input: RuntimeHostPeerConnectInput,
signal: AbortSignal | undefined,
): Promise<void> {
const resolver = this.#routeResolver;
if (!resolver) return;
const deadline = AbortSignal.timeout(Math.min(10_000, input.directDeadlineMs));
const operationSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
try {
await resolver.prepareRoutes(input.peerId, operationSignal);
} catch {
// Route preparation enriches an invitation/profile with fresher Mesh
// routes. It must not suppress explicit routes the caller already has.
signal?.throwIfAborted();
}
}
async connectMeshControl(
input: RuntimeHostPeerConnectInput,
signal?: AbortSignal,
): Promise<RuntimeHostPeerNativeStream> {
return this.#connect(input, signal, 'mesh-control');
}
serveApplication(
onStream: (stream: RuntimeHostPeerNativeStream) => void,
signal: AbortSignal,
): Promise<void> {
return this.#serve('application', onStream, signal);
}
serveMeshControl(
onStream: (stream: RuntimeHostPeerNativeStream) => void,
signal: AbortSignal,
): Promise<void> {
return this.#serve('mesh', onStream, signal);
}
#serve(
kind: 'application' | 'mesh',
onStream: (stream: RuntimeHostPeerNativeStream) => void,
signal: AbortSignal,
): Promise<void> {
signal.throwIfAborted();
if (kind === 'application' ? this.#applicationConsumer : this.#meshConsumer) {
return Promise.reject(
new Error(
kind === 'application'
? 'Runtime Host peer application traffic is already being served'
: 'Runtime Host peer Mesh control is already being served',
),
);
}
this.#requireEndpoint();
let resolve!: () => void;
let reject!: (error: Error) => void;
const serving = new Promise<void>((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
const consumer = { onStream, resolve, reject };
if (kind === 'application') this.#applicationConsumer = consumer;
else this.#meshConsumer = consumer;
const stop = () => {
if (kind === 'application') {
if (this.#applicationConsumer !== consumer) return;
this.#applicationConsumer = undefined;
} else {
if (this.#meshConsumer !== consumer) return;
this.#meshConsumer = undefined;
}
resolve();
};
signal.addEventListener('abort', stop, { once: true });
if (signal.aborted) stop();
return serving.finally(() => {
signal.removeEventListener('abort', stop);
if (kind === 'application' && this.#applicationConsumer === consumer) {
this.#applicationConsumer = undefined;
}
if (kind === 'mesh' && this.#meshConsumer === consumer) this.#meshConsumer = undefined;
});
}
async #connect(
input: RuntimeHostPeerConnectInput,
signal: AbortSignal | undefined,
kind: 'application' | 'mesh-control',
): Promise<RuntimeHostPeerNativeStream> {
// Mesh reconciliation must not consume the foreground application's dial
// budget. The native endpoint multiplexes both lanes over peer connections.
const lane = `${kind}:${input.peerId}`;
const previous = this.#connectTails.get(lane) ?? Promise.resolve();
let release!: () => void;
const turn = new Promise<void>((resolve) => {
release = resolve;
});
const tail = previous.then(() => turn);
this.#connectTails.set(lane, tail);
try {
await waitForPeerConnectTurn(previous, signal);
return await this.#startConnect(input, signal, kind);
} finally {
release();
void tail.then(() => {
if (this.#connectTails.get(lane) === tail) this.#connectTails.delete(lane);
});
}
}
async #startConnect(
input: RuntimeHostPeerConnectInput,
signal: AbortSignal | undefined,
kind: 'application' | 'mesh-control',
): Promise<RuntimeHostPeerNativeStream> {
signal?.throwIfAborted();
const endpoint = this.#requireEndpoint();
let settled = false;
let preparing = Boolean(
kind === 'application' && input.refreshRoutes !== false && this.#routeResolver,
);
const snapshot = () => this.#connectionResolution(input, kind, preparing);
let resolution = snapshot();
if (resolution.state === 'exhausted') {
throw new RuntimeHostPeerReachabilityUnavailableError(input.peerId);
}
const requestId = this.#allocateRequestId();
const attemptLifetime = new AbortController();
let reachabilityFailure: RuntimeHostPeerReachabilityUnavailableError | undefined;
let updateTail = Promise.resolve();
const update = () => {
if (settled) return;
const next = snapshot();
if (sameConnectionResolution(resolution, next)) return;
const candidatesChanged = !sameCandidates(resolution, next);
resolution = next;
if (next.state === 'exhausted') {
reachabilityFailure ??= new RuntimeHostPeerReachabilityUnavailableError(input.peerId);
void cancelPeerConnect(endpoint, requestId, () => settled);
return;
}
if (!candidatesChanged) return;
const candidates = connectCandidates(next);
updateTail = updateTail.then(
() => updatePeerConnect(endpoint, requestId, candidates, () => settled),
() => updatePeerConnect(endpoint, requestId, candidates, () => settled),
);
void updateTail.catch(() => undefined);
};
const unsubscribe =
kind === 'application' ? this.#subscribeRouteResolution(input.peerId, update) : undefined;
let connection: Promise<RuntimeHostPeerNativeStream>;
try {
connection = endpoint[kind === 'application' ? 'connect' : 'connectMeshControl']({
...input,
...connectCandidates(resolution),
requestId,
});
} catch (error) {
settled = true;
attemptLifetime.abort();
unsubscribe?.();
throw normalizePeerError(error);
}
if (preparing) {
const prepared = this.#prepareRoutes(
input,
signal ? AbortSignal.any([signal, attemptLifetime.signal]) : attemptLifetime.signal,
);
void prepared.then(
() => {
preparing = false;
update();
},
() => {
preparing = false;
update();
},
);
}
const cancel = () => {
void cancelPeerConnect(endpoint, requestId, () => settled);
};
signal?.addEventListener('abort', cancel, { once: true });
if (signal?.aborted) cancel();
try {
const stream = await connection;
if (reachabilityFailure) {
stream.abort();
throw reachabilityFailure;
}
if (signal?.aborted) {
stream.abort();
signal.throwIfAborted();
}
return stream;
} catch (error) {
signal?.throwIfAborted();
if (reachabilityFailure) throw reachabilityFailure;
throw normalizePeerError(error);
} finally {
settled = true;
attemptLifetime.abort();
unsubscribe?.();
signal?.removeEventListener('abort', cancel);
}
}
#connectionResolution(
input: RuntimeHostPeerConnectInput,
kind: 'application' | 'mesh-control',
preparing: boolean,
): RuntimeHostPeerConnectResolution {
const discovered =
kind === 'application' ? this.#routeResolver?.resolveRoutes(input.peerId) : undefined;
const rememberedEntry =
kind === 'application' ? this.#authenticatedReachability.get(input.peerId) : undefined;
const remembered = rememberedEntry?.signed;
const authenticated =
remembered && isPeerReachabilityLeaseRecoverable(remembered.lease, Date.now())
? remembered
: undefined;
if (remembered && !authenticated) this.#authenticatedReachability.delete(input.peerId);
const currentAuthenticated = Boolean(
authenticated &&
rememberedEntry &&
isPeerReachabilityLeaseCurrent(authenticated, rememberedEntry.receipt, performance.now()),
);
const candidates = {
routeHints: mergeAddresses(discovered?.routeHints ?? [], [
...(currentAuthenticated ? (authenticated?.lease.directRoutes ?? []) : []),
...input.routeHints,
...(!currentAuthenticated ? (authenticated?.lease.directRoutes ?? []) : []),
]),
coordinationRelays: mergeAddresses(discovered?.coordinationRelays ?? [], [
...(currentAuthenticated ? (authenticated?.lease.coordinationRoutes ?? []) : []),
...(input.coordinationRelays ?? []),
...(!currentAuthenticated ? (authenticated?.lease.coordinationRoutes ?? []) : []),
]),
transitRelayPeerIds: mergeValues(
discovered?.transitRelayPeerIds ?? [],
input.transitRelayPeerIds,
64,
),
};
const connected =
this.#endpoint?.connectivitySnapshot.connectedPeerIds.includes(input.peerId) ?? false;
return Object.freeze({
...candidates,
state:
hasConnectionCandidates(candidates) || connected
? 'available'
: preparing
? 'recovering'
: (discovered?.state ?? 'exhausted'),
});
}
#subscribeResolver(peerId: string): void {
if (this.#routeResolverSubscriptions.has(peerId)) return;
const resolver = this.#routeResolver;
if (!resolver) return;
const unsubscribe = resolver.subscribeRoutes(peerId, () => {
this.#notifyRouteChange(peerId);
});
this.#routeResolverSubscriptions.set(peerId, unsubscribe);
}
#notifyRouteChange(peerId: string): void {
for (const listener of this.#routeListeners.get(peerId) ?? []) {
try {
listener();
} catch {
// Reachability evidence cannot let one observer disrupt the others.
}
}
}
close(): Promise<void> {
this.#closeTask ??= this.#close();
return this.#closeTask;
}
#requireEndpoint(): RuntimeHostPeerNativeEndpoint {
if (this.#closed) {
throw new RuntimeHostPeerError('peer_native_failed', 'Runtime Host peer client is closed');
}
if (this.#terminalError) {
throw new RuntimeHostPermanentReconnectError(
'Runtime Host peer networking stopped and cannot recover until this Client restarts',
{ cause: this.#terminalError },
);
}
if (this.#endpoint) return this.#endpoint;
const endpoint = startRuntimeHostPeerEndpoint({
nativePath: this.#nativePath,
keyPath: this.#keyPath,
...(this.#relayAnchorPath ? { relayAnchorPath: this.#relayAnchorPath } : {}),
...(this.#expectedPeerId ? { expectedPeerId: this.#expectedPeerId } : {}),
...(this.#listenAddresses ? { listenAddresses: this.#listenAddresses } : {}),
...(this.#coordinationRelays ? { coordinationRelays: this.#coordinationRelays } : {}),
automaticRelayDiscovery: this.#automaticRelayDiscovery,
...(this.#webRtcStunUrls === undefined ? {} : { webRtcStunUrls: this.#webRtcStunUrls }),
});
this.#endpoint = endpoint;
this.#draining = this.#drainInbound(endpoint);
this.#meshDraining = this.#drainMeshInbound(endpoint);
this.#connectivityDraining = this.#drainConnectivity(endpoint);
return endpoint;
}
async #drainConnectivity(endpoint: RuntimeHostPeerNativeEndpoint): Promise<void> {
let current = endpoint.connectivitySnapshot;
try {
while (!this.#closed) {
const next = await endpoint.watchConnectivity(current.generation, 300_000);
const previousPeers = new Set(current.connectedPeerIds);
const nextPeers = new Set(next.connectedPeerIds);
current = next;
for (const peerId of new Set([...previousPeers, ...nextPeers])) {
if (previousPeers.has(peerId) !== nextPeers.has(peerId)) this.#notifyRouteChange(peerId);
if (!previousPeers.has(peerId) && nextPeers.has(peerId))
this.#notifyPeerConnected(peerId);
}
}
} catch (error) {
if (this.#closed) return;
this.#terminalError = error instanceof Error ? error : new Error(String(error));
this.#finishConsumer('application', this.#terminalError);
this.#finishConsumer('mesh', this.#terminalError);
}
}
#notifyPeerConnected(peerId: string): void {
try {
this.#routeResolver?.peerConnected?.(peerId);
} catch {
// Mesh recovery must not interrupt the transport's connectivity watcher.
}
}
async #drainInbound(endpoint: RuntimeHostPeerNativeEndpoint): Promise<void> {
try {
while (true) {
const stream = await endpoint.accept();
if (!stream) {
const error = new Error('Runtime Host peer networking stopped unexpectedly');
if (!this.#closed) this.#terminalError = error;
this.#finishConsumer('application', this.#closed ? undefined : error);
return;
}
const consumer = this.#applicationConsumer;
if (consumer) consumer.onStream(stream);
else stream.abort();
}
} catch (error) {
// Connection attempts and streams expose a terminal native failure to
// their existing reconnect owners. This owner never replaces its Swarm.
this.#terminalError = error instanceof Error ? error : new Error(String(error));
this.#finishConsumer('application', this.#closed ? undefined : this.#terminalError);
}
}
async #drainMeshInbound(endpoint: RuntimeHostPeerNativeEndpoint): Promise<void> {
try {
while (true) {
const stream = await endpoint.acceptMeshControl();
if (!stream) {
const error = new Error('Runtime Host peer networking stopped unexpectedly');
if (!this.#closed) this.#terminalError = error;
this.#finishConsumer('mesh', this.#closed ? undefined : error);
return;
}
const consumer = this.#meshConsumer;
if (consumer) consumer.onStream(stream);
else stream.abort();
}
} catch (error) {
const failure = error instanceof Error ? error : new Error(String(error));
if (!this.#closed) this.#terminalError = failure;
this.#finishConsumer('mesh', this.#closed ? undefined : failure);
}
}
#finishConsumer(kind: 'application' | 'mesh', error?: Error): void {
const consumer = kind === 'application' ? this.#applicationConsumer : this.#meshConsumer;
if (!consumer) return;
if (kind === 'application') this.#applicationConsumer = undefined;
else this.#meshConsumer = undefined;
if (error) consumer.reject(error);
else consumer.resolve();
}
async #close(): Promise<void> {
this.#closed = true;
for (const unsubscribe of this.#routeResolverSubscriptions.values()) unsubscribe();
this.#routeResolverSubscriptions.clear();
this.#routeListeners.clear();
this.#authenticatedReachability.clear();
const endpoint = this.#endpoint;
this.#endpoint = undefined;
if (!endpoint) return;
let closeError: unknown;
let closeFailed = false;
try {
await endpoint.close();
} catch (error) {
closeFailed = true;
closeError = error;
}
await Promise.all([this.#draining, this.#meshDraining, this.#connectivityDraining]);
if (closeFailed) throw closeError;
}
#allocateRequestId(): number {
const requestId = this.#nextRequestId;
this.#nextRequestId = requestId === 0xffff_ffff ? 1 : requestId + 1;
return requestId;
}
}
interface InboundConsumer {
readonly onStream: (stream: RuntimeHostPeerNativeStream) => void;
readonly resolve: () => void;
readonly reject: (error: Error) => void;
}
interface AuthenticatedReachability {
readonly signed: SignedPeerReachabilityLeaseV1;
readonly receipt?: PeerReachabilityLeaseReceipt;
}
interface RuntimeHostPeerConnectCandidates {
readonly routeHints: readonly string[];
readonly coordinationRelays: readonly string[];
readonly transitRelayPeerIds: readonly string[];
}
interface RuntimeHostPeerConnectResolution extends RuntimeHostPeerConnectCandidates {
readonly state: RuntimeHostPeerRouteResolution['state'];
}
function notifyPhase(
observer: ((phase: RuntimeHostPeerConnectionPhase) => void) | undefined,
phase: RuntimeHostPeerConnectionPhase,
): void {
try {
observer?.(phase);
} catch {
// Connection progress is diagnostic state and cannot control the connection.
}
}
function waitForPeerConnectTurn(previous: Promise<void>, signal?: AbortSignal): Promise<void> {
if (!signal) return previous;
if (signal.aborted) return Promise.reject(signal.reason);
return new Promise((resolve, reject) => {
const onAbort = () => {
signal.removeEventListener('abort', onAbort);
reject(signal.reason);
};
signal.addEventListener('abort', onAbort, { once: true });
void previous.then(() => {
signal.removeEventListener('abort', onAbort);
resolve();
});
});
}
function mergeAddresses(
primary: readonly string[],
secondary: readonly string[] | undefined,
): readonly string[] {
return mergeValues(primary, secondary, 32);
}
function mergeValues(
primary: readonly string[],
secondary: readonly string[] | undefined,
limit: number,
): readonly string[] {
return Object.freeze([...new Set([...primary, ...(secondary ?? [])])].slice(0, limit));
}
function sameCandidates(
left: RuntimeHostPeerConnectCandidates,
right: RuntimeHostPeerConnectCandidates,
): boolean {
return (
sameValues(left.routeHints, right.routeHints) &&
sameValues(left.coordinationRelays, right.coordinationRelays) &&
sameValues(left.transitRelayPeerIds, right.transitRelayPeerIds)
);
}
function sameConnectionResolution(
left: RuntimeHostPeerConnectResolution,
right: RuntimeHostPeerConnectResolution,
): boolean {
return left.state === right.state && sameCandidates(left, right);
}
function hasConnectionCandidates(candidates: RuntimeHostPeerConnectCandidates): boolean {
return (
candidates.routeHints.length > 0 ||
candidates.coordinationRelays.length > 0 ||
candidates.transitRelayPeerIds.length > 0
);
}
function connectCandidates(
resolution: RuntimeHostPeerConnectResolution,
): RuntimeHostPeerConnectCandidates {
return {
routeHints: resolution.routeHints,
coordinationRelays: resolution.coordinationRelays,
transitRelayPeerIds: resolution.transitRelayPeerIds,
};
}
function sameValues(left: readonly string[], right: readonly string[]): boolean {
return left.length === right.length && left.every((value, index) => value === right[index]);
}
function sameReachability(
left: SignedPeerReachabilityLeaseV1,
right: SignedPeerReachabilityLeaseV1,
): boolean {
return (
left.publicKey === right.publicKey &&
left.signature === right.signature &&
left.lease.peerId === right.lease.peerId &&
left.lease.revision === right.lease.revision &&
left.lease.issuedAt === right.lease.issuedAt &&
left.lease.expiresAt === right.lease.expiresAt &&
sameValues(left.lease.directRoutes, right.lease.directRoutes) &&
sameValues(left.lease.coordinationRoutes, right.lease.coordinationRoutes)
);
}
async function cancelPeerConnect(
endpoint: RuntimeHostPeerNativeEndpoint,
requestId: number,
isSettled: () => boolean,
): Promise<void> {
try {
while (!isSettled() && !(await endpoint.cancelConnect(requestId))) {
// N-API schedules connect and cancel independently. Retry until the
// engine has observed the request or the connect promise settles.
}
} catch {
// The endpoint closing also settles the connect promise.
}
}
async function updatePeerConnect(
endpoint: RuntimeHostPeerNativeEndpoint,
requestId: number,
candidates: RuntimeHostPeerConnectCandidates,
isSettled: () => boolean,
): Promise<void> {
try {
while (!isSettled() && !(await endpoint.updateConnect({ requestId, ...candidates }))) {
// N-API schedules connect and updates independently. Retry until the
// engine has observed the request or the connect promise settles.
}
} catch {
// The active connection owns failures; route enrichment is best effort.
}
}