@sigil-dev/plugin-health 0.9.3 → 0.9.5

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.
Files changed (2) hide show
  1. package/index.ts +150 -150
  2. package/package.json +2 -2
package/index.ts CHANGED
@@ -1,150 +1,150 @@
1
- import type { CoordinatorContext, GrimoirePlugin } from "@sigil-dev/grimoire";
2
-
3
- export interface HealthPluginOptions {
4
- /** Route to serve health at. Default: /health */
5
- path?: string;
6
- /** Extra fields to include in the response */
7
- extend?: () => Record<string, unknown> | Promise<Record<string, unknown>>;
8
- /** By default the plugin pulls names from @sigil-dev/plugin-namepool, but you can specify overrides.*/
9
- names?: string[];
10
- }
11
-
12
- export type WorkerHealthStatus = "up" | "down" | "reserved";
13
-
14
- export interface WorkerHealth {
15
- name: string;
16
- status: WorkerHealthStatus;
17
- pid?: number;
18
- version: string;
19
- lastSeen: number | null; // null = never existed
20
- }
21
- export interface HealthResponse {
22
- ok: boolean;
23
- version: string;
24
- uptime: number;
25
- instanceId?: string;
26
- cluster?: WorkerHealth[];
27
- [key: string]: unknown;
28
- }
29
-
30
- export function healthPlugin(
31
- options: HealthPluginOptions = {},
32
- ): GrimoirePlugin {
33
- const healthPath = options.path ?? "/health";
34
- const startedAt = Date.now();
35
- let names: string[] | undefined = options.names;
36
-
37
- // set by onCoordinatorStart — stays null in worker context
38
- let coordinatorCtx: CoordinatorContext | null = null;
39
-
40
- // last successful contact per worker — lets a status page show
41
- // "up but hasn't heartbeated recently" vs a fresh respawn
42
- const lastSeen = new Map<string, number>();
43
-
44
- return {
45
- name: "@sigil-dev/plugin-health",
46
-
47
- onPluginsResolved(ctx) {
48
- const pool = ctx.plugins.get("namepool") as any;
49
- names ??= pool?.names;
50
- },
51
-
52
- onCoordinatorStart(ctx) {
53
- coordinatorCtx = ctx;
54
- for (const w of ctx.workers) {
55
- lastSeen.set(w.name ?? `${w.mode}-${w.index}`, Date.now());
56
- }
57
- },
58
-
59
- onWorkerReady(worker) {
60
- lastSeen.set(worker.name ?? `${worker.mode}-${worker.index}`, Date.now());
61
- },
62
-
63
- async onRequest(req, next) {
64
- const url = new URL(req.url);
65
- if (url.pathname !== healthPath) return next();
66
-
67
- // ── coordinator context: fan out to all workers ──
68
- if (coordinatorCtx) {
69
- const cluster: WorkerHealth[] = await Promise.all(
70
- coordinatorCtx.workers.map(async (w): Promise<WorkerHealth> => {
71
- const key = w.name ?? `${w.mode}-${w.index}`;
72
- try {
73
- const res = await fetch(`${w.internalUrl}${healthPath}`, {
74
- signal: AbortSignal.timeout(2_000),
75
- headers: { "X-Grimoire-Internal": coordinatorCtx!.secret },
76
- });
77
- if (res.ok) {
78
- lastSeen.set(key, Date.now());
79
- const data = (await res.json()) as HealthResponse;
80
- return {
81
- name: key,
82
- status: "up",
83
- pid: w.pid,
84
- version: data.version ?? "",
85
- lastSeen: lastSeen.get(key)!,
86
- };
87
- }
88
- } catch {}
89
- // didn't respond this poll — either mid-respawn (transient)
90
- // or crash-looping (persistent). either way: down.
91
- return {
92
- name: key,
93
- status: "down",
94
- pid: w.pid,
95
- version: "",
96
- lastSeen: lastSeen.get(key) ?? 0,
97
- };
98
- }),
99
- );
100
-
101
- if (names) {
102
- const activeNames = new Set(cluster.map((w) => w.name));
103
- for (const name of names) {
104
- if (!activeNames.has(name)) {
105
- cluster.push({
106
- name,
107
- status: "reserved",
108
- version: "",
109
- lastSeen: null,
110
- });
111
- }
112
- }
113
- }
114
- const allOk = cluster.every(
115
- (w) => w.status === "up" || w.status === "reserved",
116
- );
117
-
118
- return Response.json(
119
- {
120
- ok: allOk,
121
- version: process.env.npm_package_version ?? "0.0.0",
122
- uptime: Math.floor((Date.now() - startedAt) / 1000),
123
- cluster,
124
- } satisfies HealthResponse,
125
- {
126
- status: allOk ? 200 : 503,
127
- headers: { "Cache-Control": "no-store" },
128
- },
129
- );
130
- }
131
-
132
- // ── worker context: report local status ──
133
- const extra = options.extend ? await options.extend() : {};
134
- return Response.json(
135
- {
136
- ok: true,
137
- version: process.env.npm_package_version ?? "0.0.0",
138
- uptime: Math.floor((Date.now() - startedAt) / 1000),
139
- instanceId: process.env.GRIMOIRE_WORKER_NAME,
140
- pid: process.pid,
141
- ...extra,
142
- } satisfies HealthResponse,
143
- {
144
- status: 200,
145
- headers: { "Cache-Control": "no-store" },
146
- },
147
- );
148
- },
149
- };
150
- }
1
+ import type { CoordinatorContext, GrimoirePlugin } from "@sigil-dev/grimoire";
2
+
3
+ export interface HealthPluginOptions {
4
+ /** Route to serve health at. Default: /health */
5
+ path?: string;
6
+ /** Extra fields to include in the response */
7
+ extend?: () => Record<string, unknown> | Promise<Record<string, unknown>>;
8
+ /** By default the plugin pulls names from @sigil-dev/plugin-namepool, but you can specify overrides.*/
9
+ names?: string[];
10
+ }
11
+
12
+ export type WorkerHealthStatus = "up" | "down" | "reserved";
13
+
14
+ export interface WorkerHealth {
15
+ name: string;
16
+ status: WorkerHealthStatus;
17
+ pid?: number;
18
+ version: string;
19
+ lastSeen: number | null; // null = never existed
20
+ }
21
+ export interface HealthResponse {
22
+ ok: boolean;
23
+ version: string;
24
+ uptime: number;
25
+ instanceId?: string;
26
+ cluster?: WorkerHealth[];
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ export function healthPlugin(
31
+ options: HealthPluginOptions = {},
32
+ ): GrimoirePlugin {
33
+ const healthPath = options.path ?? "/health";
34
+ const startedAt = Date.now();
35
+ let names: string[] | undefined = options.names;
36
+
37
+ // set by onCoordinatorStart — stays null in worker context
38
+ let coordinatorCtx: CoordinatorContext | null = null;
39
+
40
+ // last successful contact per worker — lets a status page show
41
+ // "up but hasn't heartbeated recently" vs a fresh respawn
42
+ const lastSeen = new Map<string, number>();
43
+
44
+ return {
45
+ name: "@sigil-dev/plugin-health",
46
+
47
+ onPluginsResolved(ctx) {
48
+ const pool = ctx.plugins.get("namepool") as any;
49
+ names ??= pool?.names;
50
+ },
51
+
52
+ onCoordinatorStart(ctx) {
53
+ coordinatorCtx = ctx;
54
+ for (const w of ctx.workers) {
55
+ lastSeen.set(w.name ?? `${w.mode}-${w.index}`, Date.now());
56
+ }
57
+ },
58
+
59
+ onWorkerReady(worker) {
60
+ lastSeen.set(worker.name ?? `${worker.mode}-${worker.index}`, Date.now());
61
+ },
62
+
63
+ async onRequest(req, next) {
64
+ const url = new URL(req.url);
65
+ if (url.pathname !== healthPath) return next();
66
+
67
+ // ── coordinator context: fan out to all workers ──
68
+ if (coordinatorCtx) {
69
+ const cluster: WorkerHealth[] = await Promise.all(
70
+ coordinatorCtx.workers.map(async (w): Promise<WorkerHealth> => {
71
+ const key = w.name ?? `${w.mode}-${w.index}`;
72
+ try {
73
+ const res = await fetch(`${w.internalUrl}${healthPath}`, {
74
+ signal: AbortSignal.timeout(2_000),
75
+ headers: { "X-Grimoire-Internal": coordinatorCtx!.secret },
76
+ });
77
+ if (res.ok) {
78
+ lastSeen.set(key, Date.now());
79
+ const data = (await res.json()) as HealthResponse;
80
+ return {
81
+ name: key,
82
+ status: "up",
83
+ pid: w.pid,
84
+ version: data.version ?? "",
85
+ lastSeen: lastSeen.get(key)!,
86
+ };
87
+ }
88
+ } catch {}
89
+ // didn't respond this poll — either mid-respawn (transient)
90
+ // or crash-looping (persistent). either way: down.
91
+ return {
92
+ name: key,
93
+ status: "down",
94
+ pid: w.pid,
95
+ version: "",
96
+ lastSeen: lastSeen.get(key) ?? 0,
97
+ };
98
+ }),
99
+ );
100
+
101
+ if (names) {
102
+ const activeNames = new Set(cluster.map((w) => w.name));
103
+ for (const name of names) {
104
+ if (!activeNames.has(name)) {
105
+ cluster.push({
106
+ name,
107
+ status: "reserved",
108
+ version: "",
109
+ lastSeen: null,
110
+ });
111
+ }
112
+ }
113
+ }
114
+ const allOk = cluster.every(
115
+ (w) => w.status === "up" || w.status === "reserved",
116
+ );
117
+
118
+ return Response.json(
119
+ {
120
+ ok: allOk,
121
+ version: process.env.npm_package_version ?? "0.0.0",
122
+ uptime: Math.floor((Date.now() - startedAt) / 1000),
123
+ cluster,
124
+ } satisfies HealthResponse,
125
+ {
126
+ status: allOk ? 200 : 503,
127
+ headers: { "Cache-Control": "no-store" },
128
+ },
129
+ );
130
+ }
131
+
132
+ // ── worker context: report local status ──
133
+ const extra = options.extend ? await options.extend() : {};
134
+ return Response.json(
135
+ {
136
+ ok: true,
137
+ version: process.env.npm_package_version ?? "0.0.0",
138
+ uptime: Math.floor((Date.now() - startedAt) / 1000),
139
+ instanceId: process.env.GRIMOIRE_WORKER_NAME,
140
+ pid: process.pid,
141
+ ...extra,
142
+ } satisfies HealthResponse,
143
+ {
144
+ status: 200,
145
+ headers: { "Cache-Control": "no-store" },
146
+ },
147
+ );
148
+ },
149
+ };
150
+ }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "dependencies": {
8
- "@sigil-dev/grimoire": "0.9.3"
8
+ "@sigil-dev/grimoire": "0.9.5"
9
9
  },
10
10
  "devDependencies": {
11
11
  "@types/bun": "latest"
@@ -13,5 +13,5 @@
13
13
  "peerDependencies": {
14
14
  "typescript": "^5"
15
15
  },
16
- "version": "0.9.3"
16
+ "version": "0.9.5"
17
17
  }