@fatagnus/remote-cmd-relay-convex 1.0.0
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/package.json +41 -0
- package/src/README.md +413 -0
- package/src/_generated/api.ts +62 -0
- package/src/_generated/component.ts +565 -0
- package/src/_generated/dataModel.ts +60 -0
- package/src/_generated/server.ts +161 -0
- package/src/assignments.test.ts +233 -0
- package/src/assignments.ts +222 -0
- package/src/commands.test.ts +331 -0
- package/src/commands.ts +286 -0
- package/src/component.ts +11 -0
- package/src/configPush.ts +96 -0
- package/src/convex.config.ts +5 -0
- package/src/credentials.ts +112 -0
- package/src/public.test.ts +576 -0
- package/src/public.ts +436 -0
- package/src/schema.ts +180 -0
- package/src/status.test.ts +308 -0
- package/src/status.ts +172 -0
- package/src/test.helpers.ts +399 -0
- package/src/test.setup.ts +256 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { createRelayTestConvex, RelayTestConvex } from "./test.setup";
|
|
3
|
+
import { createMockRelayStatus } from "./test.helpers";
|
|
4
|
+
import { api } from "./_generated/api";
|
|
5
|
+
|
|
6
|
+
describe("status", () => {
|
|
7
|
+
let t: RelayTestConvex;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.useFakeTimers();
|
|
11
|
+
t = createRelayTestConvex();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await t.finishAllScheduledFunctions(vi.runAllTimers);
|
|
16
|
+
vi.useRealTimers();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("reportStatus", () => {
|
|
20
|
+
it("creates new relay status record", async () => {
|
|
21
|
+
const result = await t.mutation(api.status.reportStatus, {
|
|
22
|
+
relayId: "new-relay",
|
|
23
|
+
capabilities: ["ssh", "local_cmd"],
|
|
24
|
+
version: "1.0.0",
|
|
25
|
+
hostname: "relay-host",
|
|
26
|
+
platform: "linux",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(result.success).toBe(true);
|
|
30
|
+
expect(result.statusId).toBeDefined();
|
|
31
|
+
|
|
32
|
+
const status = await t.query(api.status.getByRelayId, {
|
|
33
|
+
relayId: "new-relay",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(status).not.toBeNull();
|
|
37
|
+
expect(status?.capabilities).toEqual(["ssh", "local_cmd"]);
|
|
38
|
+
expect(status?.version).toBe("1.0.0");
|
|
39
|
+
expect(status?.hostname).toBe("relay-host");
|
|
40
|
+
expect(status?.platform).toBe("linux");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("updates existing relay status record", async () => {
|
|
44
|
+
// Create initial status
|
|
45
|
+
await t.mutation(api.status.reportStatus, {
|
|
46
|
+
relayId: "update-relay",
|
|
47
|
+
capabilities: ["local_cmd"],
|
|
48
|
+
version: "1.0.0",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Update status
|
|
52
|
+
const result = await t.mutation(api.status.reportStatus, {
|
|
53
|
+
relayId: "update-relay",
|
|
54
|
+
capabilities: ["ssh", "local_cmd", "perf_metrics"],
|
|
55
|
+
version: "2.0.0",
|
|
56
|
+
hostname: "new-hostname",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(result.success).toBe(true);
|
|
60
|
+
|
|
61
|
+
const status = await t.query(api.status.getByRelayId, {
|
|
62
|
+
relayId: "update-relay",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(status?.capabilities).toEqual(["ssh", "local_cmd", "perf_metrics"]);
|
|
66
|
+
expect(status?.version).toBe("2.0.0");
|
|
67
|
+
expect(status?.hostname).toBe("new-hostname");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("reports status with metrics", async () => {
|
|
71
|
+
const result = await t.mutation(api.status.reportStatus, {
|
|
72
|
+
relayId: "metrics-relay",
|
|
73
|
+
capabilities: ["perf_metrics"],
|
|
74
|
+
metrics: {
|
|
75
|
+
cpuPercent: 45.5,
|
|
76
|
+
memoryPercent: 60.2,
|
|
77
|
+
memoryUsedMb: 4096,
|
|
78
|
+
memoryTotalMb: 8192,
|
|
79
|
+
diskPercent: 75.0,
|
|
80
|
+
loadAvg1m: 1.5,
|
|
81
|
+
loadAvg5m: 1.2,
|
|
82
|
+
loadAvg15m: 0.9,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result.success).toBe(true);
|
|
87
|
+
|
|
88
|
+
const status = await t.query(api.status.getByRelayId, {
|
|
89
|
+
relayId: "metrics-relay",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(status?.metrics?.cpuPercent).toBe(45.5);
|
|
93
|
+
expect(status?.metrics?.memoryPercent).toBe(60.2);
|
|
94
|
+
expect(status?.metrics?.loadAvg1m).toBe(1.5);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("updates lastHeartbeatAt on status report", async () => {
|
|
98
|
+
await t.mutation(api.status.reportStatus, {
|
|
99
|
+
relayId: "heartbeat-relay",
|
|
100
|
+
capabilities: ["local_cmd"],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const status = await t.query(api.status.getByRelayId, {
|
|
104
|
+
relayId: "heartbeat-relay",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(status?.lastHeartbeatAt).toBeDefined();
|
|
108
|
+
expect(status?.lastHeartbeatAt).toBeGreaterThan(0);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("getByRelayId", () => {
|
|
113
|
+
it("returns status when found", async () => {
|
|
114
|
+
await createMockRelayStatus(t, {
|
|
115
|
+
relayId: "find-me-relay",
|
|
116
|
+
capabilities: ["ssh", "local_cmd"],
|
|
117
|
+
version: "1.0.0",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const status = await t.query(api.status.getByRelayId, {
|
|
121
|
+
relayId: "find-me-relay",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(status).not.toBeNull();
|
|
125
|
+
expect(status?.relayId).toBe("find-me-relay");
|
|
126
|
+
expect(status?.capabilities).toEqual(["ssh", "local_cmd"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("returns null when not found", async () => {
|
|
130
|
+
const status = await t.query(api.status.getByRelayId, {
|
|
131
|
+
relayId: "nonexistent-relay",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(status).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("listAll", () => {
|
|
139
|
+
it("lists all relay statuses", async () => {
|
|
140
|
+
await createMockRelayStatus(t, { relayId: "relay-1" });
|
|
141
|
+
await createMockRelayStatus(t, { relayId: "relay-2" });
|
|
142
|
+
await createMockRelayStatus(t, { relayId: "relay-3" });
|
|
143
|
+
|
|
144
|
+
const statuses = await t.query(api.status.listAll, {});
|
|
145
|
+
|
|
146
|
+
expect(statuses).toHaveLength(3);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("includes isOnline flag based on heartbeat", async () => {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
|
|
152
|
+
// Create online relay (recent heartbeat)
|
|
153
|
+
await createMockRelayStatus(t, {
|
|
154
|
+
relayId: "online-relay",
|
|
155
|
+
lastHeartbeatAt: now - 30000, // 30 seconds ago
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Create offline relay (old heartbeat)
|
|
159
|
+
await createMockRelayStatus(t, {
|
|
160
|
+
relayId: "offline-relay",
|
|
161
|
+
lastHeartbeatAt: now - 120000, // 2 minutes ago
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const statuses = await t.query(api.status.listAll, {});
|
|
165
|
+
|
|
166
|
+
const onlineRelay = statuses.find((s) => s.relayId === "online-relay");
|
|
167
|
+
const offlineRelay = statuses.find((s) => s.relayId === "offline-relay");
|
|
168
|
+
|
|
169
|
+
expect(onlineRelay?.isOnline).toBe(true);
|
|
170
|
+
expect(offlineRelay?.isOnline).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("returns empty array when no statuses exist", async () => {
|
|
174
|
+
const statuses = await t.query(api.status.listAll, {});
|
|
175
|
+
|
|
176
|
+
expect(statuses).toEqual([]);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("findByCapability", () => {
|
|
181
|
+
it("finds relays with specific capability", async () => {
|
|
182
|
+
await createMockRelayStatus(t, {
|
|
183
|
+
relayId: "ssh-relay-1",
|
|
184
|
+
capabilities: ["ssh", "local_cmd"],
|
|
185
|
+
});
|
|
186
|
+
await createMockRelayStatus(t, {
|
|
187
|
+
relayId: "ssh-relay-2",
|
|
188
|
+
capabilities: ["ssh"],
|
|
189
|
+
});
|
|
190
|
+
await createMockRelayStatus(t, {
|
|
191
|
+
relayId: "local-only-relay",
|
|
192
|
+
capabilities: ["local_cmd"],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const sshRelays = await t.query(api.status.findByCapability, {
|
|
196
|
+
capability: "ssh",
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(sshRelays).toHaveLength(2);
|
|
200
|
+
expect(sshRelays.map((r) => r.relayId)).toContain("ssh-relay-1");
|
|
201
|
+
expect(sshRelays.map((r) => r.relayId)).toContain("ssh-relay-2");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("finds relays with perf_metrics capability", async () => {
|
|
205
|
+
await createMockRelayStatus(t, {
|
|
206
|
+
relayId: "metrics-relay",
|
|
207
|
+
capabilities: ["local_cmd", "perf_metrics"],
|
|
208
|
+
});
|
|
209
|
+
await createMockRelayStatus(t, {
|
|
210
|
+
relayId: "no-metrics-relay",
|
|
211
|
+
capabilities: ["local_cmd"],
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const metricsRelays = await t.query(api.status.findByCapability, {
|
|
215
|
+
capability: "perf_metrics",
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(metricsRelays).toHaveLength(1);
|
|
219
|
+
expect(metricsRelays[0].relayId).toBe("metrics-relay");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("includes isOnline flag in results", async () => {
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
|
|
225
|
+
await createMockRelayStatus(t, {
|
|
226
|
+
relayId: "online-ssh-relay",
|
|
227
|
+
capabilities: ["ssh"],
|
|
228
|
+
lastHeartbeatAt: now - 30000,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
await createMockRelayStatus(t, {
|
|
232
|
+
relayId: "offline-ssh-relay",
|
|
233
|
+
capabilities: ["ssh"],
|
|
234
|
+
lastHeartbeatAt: now - 120000,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const relays = await t.query(api.status.findByCapability, {
|
|
238
|
+
capability: "ssh",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const online = relays.find((r) => r.relayId === "online-ssh-relay");
|
|
242
|
+
const offline = relays.find((r) => r.relayId === "offline-ssh-relay");
|
|
243
|
+
|
|
244
|
+
expect(online?.isOnline).toBe(true);
|
|
245
|
+
expect(offline?.isOnline).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("returns empty array when no relays have capability", async () => {
|
|
249
|
+
await createMockRelayStatus(t, {
|
|
250
|
+
relayId: "local-only",
|
|
251
|
+
capabilities: ["local_cmd"],
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const relays = await t.query(api.status.findByCapability, {
|
|
255
|
+
capability: "ssh",
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(relays).toEqual([]);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("status lifecycle", () => {
|
|
263
|
+
it("tracks relay status through multiple updates", async () => {
|
|
264
|
+
const relayId = "lifecycle-relay";
|
|
265
|
+
|
|
266
|
+
// Initial registration
|
|
267
|
+
await t.mutation(api.status.reportStatus, {
|
|
268
|
+
relayId,
|
|
269
|
+
capabilities: ["local_cmd"],
|
|
270
|
+
version: "1.0.0",
|
|
271
|
+
platform: "linux",
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
let status = await t.query(api.status.getByRelayId, { relayId });
|
|
275
|
+
expect(status?.version).toBe("1.0.0");
|
|
276
|
+
expect(status?.capabilities).toEqual(["local_cmd"]);
|
|
277
|
+
|
|
278
|
+
// Upgrade with new capabilities
|
|
279
|
+
await t.mutation(api.status.reportStatus, {
|
|
280
|
+
relayId,
|
|
281
|
+
capabilities: ["local_cmd", "ssh"],
|
|
282
|
+
version: "1.1.0",
|
|
283
|
+
platform: "linux",
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
status = await t.query(api.status.getByRelayId, { relayId });
|
|
287
|
+
expect(status?.version).toBe("1.1.0");
|
|
288
|
+
expect(status?.capabilities).toContain("ssh");
|
|
289
|
+
|
|
290
|
+
// Add metrics capability
|
|
291
|
+
await t.mutation(api.status.reportStatus, {
|
|
292
|
+
relayId,
|
|
293
|
+
capabilities: ["local_cmd", "ssh", "perf_metrics"],
|
|
294
|
+
version: "1.2.0",
|
|
295
|
+
platform: "linux",
|
|
296
|
+
metrics: {
|
|
297
|
+
cpuPercent: 25.0,
|
|
298
|
+
memoryPercent: 50.0,
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
status = await t.query(api.status.getByRelayId, { relayId });
|
|
303
|
+
expect(status?.version).toBe("1.2.0");
|
|
304
|
+
expect(status?.capabilities).toHaveLength(3);
|
|
305
|
+
expect(status?.metrics?.cpuPercent).toBe(25.0);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
package/src/status.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "./_generated/server";
|
|
3
|
+
import { capabilityValidator, metricsValidator } from "./schema";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Report relay status (capabilities, metrics, etc.)
|
|
7
|
+
* Called by relay on startup and with each heartbeat
|
|
8
|
+
*/
|
|
9
|
+
export const reportStatus = mutation({
|
|
10
|
+
args: {
|
|
11
|
+
relayId: v.string(),
|
|
12
|
+
capabilities: v.array(capabilityValidator),
|
|
13
|
+
metrics: v.optional(metricsValidator),
|
|
14
|
+
version: v.optional(v.string()),
|
|
15
|
+
hostname: v.optional(v.string()),
|
|
16
|
+
platform: v.optional(v.string()),
|
|
17
|
+
},
|
|
18
|
+
returns: v.object({
|
|
19
|
+
success: v.boolean(),
|
|
20
|
+
statusId: v.optional(v.string()),
|
|
21
|
+
}),
|
|
22
|
+
handler: async (ctx, args) => {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
|
|
25
|
+
// Check if status record exists for this relay
|
|
26
|
+
const existing = await ctx.db
|
|
27
|
+
.query("relayStatus")
|
|
28
|
+
.withIndex("by_relayId", (q) => q.eq("relayId", args.relayId))
|
|
29
|
+
.first();
|
|
30
|
+
|
|
31
|
+
if (existing) {
|
|
32
|
+
// Update existing record
|
|
33
|
+
await ctx.db.patch(existing._id, {
|
|
34
|
+
capabilities: args.capabilities,
|
|
35
|
+
metrics: args.metrics,
|
|
36
|
+
version: args.version,
|
|
37
|
+
hostname: args.hostname,
|
|
38
|
+
platform: args.platform,
|
|
39
|
+
lastHeartbeatAt: now,
|
|
40
|
+
updatedAt: now,
|
|
41
|
+
});
|
|
42
|
+
return { success: true, statusId: existing._id };
|
|
43
|
+
} else {
|
|
44
|
+
// Create new record
|
|
45
|
+
const id = await ctx.db.insert("relayStatus", {
|
|
46
|
+
relayId: args.relayId,
|
|
47
|
+
capabilities: args.capabilities,
|
|
48
|
+
metrics: args.metrics,
|
|
49
|
+
version: args.version,
|
|
50
|
+
hostname: args.hostname,
|
|
51
|
+
platform: args.platform,
|
|
52
|
+
lastHeartbeatAt: now,
|
|
53
|
+
createdAt: now,
|
|
54
|
+
updatedAt: now,
|
|
55
|
+
});
|
|
56
|
+
return { success: true, statusId: id };
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get relay status by relay ID
|
|
63
|
+
*/
|
|
64
|
+
export const getByRelayId = query({
|
|
65
|
+
args: {
|
|
66
|
+
relayId: v.string(),
|
|
67
|
+
},
|
|
68
|
+
returns: v.union(
|
|
69
|
+
v.object({
|
|
70
|
+
_id: v.id("relayStatus"),
|
|
71
|
+
relayId: v.string(),
|
|
72
|
+
capabilities: v.array(capabilityValidator),
|
|
73
|
+
metrics: v.optional(metricsValidator),
|
|
74
|
+
version: v.optional(v.string()),
|
|
75
|
+
hostname: v.optional(v.string()),
|
|
76
|
+
platform: v.optional(v.string()),
|
|
77
|
+
lastHeartbeatAt: v.number(),
|
|
78
|
+
createdAt: v.number(),
|
|
79
|
+
updatedAt: v.number(),
|
|
80
|
+
}),
|
|
81
|
+
v.null()
|
|
82
|
+
),
|
|
83
|
+
handler: async (ctx, args) => {
|
|
84
|
+
const status = await ctx.db
|
|
85
|
+
.query("relayStatus")
|
|
86
|
+
.withIndex("by_relayId", (q) => q.eq("relayId", args.relayId))
|
|
87
|
+
.first();
|
|
88
|
+
|
|
89
|
+
if (!status) return null;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
_id: status._id,
|
|
93
|
+
relayId: status.relayId,
|
|
94
|
+
capabilities: status.capabilities,
|
|
95
|
+
metrics: status.metrics,
|
|
96
|
+
version: status.version,
|
|
97
|
+
hostname: status.hostname,
|
|
98
|
+
platform: status.platform,
|
|
99
|
+
lastHeartbeatAt: status.lastHeartbeatAt,
|
|
100
|
+
createdAt: status.createdAt,
|
|
101
|
+
updatedAt: status.updatedAt,
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* List all relay statuses
|
|
108
|
+
*/
|
|
109
|
+
export const listAll = query({
|
|
110
|
+
args: {},
|
|
111
|
+
returns: v.array(
|
|
112
|
+
v.object({
|
|
113
|
+
_id: v.id("relayStatus"),
|
|
114
|
+
relayId: v.string(),
|
|
115
|
+
capabilities: v.array(capabilityValidator),
|
|
116
|
+
metrics: v.optional(metricsValidator),
|
|
117
|
+
version: v.optional(v.string()),
|
|
118
|
+
hostname: v.optional(v.string()),
|
|
119
|
+
platform: v.optional(v.string()),
|
|
120
|
+
lastHeartbeatAt: v.number(),
|
|
121
|
+
isOnline: v.boolean(),
|
|
122
|
+
})
|
|
123
|
+
),
|
|
124
|
+
handler: async (ctx) => {
|
|
125
|
+
const statuses = await ctx.db.query("relayStatus").collect();
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const ONLINE_THRESHOLD_MS = 60000; // 1 minute
|
|
128
|
+
|
|
129
|
+
return statuses.map((s) => ({
|
|
130
|
+
_id: s._id,
|
|
131
|
+
relayId: s.relayId,
|
|
132
|
+
capabilities: s.capabilities,
|
|
133
|
+
metrics: s.metrics,
|
|
134
|
+
version: s.version,
|
|
135
|
+
hostname: s.hostname,
|
|
136
|
+
platform: s.platform,
|
|
137
|
+
lastHeartbeatAt: s.lastHeartbeatAt,
|
|
138
|
+
isOnline: now - s.lastHeartbeatAt < ONLINE_THRESHOLD_MS,
|
|
139
|
+
}));
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Find relays that have a specific capability
|
|
145
|
+
*/
|
|
146
|
+
export const findByCapability = query({
|
|
147
|
+
args: {
|
|
148
|
+
capability: capabilityValidator,
|
|
149
|
+
},
|
|
150
|
+
returns: v.array(
|
|
151
|
+
v.object({
|
|
152
|
+
relayId: v.string(),
|
|
153
|
+
capabilities: v.array(capabilityValidator),
|
|
154
|
+
lastHeartbeatAt: v.number(),
|
|
155
|
+
isOnline: v.boolean(),
|
|
156
|
+
})
|
|
157
|
+
),
|
|
158
|
+
handler: async (ctx, args) => {
|
|
159
|
+
const statuses = await ctx.db.query("relayStatus").collect();
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
const ONLINE_THRESHOLD_MS = 60000;
|
|
162
|
+
|
|
163
|
+
return statuses
|
|
164
|
+
.filter((s) => s.capabilities.includes(args.capability))
|
|
165
|
+
.map((s) => ({
|
|
166
|
+
relayId: s.relayId,
|
|
167
|
+
capabilities: s.capabilities,
|
|
168
|
+
lastHeartbeatAt: s.lastHeartbeatAt,
|
|
169
|
+
isOnline: now - s.lastHeartbeatAt < ONLINE_THRESHOLD_MS,
|
|
170
|
+
}));
|
|
171
|
+
},
|
|
172
|
+
});
|