Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion ts/packages/core/src/adapters/frame-codec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
// The CBOR frame codec shared by every message-based Connection adapter (WebSocket, WebRTC DataChannel, or any future one): one CBOR frame per message, no length prefix, with schema validation distinguishing an undecodable payload (connection-level failure) from a decodable-but-unrecognised frame (dropped, connection survives). Distinct from tcp-transport.ts's own inline codec, which frames a byte *stream* with a length prefix -- a different transport shape, not a duplicate of this one.

import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2";
import { frameSchema, type Frame } from "../generated/protocol.js";
import {
frameSchema,
type DeviceId,
type Frame,
type RelayDataFrame,
} from "../generated/protocol.js";

export function messageFromFrame(frame: Frame): Uint8Array<ArrayBuffer> {
// A fresh whole-buffer view over a plain ArrayBuffer: the WebSocket/DataChannel send signatures require it, and it matches the fresh-buffer discipline the other adapters apply to anything crossing a runtime boundary.
return new Uint8Array(encode(frame, cdeEncodeOptions));
}

/** Wraps frame as a relay-data-frame's own opaque payload, stamping to-device when the caller knows which established pairing to address it to (wire-mesh#30) -- the outbound counterpart to tryDecodeFrame's own doc comment below, which describes the inbound side of the same relay-data envelope. Omitting toDevice leaves the frame unaddressed, which the receiving hub then routes via its own most-recently-established-pairing fallback. */
export function wrapRelayData(
frame: Frame,
toDevice?: DeviceId,
): RelayDataFrame {
return {
type: "relay-data",
payload: messageFromFrame(frame),
...(toDevice !== undefined ? { "to-device": toDevice } : {}),
};
}

/** A frame that fails schema validation, caught separately from a decode failure so it can be dropped without disconnecting. */
export class SchemaInvalidFrameError extends Error {
constructor(message: string) {
Expand Down
53 changes: 28 additions & 25 deletions ts/packages/core/src/domain/mesh-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ import {
type ManageResponseFrame,
type PeerAdvert,
type ProtocolVersion,
type RelayDataFrame,
type RevocationAnnounceFrame,
type RevocationEntry,
} from "../generated/protocol.js";
import { SUPPORTED_PROTOCOL_VERSION, negotiate } from "./handshake.js";
import { deviceIdToHex } from "./device-id.js";
import { createRelayPairings } from "./relay-pairing.js";
import type { Clock } from "../ports/clock.js";
import type { IdentityPort } from "../ports/identity.js";
import type { Connection, Transport } from "../ports/transport.js";
import { messageFromFrame, tryDecodeFrame } from "../adapters/frame-codec.js";
import { tryDecodeFrame, wrapRelayData } from "../adapters/frame-codec.js";

const MS_PER_SECOND = 1000;

Expand Down Expand Up @@ -81,8 +83,10 @@ export interface IncomingManageRequest {
command: ManageCommand;
scope: CapabilityScope;
token?: CapabilityToken;
/** The device-id of the peer this request was relayed on behalf of, present only when the request arrived wrapped in a relay-data frame rather than directly over this session's own connection. A caller that needs to address a further request back to the same peer (one not sent via respond(), which already routes back correctly on its own) passes this as sendManageRequest's targetDevice. */
/** The device-id of the peer this request was relayed on behalf of, read directly from the enclosing relay-data-frame's own `from-device` field (stamped by the hub on every frame it forwards, wire-mesh#30) -- present only when the request arrived wrapped in a relay-data frame that carried one. A caller that needs to address a further request back to the same peer (one not sent via respond(), which already routes back correctly on its own) passes this as sendManageRequest's targetDevice. Never inferred from which relay pairing happens to be most recently established: a connection can hold several concurrent pairings (wire-mesh#30's own multiplexed adjacency map), so only the frame's own per-message addressing can say who actually sent it. */
fromDevice?: DeviceId;
/** The device-id this request's relay-data frame was explicitly addressed to, read from its own `to-device` field -- present only when the request arrived relay-wrapped and the frame carried one. A caller fronting more than one locally-addressable device behind a single hub connection (a gateway advertising several local peers through the same relay pairing) uses this to decide whether the request is for this device or should be routed on to a different local peer it also advertises; this session has no such routing logic of its own, since it represents exactly one identity. */
toDevice?: DeviceId;
respond: (outcome: ManageOutcome) => Promise<void>;
}

Expand Down Expand Up @@ -192,8 +196,8 @@ function createSessionCore(
let attempt = 0;
let currentToken: CapabilityToken | null = null;
let nextRequestId = 0;
// The device-id this session's relay-hub connection is currently paired with, in either role: set when this session sends its own relay-connect (initiator role), or when it receives a relay-inbound naming who is now paired with it (target role). relay-hub pairs at most one device per connection at a time -- a fresh relay-connect re-pairs totally -- so a single field is enough to track it, in whichever role this session is currently playing.
let relayPeerDevice: DeviceId | null = null;
// Every device this session's own connection currently holds a relay pairing with, in either role: added when this session sends its own relay-connect (initiator role) or when it receives a relay-inbound naming who is now paired with it (target role). See relay-pairing.ts for why establishing a pairing with a new target never discards an already-established one with a different target, and why this is never consulted for addressing -- only for ensureRelayPairing's own "already paired" check.
const relayPairings = createRelayPairings();
const pendingManageRequests = new Map<
number,
{
Expand Down Expand Up @@ -266,17 +270,17 @@ function createSessionCore(
};
}

/** Sends a frame, wrapping it as relay-data first when viaRelay is set -- the single choke point every outbound manage-request/manage-response passes through, so a consumer of sendManageRequest/respond never needs its own relay-wrapping logic. */
async function transmit(frame: Frame, viaRelay: boolean): Promise<void> {
/** Sends a frame, wrapping it as relay-data first when viaRelay is set -- the single choke point every outbound manage-request/manage-response passes through, so a consumer of sendManageRequest/respond never needs its own relay-wrapping logic. When relaying, toDevice is stamped onto the outer relay-data-frame's own `to-device` field so the hub addresses it to the correct pairing directly (wire-mesh#30) rather than falling back to whichever pairing it last saw -- the one case this is omitted is a response to a request that itself arrived with no from-device to echo back, which is left to that same hub fallback exactly as an unaddressed relay-data always has been. */
async function transmit(
frame: Frame,
viaRelay: boolean,
toDevice?: DeviceId,
): Promise<void> {
if (connection === null) {
throw new Error("not connected");
}
if (viaRelay) {
const relayFrame: Frame = {
type: "relay-data",
payload: messageFromFrame(frame),
};
await connection.send(relayFrame);
await connection.send(wrapRelayData(frame, toDevice));
return;
}
await connection.send(frame);
Expand All @@ -291,27 +295,29 @@ function createSessionCore(
}
}

/** relayFrame is present only for a manage-request that arrived wrapped in relay-data, and is that same outer relay-data-frame -- its own to-device/from-device fields carry whatever addressing it received. See IncomingManageRequest's own fromDevice/toDevice doc comments for what each means and why neither is ever guessed from pairing state. */
function applyManageRequest(
frame: ManageRequestFrame,
viaRelay: boolean,
relayFrame?: RelayDataFrame,
): void {
const requestId = frame["request-id"];
const fromDevice =
viaRelay && relayPeerDevice !== null ? relayPeerDevice : undefined;
const fromDevice = relayFrame?.["from-device"];
const toDevice = relayFrame?.["to-device"];
const incoming: IncomingManageRequest = {
requestId,
command: frame.command,
scope: frame.scope,
...(frame.token !== undefined ? { token: frame.token } : {}),
...(fromDevice !== undefined ? { fromDevice } : {}),
...(toDevice !== undefined ? { toDevice } : {}),
respond: async (outcome: ManageOutcome): Promise<void> => {
const response: ManageResponseFrame = {
type: "manage-response",
"request-id": requestId,
outcome,
};
frameLog.push({ direction: "sent", frame: response });
await transmit(response, viaRelay);
await transmit(response, relayFrame !== undefined, fromDevice);
emit();
},
};
Expand All @@ -330,7 +336,7 @@ function createSessionCore(
if (inner.type === "manage-response") {
applyManageResponse(inner);
} else {
applyManageRequest(inner, true);
applyManageRequest(inner, frame);
}
return;
}
Expand All @@ -351,27 +357,24 @@ function createSessionCore(
}
} else if (frame.type === "relay-inbound") {
// The target-role side of a relay-connect pairing learns who dialed it only via this frame -- there is no ack frame for relay-connect itself, so an initiator simply proceeds to relay-data right after sending it.
relayPeerDevice = frame["source-device"];
relayPairings.add(frame["source-device"]);
} else if (frame.type === "manage-response") {
applyManageResponse(frame);
} else if (frame.type === "manage-request") {
applyManageRequest(frame, false);
applyManageRequest(frame);
} else if (frame.type === "revocation-announce") {
for (const entry of frame.entries) {
emitRevocationEntry(entry);
}
}
}

/** Establishes a relay-connect pairing to targetDevice if this session isn't already paired with it -- a no-op when it already is, whether that pairing was established by this session's own prior relay-connect (initiator role) or learned from an incoming relay-inbound (target role, replying back to whoever dialed it). relay-connect has no ack frame: the initiator proceeds to relay-data right after sending it. */
/** Establishes a relay-connect pairing to targetDevice if this session isn't already paired with it -- a no-op when it already is, whether that pairing was established by this session's own prior relay-connect (initiator role) or learned from an incoming relay-inbound (target role, replying back to whoever dialed it). Pairing with a new target never tears down an existing pairing with a different one: this connection can hold several simultaneously (wire-mesh#30's own multiplexed adjacency map), so a later request back to an already-paired target must not re-send relay-connect for it. relay-connect has no ack frame: the initiator proceeds to relay-data right after sending it. */
async function ensureRelayPairing(targetDevice: DeviceId): Promise<void> {
if (connection === null) {
throw new Error("not connected");
}
if (
relayPeerDevice !== null &&
deviceIdToHex(relayPeerDevice) === deviceIdToHex(targetDevice)
) {
if (relayPairings.has(targetDevice)) {
return;
}
const relayConnect: Frame = {
Expand All @@ -380,7 +383,7 @@ function createSessionCore(
};
frameLog.push({ direction: "sent", frame: relayConnect });
await connection.send(relayConnect);
relayPeerDevice = targetDevice;
relayPairings.add(targetDevice);
emit();
}

Expand Down Expand Up @@ -636,7 +639,7 @@ function createSessionCore(
pendingManageRequests.set(requestId, { resolve, reject });
});
frameLog.push({ direction: "sent", frame });
await transmit(frame, targetDevice !== undefined);
await transmit(frame, targetDevice !== undefined, targetDevice);
emit();
if (timeoutMs === undefined) {
return outcome;
Expand Down
19 changes: 19 additions & 0 deletions ts/packages/core/src/domain/relay-pairing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Tracks every relay pairing a MeshSession's own connection currently holds, keyed by device hex -- the session-side counterpart to relay-hub.ts's own multiplexed adjacency map (wire-mesh#30): a connection can hold simultaneous pairings with several remote devices, so pairing with a new target must never discard an already-established pairing with a different one. Deliberately holds no addressing logic of its own -- outbound relay-data is always addressed explicitly via the caller's own known target/source device (see mesh-session.ts's transmit/applyManageRequest), and inbound attribution is read solely from each frame's own to-device/from-device fields, never guessed from this set. This exists only to answer "have we already relay-connected to this device", so ensureRelayPairing never sends a redundant relay-connect for a target it is already paired with.

import type { DeviceId } from "../generated/protocol.js";
import { deviceIdToHex } from "./device-id.js";

export interface RelayPairings {
has: (device: DeviceId) => boolean;
add: (device: DeviceId) => void;
}

export function createRelayPairings(): RelayPairings {
const established = new Map<string, DeviceId>();
return {
has: (device) => established.has(deviceIdToHex(device)),
add: (device) => {
established.set(deviceIdToHex(device), device);
},
};
}
1 change: 1 addition & 0 deletions ts/packages/core/test/mesh-session-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { deviceIdFromFillHex } from "./hex.js";

export const deviceA = deviceIdFromFillHex("11");
export const deviceB = deviceIdFromFillHex("22");
export const deviceC = deviceIdFromFillHex("33");

export const testIdentityDeviceId = deviceIdFromFillHex("ee");
export const testIdentity: IdentityPort = {
Expand Down
Loading