@privateclaw/privateclaw-relay 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/README.md +20 -0
- package/dist/admin-api.d.ts +11 -0
- package/dist/admin-api.js +111 -0
- package/dist/admin-api.js.map +1 -0
- package/dist/admin-metrics-store.d.ts +245 -0
- package/dist/admin-metrics-store.js +952 -0
- package/dist/admin-metrics-store.js.map +1 -0
- package/dist/admin-web/admin.css +379 -0
- package/dist/admin-web/admin.js +366 -0
- package/dist/admin-web/index.html +145 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/provider-setup.js +0 -12
- package/dist/provider-setup.js.map +1 -1
- package/dist/relay-server.d.ts +2 -0
- package/dist/relay-server.js +206 -12
- package/dist/relay-server.js.map +1 -1
- package/dist/relay-web.d.ts +2 -0
- package/dist/relay-web.js +35 -6
- package/dist/relay-web.js.map +1 -1
- package/package.json +1 -1
package/dist/relay-server.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RelayAdminMetricsStore } from "./admin-metrics-store.js";
|
|
1
2
|
import type { RelayServerConfig } from "./config.js";
|
|
2
3
|
import { type EncryptedFrameCache } from "./frame-cache.js";
|
|
3
4
|
import { type RelayPushRegistrationStore } from "./push-registration-store.js";
|
|
@@ -33,6 +34,7 @@ export interface RelayServerDependencies {
|
|
|
33
34
|
sessionStore?: RelaySessionStore;
|
|
34
35
|
pushRegistrationStore?: RelayPushRegistrationStore;
|
|
35
36
|
pushNotifier?: RelayPushNotifier;
|
|
37
|
+
adminMetricsStore?: RelayAdminMetricsStore;
|
|
36
38
|
cluster?: RelayClusterClient;
|
|
37
39
|
clusterFactory?: (callbacks: RelayClusterCallbacks) => RelayClusterClient;
|
|
38
40
|
now?: () => number;
|
package/dist/relay-server.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createServer, } from "node:http";
|
|
3
3
|
import { WebSocket, WebSocketServer } from "ws";
|
|
4
|
+
import { handleRelayAdminRequest } from "./admin-api.js";
|
|
5
|
+
import { createRelayAdminMetricsStore, } from "./admin-metrics-store.js";
|
|
4
6
|
import { createEncryptedFrameCache, } from "./frame-cache.js";
|
|
5
7
|
import { createRelayPushRegistrationStore, } from "./push-registration-store.js";
|
|
6
8
|
import { createRelayPushNotifier, } from "./push-notifier.js";
|
|
7
9
|
import { createRedisRelayClusterClient, RelayClaimConflictError, } from "./relay-cluster.js";
|
|
8
10
|
import { createRelaySessionStore, } from "./session-store.js";
|
|
9
|
-
import { serveRelayWebRequest } from "./relay-web.js";
|
|
11
|
+
import { resolveRelayAdminWebRootDir, serveRelayWebRequest, } from "./relay-web.js";
|
|
10
12
|
const HEARTBEAT_INTERVAL_MS = 15_000;
|
|
11
13
|
const PUSH_WAKE_COOLDOWN_MS = 5_000;
|
|
12
14
|
const DEFAULT_PROTOCOL_MESSAGE_BYTES = 24 * 1024 * 1024;
|
|
@@ -65,6 +67,14 @@ class FixedWindowRateLimiter {
|
|
|
65
67
|
now() {
|
|
66
68
|
return this.params.now?.() ?? Date.now();
|
|
67
69
|
}
|
|
70
|
+
async recordAdminMetric(action, label) {
|
|
71
|
+
try {
|
|
72
|
+
await action();
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.error(`[privateclaw-relay] failed to record ${label}`, error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
68
78
|
allow(key) {
|
|
69
79
|
const now = this.now();
|
|
70
80
|
const existing = this.entries.get(key);
|
|
@@ -194,6 +204,14 @@ class SessionHub {
|
|
|
194
204
|
now() {
|
|
195
205
|
return this.params.now?.() ?? Date.now();
|
|
196
206
|
}
|
|
207
|
+
async recordAdminMetric(action, label) {
|
|
208
|
+
try {
|
|
209
|
+
await action();
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
console.error(`[privateclaw-relay] failed to record ${label}`, error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
197
215
|
wakeKey(sessionId, appId) {
|
|
198
216
|
return `${sessionId}:${appId}`;
|
|
199
217
|
}
|
|
@@ -255,7 +273,7 @@ class SessionHub {
|
|
|
255
273
|
await this.cluster.subscribeApp(binding.sessionId, binding.appId);
|
|
256
274
|
}
|
|
257
275
|
}
|
|
258
|
-
async forgetLocalAppBinding(appSocket, binding) {
|
|
276
|
+
async forgetLocalAppBinding(appSocket, binding, reason) {
|
|
259
277
|
const appSockets = this.sessionApps.get(binding.sessionId);
|
|
260
278
|
if (!appSockets || appSockets.get(binding.appId) !== appSocket) {
|
|
261
279
|
this.appSessions.delete(appSocket);
|
|
@@ -270,6 +288,9 @@ class SessionHub {
|
|
|
270
288
|
await this.cluster.unsubscribeApp(binding.sessionId, binding.appId);
|
|
271
289
|
await this.cluster.releaseApp(binding);
|
|
272
290
|
}
|
|
291
|
+
if (reason) {
|
|
292
|
+
await this.recordAdminMetric(() => this.params.adminMetricsStore.recordAppDetached(binding.sessionId, binding.appId, this.now(), reason), "app detach metrics");
|
|
293
|
+
}
|
|
273
294
|
return true;
|
|
274
295
|
}
|
|
275
296
|
async attachProvider(providerId, providerSocket) {
|
|
@@ -309,6 +330,7 @@ class SessionHub {
|
|
|
309
330
|
};
|
|
310
331
|
await this.params.sessionStore.saveSession(session);
|
|
311
332
|
await this.rememberLocalProviderSession(providerId, session.sessionId);
|
|
333
|
+
await this.recordAdminMetric(() => this.params.adminMetricsStore.recordSessionCreated(session, this.now()), "session creation metrics");
|
|
312
334
|
return session;
|
|
313
335
|
}
|
|
314
336
|
async requireSession(sessionId) {
|
|
@@ -357,6 +379,7 @@ class SessionHub {
|
|
|
357
379
|
};
|
|
358
380
|
await this.params.sessionStore.saveSession(renewedSession);
|
|
359
381
|
await this.params.pushRegistrationStore.touchSession(sessionId, renewedSession.expiresAt);
|
|
382
|
+
await this.recordAdminMetric(() => this.params.adminMetricsStore.recordSessionRenewed(renewedSession, this.now()), "session renewal metrics");
|
|
360
383
|
return renewedSession;
|
|
361
384
|
}
|
|
362
385
|
async registerAppPushToken(sessionId, appId, token) {
|
|
@@ -479,6 +502,7 @@ class SessionHub {
|
|
|
479
502
|
}
|
|
480
503
|
}
|
|
481
504
|
await this.rememberLocalAppBinding(binding, appSocket);
|
|
505
|
+
await this.recordAdminMetric(() => this.params.adminMetricsStore.recordAppAttached(session, normalizedAppId, this.now()), "app attach metrics");
|
|
482
506
|
return session;
|
|
483
507
|
}
|
|
484
508
|
deliverLocalToApp(sessionId, envelope, targetAppId) {
|
|
@@ -583,7 +607,7 @@ class SessionHub {
|
|
|
583
607
|
if (!binding || binding.appId !== appId) {
|
|
584
608
|
continue;
|
|
585
609
|
}
|
|
586
|
-
await this.forgetLocalAppBinding(appSocket, binding);
|
|
610
|
+
await this.forgetLocalAppBinding(appSocket, binding, reason);
|
|
587
611
|
sendJson(appSocket, {
|
|
588
612
|
type: "relay:session_closed",
|
|
589
613
|
sessionId,
|
|
@@ -611,6 +635,7 @@ class SessionHub {
|
|
|
611
635
|
await this.params.sessionStore.deleteSession(sessionId);
|
|
612
636
|
await this.params.pushRegistrationStore.clearSession(sessionId);
|
|
613
637
|
this.clearWakeState(sessionId);
|
|
638
|
+
await this.recordAdminMetric(() => this.params.adminMetricsStore.recordSessionClosed(sessionId, reason, this.now()), "session close metrics");
|
|
614
639
|
if (!this.hasLocalSession(sessionId) && !this.cluster) {
|
|
615
640
|
await this.params.frameCache.clear(sessionId);
|
|
616
641
|
return;
|
|
@@ -661,7 +686,7 @@ class SessionHub {
|
|
|
661
686
|
if (!binding) {
|
|
662
687
|
return;
|
|
663
688
|
}
|
|
664
|
-
await this.forgetLocalAppBinding(appSocket, binding);
|
|
689
|
+
await this.forgetLocalAppBinding(appSocket, binding, "app_disconnected");
|
|
665
690
|
}
|
|
666
691
|
async closeLocalApp(sessionId, appId, reason) {
|
|
667
692
|
const appSocket = this.sessionApps.get(sessionId)?.get(appId);
|
|
@@ -672,7 +697,7 @@ class SessionHub {
|
|
|
672
697
|
if (!binding) {
|
|
673
698
|
return;
|
|
674
699
|
}
|
|
675
|
-
await this.forgetLocalAppBinding(appSocket, binding);
|
|
700
|
+
await this.forgetLocalAppBinding(appSocket, binding, reason);
|
|
676
701
|
sendJson(appSocket, {
|
|
677
702
|
type: "relay:session_closed",
|
|
678
703
|
sessionId,
|
|
@@ -734,6 +759,36 @@ class SessionHub {
|
|
|
734
759
|
appBindings,
|
|
735
760
|
});
|
|
736
761
|
}
|
|
762
|
+
getAdminLocalSnapshot() {
|
|
763
|
+
const providerIds = [...this.providerSockets.entries()]
|
|
764
|
+
.filter(([, socket]) => socket.readyState === WebSocket.OPEN)
|
|
765
|
+
.map(([providerId]) => providerId)
|
|
766
|
+
.sort();
|
|
767
|
+
const participantBindings = [];
|
|
768
|
+
const sessionIds = new Set(this.sessionProviders.keys());
|
|
769
|
+
for (const [appSocket, binding] of this.appSessions.entries()) {
|
|
770
|
+
const currentSocket = this.sessionApps.get(binding.sessionId)?.get(binding.appId);
|
|
771
|
+
if (currentSocket !== appSocket || appSocket.readyState !== WebSocket.OPEN) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
participantBindings.push({
|
|
775
|
+
sessionId: binding.sessionId,
|
|
776
|
+
appId: binding.appId,
|
|
777
|
+
});
|
|
778
|
+
sessionIds.add(binding.sessionId);
|
|
779
|
+
}
|
|
780
|
+
participantBindings.sort((left, right) => left.sessionId.localeCompare(right.sessionId) ||
|
|
781
|
+
left.appId.localeCompare(right.appId));
|
|
782
|
+
return {
|
|
783
|
+
activeProviders: providerIds.length,
|
|
784
|
+
activeApps: participantBindings.length,
|
|
785
|
+
localSessions: sessionIds.size,
|
|
786
|
+
providerIds,
|
|
787
|
+
sessionIds: [...sessionIds].sort(),
|
|
788
|
+
participantBindings,
|
|
789
|
+
memoryUsage: process.memoryUsage(),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
737
792
|
async closeAll(reason) {
|
|
738
793
|
const sessionIds = new Set([
|
|
739
794
|
...this.sessionProviders.keys(),
|
|
@@ -773,12 +828,21 @@ export function createRelayServer(config, deps = {}) {
|
|
|
773
828
|
fcmClientEmail: config.fcmClientEmail,
|
|
774
829
|
fcmPrivateKey: config.fcmPrivateKey,
|
|
775
830
|
});
|
|
831
|
+
const ownsAdminMetricsStore = !deps.adminMetricsStore;
|
|
832
|
+
const adminMetricsStore = deps.adminMetricsStore ??
|
|
833
|
+
createRelayAdminMetricsStore({
|
|
834
|
+
...(config.redisUrl ? { redisUrl: config.redisUrl } : {}),
|
|
835
|
+
});
|
|
836
|
+
const relayStartedAt = Date.now();
|
|
837
|
+
const adminWebRootDir = config.adminToken &&
|
|
838
|
+
(config.adminWebRootDir ?? resolveRelayAdminWebRootDir());
|
|
776
839
|
const sessionHub = new SessionHub({
|
|
777
840
|
defaultTtlMs: config.sessionTtlMs,
|
|
778
841
|
frameCache,
|
|
779
842
|
sessionStore,
|
|
780
843
|
pushRegistrationStore,
|
|
781
844
|
pushNotifier,
|
|
845
|
+
adminMetricsStore,
|
|
782
846
|
...(deps.now ? { now: deps.now } : {}),
|
|
783
847
|
});
|
|
784
848
|
const appMessageRateLimiter = new FixedWindowRateLimiter({
|
|
@@ -826,6 +890,22 @@ export function createRelayServer(config, deps = {}) {
|
|
|
826
890
|
let startedPort = config.port;
|
|
827
891
|
let startedUrl = "";
|
|
828
892
|
let started = false;
|
|
893
|
+
async function recordAdminMetric(action, label) {
|
|
894
|
+
try {
|
|
895
|
+
await action();
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
console.error(`[privateclaw-relay] failed to record ${label}`, error);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
async function recordInstanceHeartbeat() {
|
|
902
|
+
await recordAdminMetric(() => adminMetricsStore.recordInstanceHeartbeat({
|
|
903
|
+
instanceId: clusterNodeId,
|
|
904
|
+
startedAt: relayStartedAt,
|
|
905
|
+
recordedAt: deps.now?.() ?? Date.now(),
|
|
906
|
+
snapshot: sessionHub.getAdminLocalSnapshot(),
|
|
907
|
+
}), "relay instance heartbeat");
|
|
908
|
+
}
|
|
829
909
|
async function handleHttpRequest(request, response) {
|
|
830
910
|
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? `${config.host}:${startedPort}`}`);
|
|
831
911
|
try {
|
|
@@ -836,6 +916,29 @@ export function createRelayServer(config, deps = {}) {
|
|
|
836
916
|
response.end(JSON.stringify({ ok: true, sessions, instanceId: clusterNodeId }));
|
|
837
917
|
return;
|
|
838
918
|
}
|
|
919
|
+
const handledAdminApi = await handleRelayAdminRequest({
|
|
920
|
+
request,
|
|
921
|
+
response,
|
|
922
|
+
url,
|
|
923
|
+
metricsStore: adminMetricsStore,
|
|
924
|
+
...(config.adminToken ? { adminToken: config.adminToken } : {}),
|
|
925
|
+
...(deps.now ? { now: deps.now } : {}),
|
|
926
|
+
});
|
|
927
|
+
if (handledAdminApi) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (adminWebRootDir && url.pathname.startsWith("/admin")) {
|
|
931
|
+
const handledAdminWeb = await serveRelayWebRequest({
|
|
932
|
+
request,
|
|
933
|
+
response,
|
|
934
|
+
url,
|
|
935
|
+
webRootDir: adminWebRootDir,
|
|
936
|
+
mountPath: "/admin",
|
|
937
|
+
});
|
|
938
|
+
if (handledAdminWeb) {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
839
942
|
if (config.webRootDir) {
|
|
840
943
|
const handled = await serveRelayWebRequest({
|
|
841
944
|
request,
|
|
@@ -885,6 +988,7 @@ export function createRelayServer(config, deps = {}) {
|
|
|
885
988
|
void sessionHub.refreshClusterPresence().catch((error) => {
|
|
886
989
|
console.error("[privateclaw-relay] failed to refresh relay presence", error);
|
|
887
990
|
});
|
|
991
|
+
void recordInstanceHeartbeat();
|
|
888
992
|
}, HEARTBEAT_INTERVAL_MS);
|
|
889
993
|
function terminateSockets(server) {
|
|
890
994
|
for (const socket of server.clients) {
|
|
@@ -898,12 +1002,14 @@ export function createRelayServer(config, deps = {}) {
|
|
|
898
1002
|
}
|
|
899
1003
|
async function handleProviderMessage(socket, raw) {
|
|
900
1004
|
let requestId;
|
|
1005
|
+
let requestType = "unknown";
|
|
901
1006
|
try {
|
|
902
1007
|
assertMessageSizeWithinLimit(raw, maxMessageBytes, "Provider");
|
|
903
1008
|
const message = parseJson(raw);
|
|
904
1009
|
if (!isObject(message) || typeof message.type !== "string") {
|
|
905
1010
|
throw new RelayProtocolError("invalid_message", "Provider message is missing a type field.");
|
|
906
1011
|
}
|
|
1012
|
+
requestType = message.type;
|
|
907
1013
|
if ("requestId" in message &&
|
|
908
1014
|
typeof message.requestId === "string" &&
|
|
909
1015
|
message.requestId !== "") {
|
|
@@ -944,6 +1050,12 @@ export function createRelayServer(config, deps = {}) {
|
|
|
944
1050
|
sessionId: session.sessionId,
|
|
945
1051
|
expiresAt: new Date(session.expiresAt).toISOString(),
|
|
946
1052
|
});
|
|
1053
|
+
await recordInstanceHeartbeat();
|
|
1054
|
+
await recordAdminMetric(() => adminMetricsStore.recordRequest({
|
|
1055
|
+
actor: "provider",
|
|
1056
|
+
type: requestType,
|
|
1057
|
+
ok: true,
|
|
1058
|
+
}), "provider request metrics");
|
|
947
1059
|
return;
|
|
948
1060
|
}
|
|
949
1061
|
case "provider:renew_session": {
|
|
@@ -967,6 +1079,12 @@ export function createRelayServer(config, deps = {}) {
|
|
|
967
1079
|
sessionId: session.sessionId,
|
|
968
1080
|
expiresAt: new Date(session.expiresAt).toISOString(),
|
|
969
1081
|
});
|
|
1082
|
+
await recordInstanceHeartbeat();
|
|
1083
|
+
await recordAdminMetric(() => adminMetricsStore.recordRequest({
|
|
1084
|
+
actor: "provider",
|
|
1085
|
+
type: requestType,
|
|
1086
|
+
ok: true,
|
|
1087
|
+
}), "provider request metrics");
|
|
970
1088
|
return;
|
|
971
1089
|
}
|
|
972
1090
|
case "provider:frame": {
|
|
@@ -978,7 +1096,16 @@ export function createRelayServer(config, deps = {}) {
|
|
|
978
1096
|
typeof message.targetAppId !== "string") {
|
|
979
1097
|
throw new RelayProtocolError("invalid_target_app_id", "provider:frame targetAppId must be a string when provided.");
|
|
980
1098
|
}
|
|
981
|
-
|
|
1099
|
+
const sessionIdValue = message.sessionId;
|
|
1100
|
+
await sessionHub.forwardToApp(socket, sessionIdValue, message.envelope, message.targetAppId);
|
|
1101
|
+
await recordAdminMetric(async () => {
|
|
1102
|
+
await adminMetricsStore.recordProviderFrame(sessionIdValue, deps.now?.() ?? Date.now());
|
|
1103
|
+
await adminMetricsStore.recordRequest({
|
|
1104
|
+
actor: "provider",
|
|
1105
|
+
type: requestType,
|
|
1106
|
+
ok: true,
|
|
1107
|
+
});
|
|
1108
|
+
}, "provider frame metrics");
|
|
982
1109
|
return;
|
|
983
1110
|
}
|
|
984
1111
|
case "provider:close_session": {
|
|
@@ -988,6 +1115,12 @@ export function createRelayServer(config, deps = {}) {
|
|
|
988
1115
|
await sessionHub.closeSessionForProvider(socket, message.sessionId, typeof message.reason === "string"
|
|
989
1116
|
? message.reason
|
|
990
1117
|
: "provider_closed");
|
|
1118
|
+
await recordInstanceHeartbeat();
|
|
1119
|
+
await recordAdminMetric(() => adminMetricsStore.recordRequest({
|
|
1120
|
+
actor: "provider",
|
|
1121
|
+
type: requestType,
|
|
1122
|
+
ok: true,
|
|
1123
|
+
}), "provider request metrics");
|
|
991
1124
|
return;
|
|
992
1125
|
}
|
|
993
1126
|
case "provider:close_app": {
|
|
@@ -999,6 +1132,12 @@ export function createRelayServer(config, deps = {}) {
|
|
|
999
1132
|
await sessionHub.closeAppForProvider(socket, message.sessionId, message.appId, typeof message.reason === "string"
|
|
1000
1133
|
? message.reason
|
|
1001
1134
|
: "provider_closed_app");
|
|
1135
|
+
await recordInstanceHeartbeat();
|
|
1136
|
+
await recordAdminMetric(() => adminMetricsStore.recordRequest({
|
|
1137
|
+
actor: "provider",
|
|
1138
|
+
type: requestType,
|
|
1139
|
+
ok: true,
|
|
1140
|
+
}), "provider request metrics");
|
|
1002
1141
|
return;
|
|
1003
1142
|
}
|
|
1004
1143
|
default:
|
|
@@ -1007,6 +1146,14 @@ export function createRelayServer(config, deps = {}) {
|
|
|
1007
1146
|
}
|
|
1008
1147
|
catch (error) {
|
|
1009
1148
|
const relayError = toRelayProtocolError(error);
|
|
1149
|
+
await recordAdminMetric(() => adminMetricsStore.recordRequest({
|
|
1150
|
+
actor: "provider",
|
|
1151
|
+
type: requestType === "unknown" && relayError.code === "invalid_json"
|
|
1152
|
+
? "invalid_json"
|
|
1153
|
+
: requestType,
|
|
1154
|
+
ok: false,
|
|
1155
|
+
errorCode: relayError.code,
|
|
1156
|
+
}), "provider error metrics");
|
|
1010
1157
|
sendJson(socket, {
|
|
1011
1158
|
type: "relay:error",
|
|
1012
1159
|
code: relayError.code,
|
|
@@ -1016,6 +1163,7 @@ export function createRelayServer(config, deps = {}) {
|
|
|
1016
1163
|
}
|
|
1017
1164
|
}
|
|
1018
1165
|
async function handleAppMessage(socket, sessionId, appId, raw) {
|
|
1166
|
+
let requestType = "unknown";
|
|
1019
1167
|
try {
|
|
1020
1168
|
assertMessageSizeWithinLimit(raw, maxMessageBytes, "App");
|
|
1021
1169
|
if (!appMessageRateLimiter.allow(`${sessionId}:${appId}`)) {
|
|
@@ -1025,21 +1173,40 @@ export function createRelayServer(config, deps = {}) {
|
|
|
1025
1173
|
if (!isObject(message) || typeof message.type !== "string") {
|
|
1026
1174
|
throw new RelayProtocolError("invalid_message", "App message is missing a type field.");
|
|
1027
1175
|
}
|
|
1176
|
+
requestType = message.type;
|
|
1028
1177
|
switch (message.type) {
|
|
1029
1178
|
case "app:frame":
|
|
1030
1179
|
if (!isEncryptedEnvelope(message.envelope)) {
|
|
1031
1180
|
throw new RelayProtocolError("invalid_frame", "app:frame must include a valid encrypted envelope.");
|
|
1032
1181
|
}
|
|
1033
1182
|
await sessionHub.forwardToProvider(sessionId, message.envelope);
|
|
1183
|
+
await recordAdminMetric(async () => {
|
|
1184
|
+
await adminMetricsStore.recordAppFrame(sessionId, appId, deps.now?.() ?? Date.now());
|
|
1185
|
+
await adminMetricsStore.recordRequest({
|
|
1186
|
+
actor: "app",
|
|
1187
|
+
type: requestType,
|
|
1188
|
+
ok: true,
|
|
1189
|
+
});
|
|
1190
|
+
}, "app frame metrics");
|
|
1034
1191
|
return;
|
|
1035
1192
|
case "app:register_push":
|
|
1036
1193
|
if (typeof message.token !== "string") {
|
|
1037
1194
|
throw new RelayProtocolError("invalid_push_token", "app:register_push requires a string token.");
|
|
1038
1195
|
}
|
|
1039
1196
|
await sessionHub.registerAppPushToken(sessionId, appId, message.token);
|
|
1197
|
+
await recordAdminMetric(() => adminMetricsStore.recordRequest({
|
|
1198
|
+
actor: "app",
|
|
1199
|
+
type: requestType,
|
|
1200
|
+
ok: true,
|
|
1201
|
+
}), "app request metrics");
|
|
1040
1202
|
return;
|
|
1041
1203
|
case "app:unregister_push":
|
|
1042
1204
|
await sessionHub.unregisterAppPushToken(sessionId, appId);
|
|
1205
|
+
await recordAdminMetric(() => adminMetricsStore.recordRequest({
|
|
1206
|
+
actor: "app",
|
|
1207
|
+
type: requestType,
|
|
1208
|
+
ok: true,
|
|
1209
|
+
}), "app request metrics");
|
|
1043
1210
|
return;
|
|
1044
1211
|
default:
|
|
1045
1212
|
throw new RelayProtocolError("unsupported_message", `Unsupported app message type: ${String(message.type)}`);
|
|
@@ -1047,6 +1214,14 @@ export function createRelayServer(config, deps = {}) {
|
|
|
1047
1214
|
}
|
|
1048
1215
|
catch (error) {
|
|
1049
1216
|
const relayError = toRelayProtocolError(error);
|
|
1217
|
+
await recordAdminMetric(() => adminMetricsStore.recordRequest({
|
|
1218
|
+
actor: "app",
|
|
1219
|
+
type: requestType === "unknown" && relayError.code === "invalid_json"
|
|
1220
|
+
? "invalid_json"
|
|
1221
|
+
: requestType,
|
|
1222
|
+
ok: false,
|
|
1223
|
+
errorCode: relayError.code,
|
|
1224
|
+
}), "app error metrics");
|
|
1050
1225
|
sendJson(socket, {
|
|
1051
1226
|
type: "relay:error",
|
|
1052
1227
|
code: relayError.code,
|
|
@@ -1063,6 +1238,7 @@ export function createRelayServer(config, deps = {}) {
|
|
|
1063
1238
|
void (async () => {
|
|
1064
1239
|
try {
|
|
1065
1240
|
await sessionHub.attachProvider(providerId, socket);
|
|
1241
|
+
await recordInstanceHeartbeat();
|
|
1066
1242
|
}
|
|
1067
1243
|
catch (error) {
|
|
1068
1244
|
const relayError = toRelayProtocolError(error);
|
|
@@ -1082,9 +1258,15 @@ export function createRelayServer(config, deps = {}) {
|
|
|
1082
1258
|
void handleProviderMessage(socket, data.toString());
|
|
1083
1259
|
});
|
|
1084
1260
|
socket.on("close", () => {
|
|
1085
|
-
void
|
|
1086
|
-
|
|
1087
|
-
|
|
1261
|
+
void (async () => {
|
|
1262
|
+
try {
|
|
1263
|
+
await sessionHub.detachProvider(socket);
|
|
1264
|
+
}
|
|
1265
|
+
catch (error) {
|
|
1266
|
+
console.error("[privateclaw-relay] failed to detach provider socket", error);
|
|
1267
|
+
}
|
|
1268
|
+
await recordInstanceHeartbeat();
|
|
1269
|
+
})();
|
|
1088
1270
|
});
|
|
1089
1271
|
})();
|
|
1090
1272
|
});
|
|
@@ -1107,6 +1289,7 @@ export function createRelayServer(config, deps = {}) {
|
|
|
1107
1289
|
let session;
|
|
1108
1290
|
try {
|
|
1109
1291
|
session = await sessionHub.attachApp(sessionId, appId, socket);
|
|
1292
|
+
await recordInstanceHeartbeat();
|
|
1110
1293
|
}
|
|
1111
1294
|
catch (error) {
|
|
1112
1295
|
const relayError = toRelayProtocolError(error);
|
|
@@ -1131,9 +1314,15 @@ export function createRelayServer(config, deps = {}) {
|
|
|
1131
1314
|
void handleAppMessage(socket, sessionId, appId, data.toString());
|
|
1132
1315
|
});
|
|
1133
1316
|
socket.on("close", () => {
|
|
1134
|
-
void
|
|
1135
|
-
|
|
1136
|
-
|
|
1317
|
+
void (async () => {
|
|
1318
|
+
try {
|
|
1319
|
+
await sessionHub.detachApp(socket);
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
console.error("[privateclaw-relay] failed to detach app socket", error);
|
|
1323
|
+
}
|
|
1324
|
+
await recordInstanceHeartbeat();
|
|
1325
|
+
})();
|
|
1137
1326
|
});
|
|
1138
1327
|
})();
|
|
1139
1328
|
});
|
|
@@ -1170,6 +1359,9 @@ export function createRelayServer(config, deps = {}) {
|
|
|
1170
1359
|
if (ownsFrameCache) {
|
|
1171
1360
|
closes.push(frameCache.close());
|
|
1172
1361
|
}
|
|
1362
|
+
if (ownsAdminMetricsStore) {
|
|
1363
|
+
closes.push(adminMetricsStore.close());
|
|
1364
|
+
}
|
|
1173
1365
|
await Promise.all(closes);
|
|
1174
1366
|
}
|
|
1175
1367
|
return {
|
|
@@ -1197,6 +1389,7 @@ export function createRelayServer(config, deps = {}) {
|
|
|
1197
1389
|
startedPort = address.port;
|
|
1198
1390
|
startedUrl = `http://${config.host}:${startedPort}`;
|
|
1199
1391
|
started = true;
|
|
1392
|
+
await recordInstanceHeartbeat();
|
|
1200
1393
|
return { port: startedPort, url: startedUrl };
|
|
1201
1394
|
},
|
|
1202
1395
|
async stop() {
|
|
@@ -1224,6 +1417,7 @@ export function createRelayServer(config, deps = {}) {
|
|
|
1224
1417
|
resolve();
|
|
1225
1418
|
});
|
|
1226
1419
|
});
|
|
1420
|
+
await recordAdminMetric(() => adminMetricsStore.unregisterInstance(clusterNodeId), "relay instance shutdown");
|
|
1227
1421
|
await closeOwnedResources();
|
|
1228
1422
|
started = false;
|
|
1229
1423
|
startedUrl = "";
|