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
21 changes: 18 additions & 3 deletions ts/packages/core/src/domain/mesh-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,13 @@ export interface MeshSession {
sendRevocationAnnounce: (
entries: readonly RevocationEntry[],
) => Promise<void>;
/** Sends a manage-request and resolves with the matching manage-response's outcome, correlated by request-id. When targetDevice is given, the request is routed to that specific peer via an established relay-connect pairing (wrapped as relay-data) rather than sent directly over this session's own Connection -- relay-hub deliberately drops manage-request/manage-response frames sent to it directly, since routing between two connected peers is not the relay role's business, so a specific peer reachable only through a relay hub can only be addressed this way. Absent, this sends directly over the Connection exactly as before. When token is given, it is attached to this one request instead of whatever setToken last set -- a single session routinely needs a different token per request when its peer shares more than one scope with this side (e.g. several core/room memberships over one connection), and a session-global token can only ever be correct for one of them. Absent, this request carries setToken's own session-global token exactly as before. */
/** Sends a manage-request and resolves with the matching manage-response's outcome, correlated by request-id. When targetDevice is given, the request is routed to that specific peer via an established relay-connect pairing (wrapped as relay-data) rather than sent directly over this session's own Connection -- relay-hub deliberately drops manage-request/manage-response frames sent to it directly, since routing between two connected peers is not the relay role's business, so a specific peer reachable only through a relay hub can only be addressed this way. Absent, this sends directly over the Connection exactly as before. When token is given, it is attached to this one request instead of whatever setToken last set -- a single session routinely needs a different token per request when its peer shares more than one scope with this side (e.g. several core/room memberships over one connection), and a session-global token can only ever be correct for one of them. Absent, this request carries setToken's own session-global token exactly as before. When timeoutMs is given, the returned promise resolves with `{ result: "error", code: "timeout" }` rather than hanging forever if no manage-response arrives in time -- a held-open request (a human approval, a not-yet-online peer) otherwise has no way for the caller to give up on it. Absent, this request waits exactly as before, with no time limit of its own. */
sendManageRequest: (
command: ManageCommand,
scope: Readonly<CapabilityScope>,
targetDevice?: DeviceId,
token?: CapabilityToken,
timeoutMs?: number,
) => Promise<ManageOutcome>;
close: () => Promise<void>;
}
Expand Down Expand Up @@ -569,6 +570,7 @@ function createSessionCore(
scope: Readonly<CapabilityScope>,
targetDevice?: DeviceId,
token?: CapabilityToken,
timeoutMs?: number,
): Promise<ManageOutcome> {
if (connection === null || state.status !== "connected") {
throw new Error("not connected");
Expand All @@ -577,13 +579,26 @@ function createSessionCore(
await ensureRelayPairing(targetDevice);
}
const frame = buildManageRequest(command, scope, token);
const requestId = frame["request-id"];
const outcome = new Promise<ManageOutcome>((resolve, reject) => {
pendingManageRequests.set(frame["request-id"], { resolve, reject });
pendingManageRequests.set(requestId, { resolve, reject });
});
frameLog.push({ direction: "sent", frame });
await transmit(frame, targetDevice !== undefined);
emit();
return outcome;
if (timeoutMs === undefined) {
return outcome;
}
return Promise.race([
outcome,
new Promise<ManageOutcome>((resolve) => {
setTimeout(() => {
if (pendingManageRequests.delete(requestId)) {
resolve({ result: "error", code: "timeout" });
}
}, timeoutMs);
}),
]);
},
async sendRevocationAnnounce(
entries: readonly RevocationEntry[],
Expand Down
53 changes: 53 additions & 0 deletions ts/packages/core/test/mesh-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ describe("reconnect policy", () => {
const TEST_TOKEN_SIGNATURE_BYTE = 3;
const TEST_INCOMING_REQUEST_ID = 7;
const OVERRIDE_TOKEN_BYTE = 9;
const MANAGE_REQUEST_TIMEOUT_MS = 5000;

describe("capability tokens and manage-request plumbing", () => {
const testCommand: ManageCommand = {
Expand Down Expand Up @@ -612,6 +613,58 @@ describe("capability tokens and manage-request plumbing", () => {
);
});

it("resolves with a timeout outcome, not a hang, when no response arrives within timeoutMs", async () => {
vi.useFakeTimers();
try {
const { transport } = fakeTransport();
const session = createMeshSession(transport, testIdentity, testClock);
await session.connect("ws://node", ["core/management"]);
const pending = session.sendManageRequest(
testCommand,
testScope,
undefined,
undefined,
MANAGE_REQUEST_TIMEOUT_MS,
);
await vi.advanceTimersByTimeAsync(MANAGE_REQUEST_TIMEOUT_MS);
await expect(pending).resolves.toEqual({
result: "error",
code: "timeout",
});
await session.close();
} finally {
vi.useRealTimers();
}
});

it("does not time out a request whose response arrives before timeoutMs elapses", async () => {
vi.useFakeTimers();
try {
const { transport, connection } = fakeTransport();
const session = createMeshSession(transport, testIdentity, testClock);
await session.connect("ws://node", ["core/management"]);
const pending = session.sendManageRequest(
testCommand,
testScope,
undefined,
undefined,
MANAGE_REQUEST_TIMEOUT_MS,
);
await vi.advanceTimersByTimeAsync(0);
const sentRequest = connection.sent.at(-1) as ManageRequestFrame;
connection.push({
type: "manage-response",
"request-id": sentRequest["request-id"],
outcome: { result: "ok" },
} satisfies ManageResponseFrame);
await expect(pending).resolves.toEqual({ result: "ok" });
await vi.advanceTimersByTimeAsync(MANAGE_REQUEST_TIMEOUT_MS);
await session.close();
} finally {
vi.useRealTimers();
}
});

it("surfaces an incoming manage-request on incomingManageRequests, and sends the response frame from respond()", async () => {
const { transport, connection } = fakeTransport();
const session = createMeshSession(transport, testIdentity, testClock);
Expand Down