Handle heartbeat builtin messages

This commit is contained in:
nav
2026-04-08 22:35:02 +00:00
parent 83f6195c1f
commit ba007ebd59

View File

@@ -9,6 +9,7 @@ import {
buildAuthFailed, buildAuthFailed,
buildAuthSuccess, buildAuthSuccess,
buildError, buildError,
buildHeartbeatAck,
buildHelloAck, buildHelloAck,
buildPairFailed, buildPairFailed,
buildPairRequest, buildPairRequest,
@@ -20,7 +21,8 @@ import {
isBuiltinMessage, isBuiltinMessage,
isTimestampFresh, isTimestampFresh,
isValidAuthNonce, isValidAuthNonce,
type AuthRequestPayload type AuthRequestPayload,
type HeartbeatPayload
} from "../../../Yonexus.Protocol/src/index.js"; } from "../../../Yonexus.Protocol/src/index.js";
import type { YonexusServerConfig } from "./config.js"; import type { YonexusServerConfig } from "./config.js";
import { import {
@@ -156,6 +158,14 @@ export class YonexusServerRuntime {
connection, connection,
envelope as BuiltinEnvelope<"auth_request", AuthRequestPayload> envelope as BuiltinEnvelope<"auth_request", AuthRequestPayload>
); );
return;
}
if (envelope.type === "heartbeat") {
await this.handleHeartbeat(
connection,
envelope as BuiltinEnvelope<"heartbeat", HeartbeatPayload>
);
} }
} }
@@ -590,6 +600,75 @@ export class YonexusServerRuntime {
} }
} }
private async handleHeartbeat(
connection: ClientConnection,
envelope: BuiltinEnvelope<"heartbeat", HeartbeatPayload>
): Promise<void> {
const payload = envelope.payload;
if (!payload) {
this.options.transport.sendToConnection(
connection,
encodeBuiltin(
buildError(
{ code: "MALFORMED_MESSAGE", message: "heartbeat payload is required" },
{ requestId: envelope.requestId, timestamp: this.now() }
)
)
);
return;
}
const identifier = payload.identifier?.trim();
if (!identifier || !this.options.config.followerIdentifiers.includes(identifier)) {
this.options.transport.sendToConnection(
connection,
encodeBuiltin(
buildError(
{ code: "IDENTIFIER_NOT_ALLOWED", message: "identifier is not allowed" },
{ requestId: envelope.requestId, timestamp: this.now() }
)
)
);
return;
}
const session = this.registry.sessions.get(identifier);
if (!session || !session.isAuthenticated) {
this.options.transport.sendToConnection(
connection,
encodeBuiltin(
buildError(
{ code: "AUTH_FAILED", message: "heartbeat requires authentication" },
{ requestId: envelope.requestId, timestamp: this.now() }
)
)
);
return;
}
const record = this.ensureClientRecord(identifier);
const now = this.now();
record.lastHeartbeatAt = now;
record.status = "online";
record.updatedAt = now;
session.lastActivityAt = now;
this.options.transport.sendToConnection(
{ ...connection, identifier },
encodeBuiltin(
buildHeartbeatAck(
{
identifier,
status: record.status
},
{ requestId: envelope.requestId, timestamp: now }
)
)
);
await this.persist();
}
private async triggerRePairRequired( private async triggerRePairRequired(
connection: ClientConnection, connection: ClientConnection,
record: ClientRecord, record: ClientRecord,