@checkstack/satellite-backend 0.3.6 → 0.4.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/CHANGELOG.md +51 -0
- package/package.json +11 -9
- package/src/automations.test.ts +54 -0
- package/src/automations.ts +66 -0
- package/src/heartbeat-monitor.ts +41 -1
- package/src/hooks.ts +37 -0
- package/src/index.ts +21 -0
- package/src/satellite-ws-handler.ts +70 -3
- package/tsconfig.json +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @checkstack/satellite-backend
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 41c77f4: feat(satellite): Phase 9 — connection lifecycle triggers
|
|
8
|
+
|
|
9
|
+
- New hooks `satelliteHooks.connected`, `satelliteHooks.disconnected`,
|
|
10
|
+
and `satelliteHooks.heartbeatLost`. `connected` and `disconnected`
|
|
11
|
+
fire from the WS handler at auth completion and `onClose`
|
|
12
|
+
respectively; `heartbeatLost` fires from the heartbeat monitor on
|
|
13
|
+
the `online → offline` edge only (the opposite edge is observable
|
|
14
|
+
via `connected`).
|
|
15
|
+
- Triggers `satellite.connected`, `satellite.disconnected`,
|
|
16
|
+
`satellite.heartbeat_lost` registered against the Automation
|
|
17
|
+
Platform. All carry `contextKey: (p) => p.satelliteId` so a
|
|
18
|
+
long-running automation can resume on the same satellite.
|
|
19
|
+
- No mutation actions in this chunk — connection lifecycle is
|
|
20
|
+
observed only, not commanded.
|
|
21
|
+
|
|
22
|
+
Plumbing: `SatelliteWsHandler` and `HeartbeatMonitor` both take an
|
|
23
|
+
optional hook sink in their constructor. The sink is provided from
|
|
24
|
+
`afterPluginsReady` where `emitHook` is available; until then, the
|
|
25
|
+
classes behave exactly as before (no hooks fired, no behavioural
|
|
26
|
+
change).
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- Updated dependencies [e2d6f25]
|
|
31
|
+
- Updated dependencies [41c77f4]
|
|
32
|
+
- Updated dependencies [41c77f4]
|
|
33
|
+
- Updated dependencies [e1a2077]
|
|
34
|
+
- Updated dependencies [41c77f4]
|
|
35
|
+
- Updated dependencies [41c77f4]
|
|
36
|
+
- Updated dependencies [41c77f4]
|
|
37
|
+
- Updated dependencies [41c77f4]
|
|
38
|
+
- Updated dependencies [41c77f4]
|
|
39
|
+
- Updated dependencies [41c77f4]
|
|
40
|
+
- Updated dependencies [6d52276]
|
|
41
|
+
- Updated dependencies [6d52276]
|
|
42
|
+
- Updated dependencies [35bc682]
|
|
43
|
+
- @checkstack/automation-backend@0.2.0
|
|
44
|
+
- @checkstack/healthcheck-backend@1.3.0
|
|
45
|
+
- @checkstack/common@0.12.0
|
|
46
|
+
- @checkstack/backend-api@0.18.0
|
|
47
|
+
- @checkstack/healthcheck-common@1.3.0
|
|
48
|
+
- @checkstack/satellite-common@0.6.0
|
|
49
|
+
- @checkstack/gitops-backend@0.3.7
|
|
50
|
+
- @checkstack/gitops-common@0.4.2
|
|
51
|
+
- @checkstack/signal-common@0.2.5
|
|
52
|
+
- @checkstack/queue-api@0.3.6
|
|
53
|
+
|
|
3
54
|
## 0.3.6
|
|
4
55
|
|
|
5
56
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/satellite-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -11,18 +11,20 @@
|
|
|
11
11
|
"typecheck": "tsgo -b",
|
|
12
12
|
"generate": "drizzle-kit generate",
|
|
13
13
|
"lint": "bun run lint:code",
|
|
14
|
-
"lint:code": "eslint . --max-warnings 0"
|
|
14
|
+
"lint:code": "eslint . --max-warnings 0",
|
|
15
|
+
"test": "bun test"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
17
|
-
"@checkstack/backend-api": "0.17.
|
|
18
|
-
"@checkstack/
|
|
19
|
-
"@checkstack/
|
|
18
|
+
"@checkstack/backend-api": "0.17.1",
|
|
19
|
+
"@checkstack/automation-backend": "0.1.0",
|
|
20
|
+
"@checkstack/satellite-common": "0.5.3",
|
|
21
|
+
"@checkstack/healthcheck-common": "1.2.0",
|
|
20
22
|
"@checkstack/signal-common": "0.2.4",
|
|
21
|
-
"@checkstack/healthcheck-backend": "1.
|
|
22
|
-
"@checkstack/gitops-backend": "0.3.
|
|
23
|
+
"@checkstack/healthcheck-backend": "1.2.0",
|
|
24
|
+
"@checkstack/gitops-backend": "0.3.6",
|
|
23
25
|
"@checkstack/gitops-common": "0.4.1",
|
|
24
26
|
"@checkstack/common": "0.11.0",
|
|
25
|
-
"@checkstack/queue-api": "0.3.
|
|
27
|
+
"@checkstack/queue-api": "0.3.5",
|
|
26
28
|
"drizzle-orm": "^0.45.0",
|
|
27
29
|
"zod": "^4.2.1",
|
|
28
30
|
"@orpc/server": "^1.13.2"
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
"devDependencies": {
|
|
31
33
|
"@checkstack/drizzle-helper": "0.0.5",
|
|
32
34
|
"@checkstack/scripts": "0.3.3",
|
|
33
|
-
"@checkstack/test-utils-backend": "0.1.
|
|
35
|
+
"@checkstack/test-utils-backend": "0.1.30",
|
|
34
36
|
"@checkstack/tsconfig": "0.0.7",
|
|
35
37
|
"@types/bun": "^1.0.0",
|
|
36
38
|
"drizzle-kit": "^0.31.10",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behaviour tests for the satellite automation triggers.
|
|
3
|
+
* No mutation actions in this chunk — connection lifecycle is observed
|
|
4
|
+
* only, not commanded.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from "bun:test";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
satelliteConnectedTrigger,
|
|
10
|
+
satelliteDisconnectedTrigger,
|
|
11
|
+
satelliteHeartbeatLostTrigger,
|
|
12
|
+
satelliteTriggers,
|
|
13
|
+
} from "./automations";
|
|
14
|
+
import { satelliteHooks } from "./hooks";
|
|
15
|
+
|
|
16
|
+
describe("satellite triggers", () => {
|
|
17
|
+
it("exposes three triggers in a stable order", () => {
|
|
18
|
+
expect(satelliteTriggers).toHaveLength(3);
|
|
19
|
+
expect(satelliteTriggers[0]).toBe(
|
|
20
|
+
satelliteConnectedTrigger as (typeof satelliteTriggers)[number],
|
|
21
|
+
);
|
|
22
|
+
expect(satelliteTriggers[1]).toBe(
|
|
23
|
+
satelliteDisconnectedTrigger as (typeof satelliteTriggers)[number],
|
|
24
|
+
);
|
|
25
|
+
expect(satelliteTriggers[2]).toBe(
|
|
26
|
+
satelliteHeartbeatLostTrigger as (typeof satelliteTriggers)[number],
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("binds each trigger to the matching hook", () => {
|
|
31
|
+
expect(satelliteConnectedTrigger.hook).toBe(satelliteHooks.connected);
|
|
32
|
+
expect(satelliteDisconnectedTrigger.hook).toBe(satelliteHooks.disconnected);
|
|
33
|
+
expect(satelliteHeartbeatLostTrigger.hook).toBe(satelliteHooks.heartbeatLost);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("extracts satelliteId as the contextKey on all three", () => {
|
|
37
|
+
const payload = {
|
|
38
|
+
satelliteId: "sat-1",
|
|
39
|
+
name: "EU-Central",
|
|
40
|
+
region: "eu-central-1",
|
|
41
|
+
timestamp: "2026-05-29T11:00:00Z",
|
|
42
|
+
};
|
|
43
|
+
expect(satelliteConnectedTrigger.contextKey?.(payload)).toBe("sat-1");
|
|
44
|
+
expect(satelliteDisconnectedTrigger.contextKey?.(payload)).toBe("sat-1");
|
|
45
|
+
expect(satelliteHeartbeatLostTrigger.contextKey?.(payload)).toBe("sat-1");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("rejects payloads missing required fields", () => {
|
|
49
|
+
const bad = satelliteConnectedTrigger.payloadSchema.safeParse({
|
|
50
|
+
satelliteId: "sat-1",
|
|
51
|
+
});
|
|
52
|
+
expect(bad.success).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Satellite triggers registered with the Automation Platform.
|
|
3
|
+
*
|
|
4
|
+
* The plan calls for `satellite.connected`, `satellite.disconnected`,
|
|
5
|
+
* and `satellite.heartbeat_lost` — all three were added as new hooks
|
|
6
|
+
* in `./hooks.ts` and emitted from the WS handler (connect/disconnect)
|
|
7
|
+
* and heartbeat monitor (online → offline). No mutation actions for
|
|
8
|
+
* satellite in this phase.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import type { TriggerDefinition } from "@checkstack/automation-backend";
|
|
12
|
+
|
|
13
|
+
import { satelliteHooks } from "./hooks";
|
|
14
|
+
|
|
15
|
+
const satelliteEventPayloadSchema = z.object({
|
|
16
|
+
satelliteId: z.string(),
|
|
17
|
+
name: z.string(),
|
|
18
|
+
region: z.string(),
|
|
19
|
+
timestamp: z.string(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const satelliteConnectedTrigger: TriggerDefinition<
|
|
23
|
+
z.infer<typeof satelliteEventPayloadSchema>
|
|
24
|
+
> = {
|
|
25
|
+
id: "connected",
|
|
26
|
+
displayName: "Satellite Connected",
|
|
27
|
+
description: "Fires when a satellite WebSocket completes authentication",
|
|
28
|
+
category: "Satellites",
|
|
29
|
+
icon: "Satellite",
|
|
30
|
+
payloadSchema: satelliteEventPayloadSchema,
|
|
31
|
+
hook: satelliteHooks.connected,
|
|
32
|
+
contextKey: (p) => p.satelliteId,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const satelliteDisconnectedTrigger: TriggerDefinition<
|
|
36
|
+
z.infer<typeof satelliteEventPayloadSchema>
|
|
37
|
+
> = {
|
|
38
|
+
id: "disconnected",
|
|
39
|
+
displayName: "Satellite Disconnected",
|
|
40
|
+
description: "Fires when a satellite's WebSocket closes",
|
|
41
|
+
category: "Satellites",
|
|
42
|
+
icon: "Satellite",
|
|
43
|
+
payloadSchema: satelliteEventPayloadSchema,
|
|
44
|
+
hook: satelliteHooks.disconnected,
|
|
45
|
+
contextKey: (p) => p.satelliteId,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const satelliteHeartbeatLostTrigger: TriggerDefinition<
|
|
49
|
+
z.infer<typeof satelliteEventPayloadSchema>
|
|
50
|
+
> = {
|
|
51
|
+
id: "heartbeat_lost",
|
|
52
|
+
displayName: "Satellite Heartbeat Lost",
|
|
53
|
+
description:
|
|
54
|
+
"Fires when a satellite transitions online → offline (no heartbeat within threshold)",
|
|
55
|
+
category: "Satellites",
|
|
56
|
+
icon: "Satellite",
|
|
57
|
+
payloadSchema: satelliteEventPayloadSchema,
|
|
58
|
+
hook: satelliteHooks.heartbeatLost,
|
|
59
|
+
contextKey: (p) => p.satelliteId,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const satelliteTriggers: TriggerDefinition<unknown>[] = [
|
|
63
|
+
satelliteConnectedTrigger as TriggerDefinition<unknown>,
|
|
64
|
+
satelliteDisconnectedTrigger as TriggerDefinition<unknown>,
|
|
65
|
+
satelliteHeartbeatLostTrigger as TriggerDefinition<unknown>,
|
|
66
|
+
];
|
package/src/heartbeat-monitor.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Logger } from "@checkstack/backend-api";
|
|
1
|
+
import type { Hook, Logger } from "@checkstack/backend-api";
|
|
2
2
|
import type { SignalService } from "@checkstack/signal-common";
|
|
3
3
|
import type { SatelliteService } from "./service";
|
|
4
4
|
import {
|
|
@@ -6,6 +6,22 @@ import {
|
|
|
6
6
|
OFFLINE_THRESHOLD_MS,
|
|
7
7
|
} from "@checkstack/satellite-common";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Optional plug-point for firing the automation
|
|
11
|
+
* `satellite.heartbeat_lost` trigger when the monitor observes a
|
|
12
|
+
* satellite transitioning `online` → `offline`. Bound from
|
|
13
|
+
* `afterPluginsReady`; when not provided, no hook fires.
|
|
14
|
+
*/
|
|
15
|
+
export interface SatelliteHeartbeatHookSink {
|
|
16
|
+
emitHook: <T>(hook: Hook<T>, payload: T) => Promise<void>;
|
|
17
|
+
heartbeatLostHook: Hook<{
|
|
18
|
+
satelliteId: string;
|
|
19
|
+
name: string;
|
|
20
|
+
region: string;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
/**
|
|
10
26
|
* Monitors satellite heartbeats and broadcasts status change signals.
|
|
11
27
|
* Tracks previous state in-memory to detect transitions (online → offline, offline → online).
|
|
@@ -21,6 +37,7 @@ export class HeartbeatMonitor {
|
|
|
21
37
|
private service: SatelliteService,
|
|
22
38
|
private signalService: SignalService,
|
|
23
39
|
private logger: Logger,
|
|
40
|
+
private hookSink?: SatelliteHeartbeatHookSink,
|
|
24
41
|
) {}
|
|
25
42
|
|
|
26
43
|
/**
|
|
@@ -46,6 +63,29 @@ export class HeartbeatMonitor {
|
|
|
46
63
|
name: satellite.name,
|
|
47
64
|
region: satellite.region,
|
|
48
65
|
});
|
|
66
|
+
|
|
67
|
+
// Fire the automation `heartbeat_lost` hook only on the
|
|
68
|
+
// online → offline edge. The opposite transition is observable
|
|
69
|
+
// via the `satellite.connected` hook fired by the WS handler.
|
|
70
|
+
if (
|
|
71
|
+
previousStatus === "online" &&
|
|
72
|
+
currentStatus === "offline" &&
|
|
73
|
+
this.hookSink
|
|
74
|
+
) {
|
|
75
|
+
try {
|
|
76
|
+
await this.hookSink.emitHook(this.hookSink.heartbeatLostHook, {
|
|
77
|
+
satelliteId: satellite.id,
|
|
78
|
+
name: satellite.name,
|
|
79
|
+
region: satellite.region,
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
});
|
|
82
|
+
} catch (error) {
|
|
83
|
+
this.logger.error(
|
|
84
|
+
`Failed to emit satellite.heartbeat_lost hook for ${satellite.name}:`,
|
|
85
|
+
error,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
49
89
|
}
|
|
50
90
|
|
|
51
91
|
this.previousStatuses.set(satellite.id, currentStatus);
|
package/src/hooks.ts
CHANGED
|
@@ -14,4 +14,41 @@ export const satelliteHooks = {
|
|
|
14
14
|
satelliteRemoved: createHook<{
|
|
15
15
|
satelliteId: string;
|
|
16
16
|
}>("satellite.removed"),
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Emitted when a satellite WebSocket completes authentication and
|
|
20
|
+
* registers itself in the in-memory connection map.
|
|
21
|
+
*/
|
|
22
|
+
connected: createHook<{
|
|
23
|
+
satelliteId: string;
|
|
24
|
+
name: string;
|
|
25
|
+
region: string;
|
|
26
|
+
timestamp: string;
|
|
27
|
+
}>("satellite.connected"),
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Emitted when a previously-connected satellite's WebSocket closes
|
|
31
|
+
* (graceful or otherwise). Distinct from `heartbeatLost`: this fires
|
|
32
|
+
* the moment the socket drops, regardless of whether the satellite
|
|
33
|
+
* comes back within the heartbeat window.
|
|
34
|
+
*/
|
|
35
|
+
disconnected: createHook<{
|
|
36
|
+
satelliteId: string;
|
|
37
|
+
name: string;
|
|
38
|
+
region: string;
|
|
39
|
+
timestamp: string;
|
|
40
|
+
}>("satellite.disconnected"),
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Emitted by the heartbeat monitor when a satellite's status
|
|
44
|
+
* transitions from `online` to `offline` — i.e. no heartbeat for
|
|
45
|
+
* longer than `OFFLINE_THRESHOLD_MS`. Used by automations that
|
|
46
|
+
* page on stale satellites.
|
|
47
|
+
*/
|
|
48
|
+
heartbeatLost: createHook<{
|
|
49
|
+
satelliteId: string;
|
|
50
|
+
name: string;
|
|
51
|
+
region: string;
|
|
52
|
+
timestamp: string;
|
|
53
|
+
}>("satellite.heartbeat_lost"),
|
|
17
54
|
} as const;
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,9 @@ import { SatelliteWsHandler } from "./satellite-ws-handler";
|
|
|
16
16
|
import { ConfigRelay } from "./config-relay";
|
|
17
17
|
import { entityKindExtensionPoint } from "@checkstack/gitops-backend";
|
|
18
18
|
import { registerSatelliteGitOpsKinds } from "./satellite-gitops-kinds";
|
|
19
|
+
import { automationTriggerExtensionPoint } from "@checkstack/automation-backend";
|
|
20
|
+
import { satelliteTriggers } from "./automations";
|
|
21
|
+
import { satelliteHooks } from "./hooks";
|
|
19
22
|
|
|
20
23
|
// Queue and job constants
|
|
21
24
|
const HEARTBEAT_QUEUE = "satellite-heartbeat";
|
|
@@ -27,6 +30,14 @@ export default createBackendPlugin({
|
|
|
27
30
|
register(env) {
|
|
28
31
|
env.registerAccessRules(satelliteAccessRules);
|
|
29
32
|
|
|
33
|
+
// ─── Automation Platform: triggers ───────────────────────────────
|
|
34
|
+
const automationTriggers = env.getExtensionPoint(
|
|
35
|
+
automationTriggerExtensionPoint,
|
|
36
|
+
);
|
|
37
|
+
for (const trigger of satelliteTriggers) {
|
|
38
|
+
automationTriggers.registerTrigger(trigger, pluginMetadata);
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
// ─── GitOps Entity Kind Registration ─────────────────────────────
|
|
31
42
|
let gitopsService: SatelliteService | undefined;
|
|
32
43
|
const kindRegistry = env.getExtensionPoint(entityKindExtensionPoint);
|
|
@@ -73,6 +84,7 @@ export default createBackendPlugin({
|
|
|
73
84
|
wsRegistry,
|
|
74
85
|
rpcClient,
|
|
75
86
|
onHook,
|
|
87
|
+
emitHook,
|
|
76
88
|
}) => {
|
|
77
89
|
const service = new SatelliteService(
|
|
78
90
|
database as SafeDatabase<typeof schema>,
|
|
@@ -112,6 +124,11 @@ export default createBackendPlugin({
|
|
|
112
124
|
},
|
|
113
125
|
},
|
|
114
126
|
logger,
|
|
127
|
+
{
|
|
128
|
+
emitHook,
|
|
129
|
+
connectedHook: satelliteHooks.connected,
|
|
130
|
+
disconnectedHook: satelliteHooks.disconnected,
|
|
131
|
+
},
|
|
115
132
|
);
|
|
116
133
|
|
|
117
134
|
// Register satellite WebSocket endpoint via the scoped WS registry
|
|
@@ -124,6 +141,10 @@ export default createBackendPlugin({
|
|
|
124
141
|
service,
|
|
125
142
|
signalService,
|
|
126
143
|
logger,
|
|
144
|
+
{
|
|
145
|
+
emitHook,
|
|
146
|
+
heartbeatLostHook: satelliteHooks.heartbeatLost,
|
|
147
|
+
},
|
|
127
148
|
);
|
|
128
149
|
|
|
129
150
|
const queue = queueManager.getQueue<Record<string, never>>(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Logger } from "@checkstack/backend-api";
|
|
1
|
+
import type { Hook, Logger } from "@checkstack/backend-api";
|
|
2
2
|
import type {
|
|
3
3
|
WebSocketRouteHandler,
|
|
4
4
|
WsConnection,
|
|
@@ -13,6 +13,27 @@ import {
|
|
|
13
13
|
type SatelliteWithStatus,
|
|
14
14
|
} from "@checkstack/satellite-common";
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Optional plug-point for firing automation triggers when a satellite
|
|
18
|
+
* connects or disconnects. Bound from `afterPluginsReady` where
|
|
19
|
+
* `emitHook` is available — when not provided, no hook fires.
|
|
20
|
+
*/
|
|
21
|
+
export interface SatelliteConnectionHookSink {
|
|
22
|
+
emitHook: <T>(hook: Hook<T>, payload: T) => Promise<void>;
|
|
23
|
+
connectedHook: Hook<{
|
|
24
|
+
satelliteId: string;
|
|
25
|
+
name: string;
|
|
26
|
+
region: string;
|
|
27
|
+
timestamp: string;
|
|
28
|
+
}>;
|
|
29
|
+
disconnectedHook: Hook<{
|
|
30
|
+
satelliteId: string;
|
|
31
|
+
name: string;
|
|
32
|
+
region: string;
|
|
33
|
+
timestamp: string;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
|
|
16
37
|
/**
|
|
17
38
|
* Callback for handling health check results received from satellites.
|
|
18
39
|
*/
|
|
@@ -45,6 +66,13 @@ export class SatelliteWsHandler implements WebSocketRouteHandler {
|
|
|
45
66
|
private configRelay: ConfigRelay,
|
|
46
67
|
private resultHandler: SatelliteResultHandler,
|
|
47
68
|
private logger: Logger,
|
|
69
|
+
/**
|
|
70
|
+
* Optional. When set, the handler fires `connected` / `disconnected`
|
|
71
|
+
* hooks at the same lifecycle points it logs. Wired by
|
|
72
|
+
* `afterPluginsReady` so the action graph stays decoupled from
|
|
73
|
+
* `emitHook` availability.
|
|
74
|
+
*/
|
|
75
|
+
private connectionHookSink?: SatelliteConnectionHookSink,
|
|
48
76
|
) {}
|
|
49
77
|
|
|
50
78
|
/**
|
|
@@ -94,6 +122,27 @@ export class SatelliteWsHandler implements WebSocketRouteHandler {
|
|
|
94
122
|
// Track connection
|
|
95
123
|
this.connections.set(satellite.id, { satellite, ws });
|
|
96
124
|
|
|
125
|
+
// Fire the automation `connected` hook (best-effort — never
|
|
126
|
+
// block the auth handshake on a hook subscriber failure).
|
|
127
|
+
if (this.connectionHookSink) {
|
|
128
|
+
try {
|
|
129
|
+
await this.connectionHookSink.emitHook(
|
|
130
|
+
this.connectionHookSink.connectedHook,
|
|
131
|
+
{
|
|
132
|
+
satelliteId: satellite.id,
|
|
133
|
+
name: satellite.name,
|
|
134
|
+
region: satellite.region,
|
|
135
|
+
timestamp: new Date().toISOString(),
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
this.logger.error(
|
|
140
|
+
`Failed to emit satellite.connected hook for ${satellite.name}:`,
|
|
141
|
+
error,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
97
146
|
// Update heartbeat on connect
|
|
98
147
|
await this.service.updateHeartbeat(satellite.id, {});
|
|
99
148
|
|
|
@@ -147,10 +196,28 @@ export class SatelliteWsHandler implements WebSocketRouteHandler {
|
|
|
147
196
|
|
|
148
197
|
const onClose = () => {
|
|
149
198
|
if (authenticatedSatellite) {
|
|
150
|
-
|
|
199
|
+
const closedSatellite = authenticatedSatellite;
|
|
200
|
+
this.connections.delete(closedSatellite.id);
|
|
151
201
|
this.logger.info(
|
|
152
|
-
`Satellite disconnected: ${
|
|
202
|
+
`Satellite disconnected: ${closedSatellite.name} (${closedSatellite.region})`,
|
|
153
203
|
);
|
|
204
|
+
if (this.connectionHookSink) {
|
|
205
|
+
// Fire-and-forget — `onClose` is sync, so don't await; we
|
|
206
|
+
// don't have a place to surface a rejection anyway.
|
|
207
|
+
void this.connectionHookSink
|
|
208
|
+
.emitHook(this.connectionHookSink.disconnectedHook, {
|
|
209
|
+
satelliteId: closedSatellite.id,
|
|
210
|
+
name: closedSatellite.name,
|
|
211
|
+
region: closedSatellite.region,
|
|
212
|
+
timestamp: new Date().toISOString(),
|
|
213
|
+
})
|
|
214
|
+
.catch((error: unknown) => {
|
|
215
|
+
this.logger.error(
|
|
216
|
+
`Failed to emit satellite.disconnected hook for ${closedSatellite.name}:`,
|
|
217
|
+
error,
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
154
221
|
}
|
|
155
222
|
};
|
|
156
223
|
|