@outfitter/daemon 0.1.0-rc.1
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/README.md +495 -0
- package/dist/index.d.ts +771 -0
- package/dist/index.js +618 -0
- package/package.json +55 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import { Result, TaggedErrorClass } from "@outfitter/contracts";
|
|
2
|
+
import { LoggerInstance } from "@outfitter/logging";
|
|
3
|
+
declare const DaemonErrorBase: TaggedErrorClass<"DaemonError", {
|
|
4
|
+
code: DaemonErrorCode;
|
|
5
|
+
message: string;
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* Error codes for daemon operations.
|
|
9
|
+
*
|
|
10
|
+
* - `ALREADY_RUNNING`: Daemon start requested but already running
|
|
11
|
+
* - `NOT_RUNNING`: Daemon stop requested but not running
|
|
12
|
+
* - `SHUTDOWN_TIMEOUT`: Graceful shutdown exceeded timeout
|
|
13
|
+
* - `PID_ERROR`: PID file operations failed
|
|
14
|
+
* - `START_FAILED`: Daemon failed to start
|
|
15
|
+
*/
|
|
16
|
+
type DaemonErrorCode = "ALREADY_RUNNING" | "NOT_RUNNING" | "SHUTDOWN_TIMEOUT" | "PID_ERROR" | "START_FAILED";
|
|
17
|
+
/**
|
|
18
|
+
* Error type for daemon operations.
|
|
19
|
+
*
|
|
20
|
+
* Uses the TaggedError pattern for type-safe error handling with Result types.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const error = new DaemonError({
|
|
25
|
+
* code: "ALREADY_RUNNING",
|
|
26
|
+
* message: "Daemon is already running with PID 1234",
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* if (error.code === "ALREADY_RUNNING") {
|
|
30
|
+
* console.log("Stop the existing daemon first");
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
declare class DaemonError extends DaemonErrorBase {}
|
|
35
|
+
/**
|
|
36
|
+
* Daemon lifecycle states.
|
|
37
|
+
*
|
|
38
|
+
* State machine transitions:
|
|
39
|
+
* - `stopped` -> `starting` (via start())
|
|
40
|
+
* - `starting` -> `running` (when initialization complete)
|
|
41
|
+
* - `starting` -> `stopped` (if start fails)
|
|
42
|
+
* - `running` -> `stopping` (via stop() or signal)
|
|
43
|
+
* - `stopping` -> `stopped` (when shutdown complete)
|
|
44
|
+
*/
|
|
45
|
+
type DaemonState = "stopped" | "starting" | "running" | "stopping";
|
|
46
|
+
/**
|
|
47
|
+
* Configuration options for creating a daemon.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const options: DaemonOptions = {
|
|
52
|
+
* name: "my-daemon",
|
|
53
|
+
* pidFile: "/var/run/my-daemon.pid",
|
|
54
|
+
* logger: myLogger,
|
|
55
|
+
* shutdownTimeout: 10000, // 10 seconds
|
|
56
|
+
* };
|
|
57
|
+
*
|
|
58
|
+
* const daemon = createDaemon(options);
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
interface DaemonOptions {
|
|
62
|
+
/**
|
|
63
|
+
* Unique name identifying this daemon.
|
|
64
|
+
* Used in log messages and error context.
|
|
65
|
+
*/
|
|
66
|
+
name: string;
|
|
67
|
+
/**
|
|
68
|
+
* Absolute path to the PID file.
|
|
69
|
+
* The daemon writes its process ID here on start and removes it on stop.
|
|
70
|
+
* Used to prevent multiple instances and for external process management.
|
|
71
|
+
*/
|
|
72
|
+
pidFile: string;
|
|
73
|
+
/**
|
|
74
|
+
* Optional logger instance for daemon messages.
|
|
75
|
+
* If not provided, logging is disabled.
|
|
76
|
+
*/
|
|
77
|
+
logger?: LoggerInstance;
|
|
78
|
+
/**
|
|
79
|
+
* Maximum time in milliseconds to wait for graceful shutdown.
|
|
80
|
+
* After this timeout, the daemon will force stop.
|
|
81
|
+
* @defaultValue 5000
|
|
82
|
+
*/
|
|
83
|
+
shutdownTimeout?: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Shutdown handler function type.
|
|
87
|
+
*
|
|
88
|
+
* Called during graceful shutdown to allow cleanup of resources.
|
|
89
|
+
* Must complete within the shutdown timeout.
|
|
90
|
+
*/
|
|
91
|
+
type ShutdownHandler = () => Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Daemon instance interface.
|
|
94
|
+
*
|
|
95
|
+
* Provides lifecycle management for a background process including
|
|
96
|
+
* start/stop operations, signal handling, and shutdown hooks.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* const daemon = createDaemon({
|
|
101
|
+
* name: "my-service",
|
|
102
|
+
* pidFile: "/var/run/my-service.pid",
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* // Register cleanup handlers
|
|
106
|
+
* daemon.onShutdown(async () => {
|
|
107
|
+
* await database.close();
|
|
108
|
+
* });
|
|
109
|
+
*
|
|
110
|
+
* // Start the daemon
|
|
111
|
+
* const result = await daemon.start();
|
|
112
|
+
* if (result.isErr()) {
|
|
113
|
+
* console.error("Failed to start:", result.error.message);
|
|
114
|
+
* process.exit(1);
|
|
115
|
+
* }
|
|
116
|
+
*
|
|
117
|
+
* // Daemon is now running...
|
|
118
|
+
* // Stop gracefully when needed
|
|
119
|
+
* await daemon.stop();
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
interface Daemon {
|
|
123
|
+
/**
|
|
124
|
+
* Current lifecycle state of the daemon.
|
|
125
|
+
*/
|
|
126
|
+
readonly state: DaemonState;
|
|
127
|
+
/**
|
|
128
|
+
* Start the daemon.
|
|
129
|
+
*
|
|
130
|
+
* Creates PID file and registers signal handlers.
|
|
131
|
+
* Transitions from `stopped` to `starting` then `running`.
|
|
132
|
+
*
|
|
133
|
+
* @returns Result with void on success, or DaemonError on failure
|
|
134
|
+
*/
|
|
135
|
+
start(): Promise<Result<void, DaemonError>>;
|
|
136
|
+
/**
|
|
137
|
+
* Stop the daemon gracefully.
|
|
138
|
+
*
|
|
139
|
+
* Runs shutdown handlers, removes PID file, and cleans up signal handlers.
|
|
140
|
+
* Transitions from `running` to `stopping` then `stopped`.
|
|
141
|
+
*
|
|
142
|
+
* @returns Result with void on success, or DaemonError on failure
|
|
143
|
+
*/
|
|
144
|
+
stop(): Promise<Result<void, DaemonError>>;
|
|
145
|
+
/**
|
|
146
|
+
* Check if the daemon is currently running.
|
|
147
|
+
*
|
|
148
|
+
* @returns true if state is "running", false otherwise
|
|
149
|
+
*/
|
|
150
|
+
isRunning(): boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Register a shutdown handler to be called during graceful shutdown.
|
|
153
|
+
*
|
|
154
|
+
* Multiple handlers can be registered and will be called in registration order.
|
|
155
|
+
* Handlers must complete within the shutdown timeout.
|
|
156
|
+
*
|
|
157
|
+
* @param handler - Async function to execute during shutdown
|
|
158
|
+
*/
|
|
159
|
+
onShutdown(handler: ShutdownHandler): void;
|
|
160
|
+
}
|
|
161
|
+
import { TaggedErrorClass as TaggedErrorClass2 } from "@outfitter/contracts";
|
|
162
|
+
declare const StaleSocketErrorBase: TaggedErrorClass2<"StaleSocketError", {
|
|
163
|
+
message: string;
|
|
164
|
+
socketPath: string;
|
|
165
|
+
pid?: number;
|
|
166
|
+
}>;
|
|
167
|
+
declare const ConnectionRefusedErrorBase: TaggedErrorClass2<"ConnectionRefusedError", {
|
|
168
|
+
message: string;
|
|
169
|
+
socketPath: string;
|
|
170
|
+
}>;
|
|
171
|
+
declare const ConnectionTimeoutErrorBase: TaggedErrorClass2<"ConnectionTimeoutError", {
|
|
172
|
+
message: string;
|
|
173
|
+
socketPath: string;
|
|
174
|
+
timeoutMs: number;
|
|
175
|
+
}>;
|
|
176
|
+
declare const ProtocolErrorBase: TaggedErrorClass2<"ProtocolError", {
|
|
177
|
+
message: string;
|
|
178
|
+
socketPath: string;
|
|
179
|
+
details?: string;
|
|
180
|
+
}>;
|
|
181
|
+
declare const LockErrorBase: TaggedErrorClass2<"LockError", {
|
|
182
|
+
message: string;
|
|
183
|
+
lockPath: string;
|
|
184
|
+
pid?: number;
|
|
185
|
+
}>;
|
|
186
|
+
/**
|
|
187
|
+
* Socket exists but daemon process is dead.
|
|
188
|
+
*
|
|
189
|
+
* Indicates a stale socket from a crashed daemon that needs cleanup
|
|
190
|
+
* before a new daemon can start.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```typescript
|
|
194
|
+
* const error = new StaleSocketError({
|
|
195
|
+
* message: "Daemon socket is stale",
|
|
196
|
+
* socketPath: "/run/user/1000/waymark/daemon.sock",
|
|
197
|
+
* pid: 12345,
|
|
198
|
+
* });
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
declare class StaleSocketError extends StaleSocketErrorBase {}
|
|
202
|
+
/**
|
|
203
|
+
* Daemon is not running (connection refused).
|
|
204
|
+
*
|
|
205
|
+
* Socket does not exist or connection was actively refused,
|
|
206
|
+
* indicating no daemon is listening.
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```typescript
|
|
210
|
+
* const error = new ConnectionRefusedError({
|
|
211
|
+
* message: "Connection refused",
|
|
212
|
+
* socketPath: "/run/user/1000/waymark/daemon.sock",
|
|
213
|
+
* });
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
declare class ConnectionRefusedError extends ConnectionRefusedErrorBase {}
|
|
217
|
+
/**
|
|
218
|
+
* Daemon did not respond within timeout.
|
|
219
|
+
*
|
|
220
|
+
* Connection was established but daemon failed to respond
|
|
221
|
+
* to ping or request within the configured timeout.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```typescript
|
|
225
|
+
* const error = new ConnectionTimeoutError({
|
|
226
|
+
* message: "Connection timed out after 5000ms",
|
|
227
|
+
* socketPath: "/run/user/1000/waymark/daemon.sock",
|
|
228
|
+
* timeoutMs: 5000,
|
|
229
|
+
* });
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
declare class ConnectionTimeoutError extends ConnectionTimeoutErrorBase {}
|
|
233
|
+
/**
|
|
234
|
+
* Invalid response format from daemon.
|
|
235
|
+
*
|
|
236
|
+
* Daemon responded but the response could not be parsed
|
|
237
|
+
* or did not match the expected protocol format.
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```typescript
|
|
241
|
+
* const error = new ProtocolError({
|
|
242
|
+
* message: "Invalid JSON response",
|
|
243
|
+
* socketPath: "/run/user/1000/waymark/daemon.sock",
|
|
244
|
+
* details: "Unexpected token at position 42",
|
|
245
|
+
* });
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
declare class ProtocolError extends ProtocolErrorBase {}
|
|
249
|
+
/**
|
|
250
|
+
* Failed to acquire or release daemon lock.
|
|
251
|
+
*
|
|
252
|
+
* Used when PID file operations fail due to permissions,
|
|
253
|
+
* concurrent access, or file system errors.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```typescript
|
|
257
|
+
* const error = new LockError({
|
|
258
|
+
* message: "Daemon already running",
|
|
259
|
+
* lockPath: "/run/user/1000/waymark/daemon.lock",
|
|
260
|
+
* pid: 12345,
|
|
261
|
+
* });
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
declare class LockError extends LockErrorBase {}
|
|
265
|
+
/**
|
|
266
|
+
* Union of all daemon connection error types.
|
|
267
|
+
*
|
|
268
|
+
* Use for exhaustive matching on connection failures:
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```typescript
|
|
272
|
+
* function handleError(error: DaemonConnectionError): string {
|
|
273
|
+
* switch (error._tag) {
|
|
274
|
+
* case "StaleSocketError":
|
|
275
|
+
* return `Stale socket at ${error.socketPath}`;
|
|
276
|
+
* case "ConnectionRefusedError":
|
|
277
|
+
* return "Daemon not running";
|
|
278
|
+
* case "ConnectionTimeoutError":
|
|
279
|
+
* return `Timeout after ${error.timeoutMs}ms`;
|
|
280
|
+
* case "ProtocolError":
|
|
281
|
+
* return `Protocol error: ${error.details}`;
|
|
282
|
+
* }
|
|
283
|
+
* }
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
type DaemonConnectionError = StaleSocketError | ConnectionRefusedError | ConnectionTimeoutError | ProtocolError;
|
|
287
|
+
/**
|
|
288
|
+
* Check if running on a Unix-like platform (macOS or Linux).
|
|
289
|
+
*
|
|
290
|
+
* @returns true on macOS/Linux, false on Windows
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```typescript
|
|
294
|
+
* if (isUnixPlatform()) {
|
|
295
|
+
* // Use Unix domain sockets
|
|
296
|
+
* } else {
|
|
297
|
+
* // Use named pipes
|
|
298
|
+
* }
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
301
|
+
declare function isUnixPlatform(): boolean;
|
|
302
|
+
/**
|
|
303
|
+
* Get the Unix domain socket path for a tool's daemon.
|
|
304
|
+
*
|
|
305
|
+
* @param toolName - Name of the tool (e.g., "waymark", "firewatch")
|
|
306
|
+
* @returns Absolute path to the daemon socket
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```typescript
|
|
310
|
+
* const socketPath = getSocketPath("waymark");
|
|
311
|
+
* // "/run/user/1000/waymark/daemon.sock" on Linux
|
|
312
|
+
* // "/var/folders/.../waymark/daemon.sock" on macOS
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
declare function getSocketPath(toolName: string): string;
|
|
316
|
+
/**
|
|
317
|
+
* Get the lock file path for a tool's daemon.
|
|
318
|
+
*
|
|
319
|
+
* @param toolName - Name of the tool (e.g., "waymark", "firewatch")
|
|
320
|
+
* @returns Absolute path to the daemon lock file
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```typescript
|
|
324
|
+
* const lockPath = getLockPath("waymark");
|
|
325
|
+
* // "/run/user/1000/waymark/daemon.lock" on Linux
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
declare function getLockPath(toolName: string): string;
|
|
329
|
+
/**
|
|
330
|
+
* Get the PID file path for a tool's daemon.
|
|
331
|
+
*
|
|
332
|
+
* @param toolName - Name of the tool (e.g., "waymark", "firewatch")
|
|
333
|
+
* @returns Absolute path to the daemon PID file
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* ```typescript
|
|
337
|
+
* const pidPath = getPidPath("waymark");
|
|
338
|
+
* // "/run/user/1000/waymark/daemon.pid" on Linux
|
|
339
|
+
* ```
|
|
340
|
+
*/
|
|
341
|
+
declare function getPidPath(toolName: string): string;
|
|
342
|
+
/**
|
|
343
|
+
* Get the directory containing daemon files for a tool.
|
|
344
|
+
*
|
|
345
|
+
* @param toolName - Name of the tool (e.g., "waymark", "firewatch")
|
|
346
|
+
* @returns Absolute path to the daemon directory
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```typescript
|
|
350
|
+
* const daemonDir = getDaemonDir("waymark");
|
|
351
|
+
* // "/run/user/1000/waymark" on Linux
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
declare function getDaemonDir(toolName: string): string;
|
|
355
|
+
import { Result as Result2 } from "@outfitter/contracts";
|
|
356
|
+
/**
|
|
357
|
+
* Handle returned when a lock is successfully acquired.
|
|
358
|
+
*
|
|
359
|
+
* Must be passed to `releaseDaemonLock` to properly release the lock.
|
|
360
|
+
*/
|
|
361
|
+
interface LockHandle {
|
|
362
|
+
/** Path to the lock file */
|
|
363
|
+
readonly lockPath: string;
|
|
364
|
+
/** PID that owns the lock */
|
|
365
|
+
readonly pid: number;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Check if a process with the given PID is alive.
|
|
369
|
+
*
|
|
370
|
+
* Uses `process.kill(pid, 0)` which sends no signal but checks
|
|
371
|
+
* if the process exists and is accessible.
|
|
372
|
+
*
|
|
373
|
+
* @param pid - Process ID to check
|
|
374
|
+
* @returns true if process exists and is accessible, false otherwise
|
|
375
|
+
*
|
|
376
|
+
* @example
|
|
377
|
+
* ```typescript
|
|
378
|
+
* if (isProcessAlive(12345)) {
|
|
379
|
+
* console.log("Process is still running");
|
|
380
|
+
* } else {
|
|
381
|
+
* console.log("Process has exited");
|
|
382
|
+
* }
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
declare function isProcessAlive(pid: number): boolean;
|
|
386
|
+
/**
|
|
387
|
+
* Check if a daemon is alive by reading its lock file.
|
|
388
|
+
*
|
|
389
|
+
* @param lockPath - Path to the daemon lock file
|
|
390
|
+
* @returns true if lock file exists and process is alive, false otherwise
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* ```typescript
|
|
394
|
+
* const alive = await isDaemonAlive("/run/user/1000/waymark/daemon.lock");
|
|
395
|
+
* if (!alive) {
|
|
396
|
+
* // Safe to start a new daemon
|
|
397
|
+
* }
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
declare function isDaemonAlive(lockPath: string): Promise<boolean>;
|
|
401
|
+
/**
|
|
402
|
+
* Acquire an exclusive lock for a daemon.
|
|
403
|
+
*
|
|
404
|
+
* Uses a PID file approach compatible with Bun:
|
|
405
|
+
* 1. Check if lock file exists with a valid PID
|
|
406
|
+
* 2. If PID is alive, refuse (daemon already running)
|
|
407
|
+
* 3. If PID is stale or no lock, write our PID atomically
|
|
408
|
+
*
|
|
409
|
+
* @param lockPath - Path to create the lock file
|
|
410
|
+
* @returns Result with LockHandle on success, LockError on failure
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* ```typescript
|
|
414
|
+
* const result = await acquireDaemonLock("/run/user/1000/waymark/daemon.lock");
|
|
415
|
+
*
|
|
416
|
+
* if (result.isOk()) {
|
|
417
|
+
* console.log(`Lock acquired for PID ${result.value.pid}`);
|
|
418
|
+
* // ... run daemon ...
|
|
419
|
+
* await releaseDaemonLock(result.value);
|
|
420
|
+
* } else {
|
|
421
|
+
* console.error(`Failed to acquire lock: ${result.error.message}`);
|
|
422
|
+
* }
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
425
|
+
declare function acquireDaemonLock(lockPath: string): Promise<Result2<LockHandle, LockError>>;
|
|
426
|
+
/**
|
|
427
|
+
* Release a daemon lock.
|
|
428
|
+
*
|
|
429
|
+
* Removes the lock file only if the PID inside matches the handle.
|
|
430
|
+
* This prevents accidentally removing a lock acquired by another process.
|
|
431
|
+
*
|
|
432
|
+
* @param handle - Lock handle returned from acquireDaemonLock
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* ```typescript
|
|
436
|
+
* const result = await acquireDaemonLock(lockPath);
|
|
437
|
+
* if (result.isOk()) {
|
|
438
|
+
* try {
|
|
439
|
+
* // ... run daemon ...
|
|
440
|
+
* } finally {
|
|
441
|
+
* await releaseDaemonLock(result.value);
|
|
442
|
+
* }
|
|
443
|
+
* }
|
|
444
|
+
* ```
|
|
445
|
+
*/
|
|
446
|
+
declare function releaseDaemonLock(handle: LockHandle): Promise<void>;
|
|
447
|
+
/**
|
|
448
|
+
* Read the PID from a lock file.
|
|
449
|
+
*
|
|
450
|
+
* @param lockPath - Path to the lock file
|
|
451
|
+
* @returns The PID if valid, undefined otherwise
|
|
452
|
+
*
|
|
453
|
+
* @internal
|
|
454
|
+
*/
|
|
455
|
+
declare function readLockPid(lockPath: string): Promise<number | undefined>;
|
|
456
|
+
/**
|
|
457
|
+
* Create a new daemon instance.
|
|
458
|
+
*
|
|
459
|
+
* The daemon manages its own lifecycle including PID file creation/removal,
|
|
460
|
+
* signal handling for graceful shutdown, and execution of registered
|
|
461
|
+
* shutdown handlers.
|
|
462
|
+
*
|
|
463
|
+
* @param options - Daemon configuration options
|
|
464
|
+
* @returns Daemon instance
|
|
465
|
+
*
|
|
466
|
+
* @example
|
|
467
|
+
* ```typescript
|
|
468
|
+
* const daemon = createDaemon({
|
|
469
|
+
* name: "my-service",
|
|
470
|
+
* pidFile: "/var/run/my-service.pid",
|
|
471
|
+
* shutdownTimeout: 10000,
|
|
472
|
+
* });
|
|
473
|
+
*
|
|
474
|
+
* daemon.onShutdown(async () => {
|
|
475
|
+
* await database.close();
|
|
476
|
+
* });
|
|
477
|
+
*
|
|
478
|
+
* const result = await daemon.start();
|
|
479
|
+
* if (result.isErr()) {
|
|
480
|
+
* console.error("Failed to start:", result.error.message);
|
|
481
|
+
* process.exit(1);
|
|
482
|
+
* }
|
|
483
|
+
* ```
|
|
484
|
+
*/
|
|
485
|
+
declare function createDaemon(options: DaemonOptions): Daemon;
|
|
486
|
+
/**
|
|
487
|
+
* Message handler type for processing incoming IPC messages.
|
|
488
|
+
*
|
|
489
|
+
* Receives a parsed message and returns a response to send back to the client.
|
|
490
|
+
* Throwing an error will result in an error response to the client.
|
|
491
|
+
*/
|
|
492
|
+
type IpcMessageHandler = (message: unknown) => Promise<unknown>;
|
|
493
|
+
/**
|
|
494
|
+
* IPC server interface for receiving messages from clients.
|
|
495
|
+
*
|
|
496
|
+
* The server listens on a Unix socket and processes incoming messages
|
|
497
|
+
* using the registered message handler.
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* ```typescript
|
|
501
|
+
* const server = createIpcServer("/var/run/my-daemon.sock");
|
|
502
|
+
*
|
|
503
|
+
* server.onMessage(async (msg) => {
|
|
504
|
+
* if (msg.type === "status") {
|
|
505
|
+
* return { status: "ok", uptime: process.uptime() };
|
|
506
|
+
* }
|
|
507
|
+
* return { error: "Unknown command" };
|
|
508
|
+
* });
|
|
509
|
+
*
|
|
510
|
+
* await server.listen();
|
|
511
|
+
* ```
|
|
512
|
+
*/
|
|
513
|
+
interface IpcServer {
|
|
514
|
+
/**
|
|
515
|
+
* Start listening for connections on the Unix socket.
|
|
516
|
+
*
|
|
517
|
+
* Creates the socket file and begins accepting client connections.
|
|
518
|
+
* Messages are processed using the handler registered via onMessage.
|
|
519
|
+
*/
|
|
520
|
+
listen(): Promise<void>;
|
|
521
|
+
/**
|
|
522
|
+
* Stop listening and close all connections.
|
|
523
|
+
*
|
|
524
|
+
* Removes the socket file and cleans up resources.
|
|
525
|
+
*/
|
|
526
|
+
close(): Promise<void>;
|
|
527
|
+
/**
|
|
528
|
+
* Register a message handler for incoming messages.
|
|
529
|
+
*
|
|
530
|
+
* Only one handler can be registered. Calling this multiple times
|
|
531
|
+
* replaces the previous handler.
|
|
532
|
+
*
|
|
533
|
+
* @param handler - Function to process incoming messages
|
|
534
|
+
*/
|
|
535
|
+
onMessage(handler: IpcMessageHandler): void;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* IPC client interface for sending messages to a server.
|
|
539
|
+
*
|
|
540
|
+
* The client connects to a Unix socket and can send messages,
|
|
541
|
+
* receiving responses asynchronously.
|
|
542
|
+
*
|
|
543
|
+
* @example
|
|
544
|
+
* ```typescript
|
|
545
|
+
* const client = createIpcClient("/var/run/my-daemon.sock");
|
|
546
|
+
*
|
|
547
|
+
* await client.connect();
|
|
548
|
+
*
|
|
549
|
+
* const response = await client.send<StatusResponse>({ type: "status" });
|
|
550
|
+
* console.log("Daemon uptime:", response.uptime);
|
|
551
|
+
*
|
|
552
|
+
* client.close();
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
interface IpcClient {
|
|
556
|
+
/**
|
|
557
|
+
* Connect to the IPC server.
|
|
558
|
+
*
|
|
559
|
+
* Establishes a connection to the Unix socket. Throws if the
|
|
560
|
+
* server is not available.
|
|
561
|
+
*/
|
|
562
|
+
connect(): Promise<void>;
|
|
563
|
+
/**
|
|
564
|
+
* Send a message and wait for a response.
|
|
565
|
+
*
|
|
566
|
+
* Serializes the message to JSON, sends it to the server, and
|
|
567
|
+
* waits for a response.
|
|
568
|
+
*
|
|
569
|
+
* @typeParam T - Expected response type
|
|
570
|
+
* @param message - Message to send (must be JSON-serializable)
|
|
571
|
+
* @returns Promise resolving to the server's response
|
|
572
|
+
* @throws Error if not connected or communication fails
|
|
573
|
+
*/
|
|
574
|
+
send<T>(message: unknown): Promise<T>;
|
|
575
|
+
/**
|
|
576
|
+
* Close the connection to the server.
|
|
577
|
+
*
|
|
578
|
+
* Can be called multiple times safely.
|
|
579
|
+
*/
|
|
580
|
+
close(): void;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Create an IPC server listening on a Unix socket.
|
|
584
|
+
*
|
|
585
|
+
* @param socketPath - Path to the Unix socket file
|
|
586
|
+
* @returns IpcServer instance
|
|
587
|
+
*
|
|
588
|
+
* @example
|
|
589
|
+
* ```typescript
|
|
590
|
+
* const server = createIpcServer("/var/run/my-daemon.sock");
|
|
591
|
+
*
|
|
592
|
+
* server.onMessage(async (msg) => {
|
|
593
|
+
* return { echo: msg };
|
|
594
|
+
* });
|
|
595
|
+
*
|
|
596
|
+
* await server.listen();
|
|
597
|
+
* // Server is now accepting connections
|
|
598
|
+
* ```
|
|
599
|
+
*/
|
|
600
|
+
declare function createIpcServer(socketPath: string): IpcServer;
|
|
601
|
+
/**
|
|
602
|
+
* Create an IPC client for connecting to a server.
|
|
603
|
+
*
|
|
604
|
+
* @param socketPath - Path to the Unix socket file
|
|
605
|
+
* @returns IpcClient instance
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* ```typescript
|
|
609
|
+
* const client = createIpcClient("/var/run/my-daemon.sock");
|
|
610
|
+
*
|
|
611
|
+
* await client.connect();
|
|
612
|
+
* const response = await client.send({ command: "status" });
|
|
613
|
+
* console.log(response);
|
|
614
|
+
* client.close();
|
|
615
|
+
* ```
|
|
616
|
+
*/
|
|
617
|
+
declare function createIpcClient(socketPath: string): IpcClient;
|
|
618
|
+
import { Result as Result3 } from "@outfitter/contracts";
|
|
619
|
+
/**
|
|
620
|
+
* A single health check definition.
|
|
621
|
+
*
|
|
622
|
+
* Health checks are used to verify that a service or resource is functioning
|
|
623
|
+
* correctly. Each check has a name for identification and a check function
|
|
624
|
+
* that returns a Result indicating success or failure.
|
|
625
|
+
*
|
|
626
|
+
* @example
|
|
627
|
+
* ```typescript
|
|
628
|
+
* const databaseCheck: HealthCheck = {
|
|
629
|
+
* name: "database",
|
|
630
|
+
* check: async () => {
|
|
631
|
+
* try {
|
|
632
|
+
* await db.ping();
|
|
633
|
+
* return Result.ok(undefined);
|
|
634
|
+
* } catch (error) {
|
|
635
|
+
* return Result.err(error);
|
|
636
|
+
* }
|
|
637
|
+
* },
|
|
638
|
+
* };
|
|
639
|
+
* ```
|
|
640
|
+
*/
|
|
641
|
+
interface HealthCheck {
|
|
642
|
+
/**
|
|
643
|
+
* Unique name identifying this health check.
|
|
644
|
+
* Used as the key in the HealthStatus.checks record.
|
|
645
|
+
*/
|
|
646
|
+
name: string;
|
|
647
|
+
/**
|
|
648
|
+
* Function that performs the health check.
|
|
649
|
+
*
|
|
650
|
+
* Should return Result.ok(undefined) if healthy, or Result.err(error)
|
|
651
|
+
* with details about the failure.
|
|
652
|
+
*/
|
|
653
|
+
check(): Promise<Result3<void, Error>>;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Result of an individual health check.
|
|
657
|
+
*
|
|
658
|
+
* Contains the healthy/unhealthy status and an optional message
|
|
659
|
+
* providing more details (typically the error message on failure).
|
|
660
|
+
*/
|
|
661
|
+
interface HealthCheckResult {
|
|
662
|
+
/** Whether this check passed (true) or failed (false) */
|
|
663
|
+
healthy: boolean;
|
|
664
|
+
/** Optional message, typically the error message on failure */
|
|
665
|
+
message?: string;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Aggregated health status from all registered checks.
|
|
669
|
+
*
|
|
670
|
+
* The overall healthy status is true only if ALL individual checks pass.
|
|
671
|
+
* Includes the uptime of the health checker in seconds.
|
|
672
|
+
*
|
|
673
|
+
* @example
|
|
674
|
+
* ```typescript
|
|
675
|
+
* const status: HealthStatus = {
|
|
676
|
+
* healthy: false,
|
|
677
|
+
* checks: {
|
|
678
|
+
* database: { healthy: true },
|
|
679
|
+
* cache: { healthy: false, message: "Connection refused" },
|
|
680
|
+
* },
|
|
681
|
+
* uptime: 3600,
|
|
682
|
+
* };
|
|
683
|
+
* ```
|
|
684
|
+
*/
|
|
685
|
+
interface HealthStatus {
|
|
686
|
+
/** Overall health status - true only if ALL checks pass */
|
|
687
|
+
healthy: boolean;
|
|
688
|
+
/** Individual check results keyed by check name */
|
|
689
|
+
checks: Record<string, HealthCheckResult>;
|
|
690
|
+
/** Uptime in seconds since the health checker was created */
|
|
691
|
+
uptime: number;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Health checker interface for managing and running health checks.
|
|
695
|
+
*
|
|
696
|
+
* Provides methods to run all registered checks and get the aggregated
|
|
697
|
+
* health status, as well as dynamically registering new checks at runtime.
|
|
698
|
+
*
|
|
699
|
+
* @example
|
|
700
|
+
* ```typescript
|
|
701
|
+
* const checker = createHealthChecker([
|
|
702
|
+
* { name: "db", check: checkDatabase },
|
|
703
|
+
* { name: "cache", check: checkCache },
|
|
704
|
+
* ]);
|
|
705
|
+
*
|
|
706
|
+
* // Later, add more checks
|
|
707
|
+
* checker.register({ name: "queue", check: checkQueue });
|
|
708
|
+
*
|
|
709
|
+
* // Get health status
|
|
710
|
+
* const status = await checker.check();
|
|
711
|
+
* console.log("Healthy:", status.healthy);
|
|
712
|
+
* ```
|
|
713
|
+
*/
|
|
714
|
+
interface HealthChecker {
|
|
715
|
+
/**
|
|
716
|
+
* Run all registered health checks and return aggregated status.
|
|
717
|
+
*
|
|
718
|
+
* Checks are run in parallel for efficiency. The overall healthy
|
|
719
|
+
* status is true only if all individual checks pass.
|
|
720
|
+
*
|
|
721
|
+
* @returns Aggregated health status
|
|
722
|
+
*/
|
|
723
|
+
check(): Promise<HealthStatus>;
|
|
724
|
+
/**
|
|
725
|
+
* Register a new health check at runtime.
|
|
726
|
+
*
|
|
727
|
+
* The check will be included in all subsequent calls to check().
|
|
728
|
+
*
|
|
729
|
+
* @param check - Health check to register
|
|
730
|
+
*/
|
|
731
|
+
register(check: HealthCheck): void;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Create a health checker with initial checks.
|
|
735
|
+
*
|
|
736
|
+
* The health checker runs all registered checks in parallel and aggregates
|
|
737
|
+
* their results. Individual check failures are isolated and don't prevent
|
|
738
|
+
* other checks from running.
|
|
739
|
+
*
|
|
740
|
+
* @param checks - Initial health checks to register
|
|
741
|
+
* @returns HealthChecker instance
|
|
742
|
+
*
|
|
743
|
+
* @example
|
|
744
|
+
* ```typescript
|
|
745
|
+
* const checker = createHealthChecker([
|
|
746
|
+
* {
|
|
747
|
+
* name: "database",
|
|
748
|
+
* check: async () => {
|
|
749
|
+
* await db.ping();
|
|
750
|
+
* return Result.ok(undefined);
|
|
751
|
+
* },
|
|
752
|
+
* },
|
|
753
|
+
* {
|
|
754
|
+
* name: "cache",
|
|
755
|
+
* check: async () => {
|
|
756
|
+
* await redis.ping();
|
|
757
|
+
* return Result.ok(undefined);
|
|
758
|
+
* },
|
|
759
|
+
* },
|
|
760
|
+
* ]);
|
|
761
|
+
*
|
|
762
|
+
* // Run health checks
|
|
763
|
+
* const status = await checker.check();
|
|
764
|
+
*
|
|
765
|
+
* if (!status.healthy) {
|
|
766
|
+
* console.error("Service unhealthy:", status.checks);
|
|
767
|
+
* }
|
|
768
|
+
* ```
|
|
769
|
+
*/
|
|
770
|
+
declare function createHealthChecker(checks: HealthCheck[]): HealthChecker;
|
|
771
|
+
export { releaseDaemonLock, readLockPid, isUnixPlatform, isProcessAlive, isDaemonAlive, getSocketPath, getPidPath, getLockPath, getDaemonDir, createIpcServer, createIpcClient, createHealthChecker, createDaemon, acquireDaemonLock, StaleSocketError, ShutdownHandler, ProtocolError, LockHandle, LockError, IpcServer, IpcMessageHandler, IpcClient, HealthStatus, HealthChecker, HealthCheckResult, HealthCheck, DaemonState, DaemonOptions, DaemonErrorCode, DaemonError, DaemonConnectionError, Daemon, ConnectionTimeoutError, ConnectionRefusedError };
|