@getdashfy/server 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1225 @@
1
+ import path, { extname } from 'path';
2
+ import cors from '@fastify/cors';
3
+ import fastifyStatic from '@fastify/static';
4
+ import { getErrorMessage } from '@getdashfy/utils';
5
+ import { watch } from 'chokidar';
6
+ import Fastify from 'fastify';
7
+ import { Server } from 'socket.io';
8
+ import { EventEmitter } from 'events';
9
+ import { request as request$1 } from 'undici';
10
+ import { readFile } from 'fs/promises';
11
+ import { parse } from 'yaml';
12
+ import { z } from 'zod';
13
+ import pino from 'pino';
14
+
15
+ // src/dashfy.ts
16
+
17
+ // ../types/dist/index.js
18
+ var WebSocketEvent = /* @__PURE__ */ ((WebSocketEvent2) => {
19
+ WebSocketEvent2["CONNECT"] = "connect";
20
+ WebSocketEvent2["DISCONNECT"] = "disconnect";
21
+ WebSocketEvent2["ERROR"] = "error";
22
+ WebSocketEvent2["CONFIGURATION"] = "configuration";
23
+ WebSocketEvent2["RECONNECT"] = "reconnect";
24
+ WebSocketEvent2["RECONNECT_ATTEMPT"] = "reconnect_attempt";
25
+ WebSocketEvent2["RECONNECT_ERROR"] = "reconnect_error";
26
+ WebSocketEvent2["RECONNECT_FAILED"] = "reconnect_failed";
27
+ WebSocketEvent2["API_SUBSCRIPTION"] = "api.subscription";
28
+ WebSocketEvent2["API_UNSUBSCRIPTION"] = "api.unsubscription";
29
+ WebSocketEvent2["API_DATA"] = "api.data";
30
+ WebSocketEvent2["API_ERROR"] = "api.error";
31
+ return WebSocketEvent2;
32
+ })(WebSocketEvent || {});
33
+
34
+ // src/constants.ts
35
+ var DEFAULT_PORT = 5001;
36
+ var DEFAULT_HOST = "0.0.0.0";
37
+ var DEFAULT_POLL_INTERVAL = 15e3;
38
+ var DEFAULT_PUSH_INTERVAL = 2e3;
39
+ var SOCKET_PING_TIMEOUT = 6e4;
40
+ var SOCKET_PING_INTERVAL = 25e3;
41
+ var DEFAULT_REQUEST_TIMEOUT = 1e4;
42
+ function createPushIntervalFactory(logger, prefix) {
43
+ return (options = {}) => {
44
+ const { interval = DEFAULT_PUSH_INTERVAL } = options;
45
+ const intervals = /* @__PURE__ */ new Map();
46
+ const logKey = (key) => prefix ? `${prefix}.${key}` : key;
47
+ return (key, callback, fetchFn) => {
48
+ const stop = () => {
49
+ const existing = intervals.get(key);
50
+ if (!existing) {
51
+ return;
52
+ }
53
+ clearInterval(existing);
54
+ intervals.delete(key);
55
+ logger.info(`[${logKey(key)}] Stopped push interval`);
56
+ };
57
+ if (intervals.has(key)) {
58
+ return stop;
59
+ }
60
+ const pushData = async () => {
61
+ try {
62
+ const data = await fetchFn();
63
+ callback(data);
64
+ } catch (error) {
65
+ logger.error(`[${logKey(key)}] Error: ${getErrorMessage(error)}`);
66
+ }
67
+ };
68
+ void pushData();
69
+ const timer = setInterval(() => {
70
+ void pushData();
71
+ }, interval);
72
+ intervals.set(key, timer);
73
+ logger.info(`[${logKey(key)}] Started push interval (${interval}ms)`);
74
+ return stop;
75
+ };
76
+ };
77
+ }
78
+ async function request(options) {
79
+ const { url, method = "GET", headers = {}, body, timeout = DEFAULT_REQUEST_TIMEOUT } = options;
80
+ const requestHeaders = {
81
+ "Content-Type": "application/json",
82
+ Accept: "application/json",
83
+ ...headers
84
+ };
85
+ const response = await request$1(url, {
86
+ method,
87
+ headers: requestHeaders,
88
+ body: body ? JSON.stringify(body) : void 0,
89
+ headersTimeout: timeout,
90
+ bodyTimeout: timeout
91
+ });
92
+ if (response.statusCode >= 400) {
93
+ const errorBody = await response.body.text().catch(() => "");
94
+ throw new Error(`HTTP ${response.statusCode}: ${errorBody || "Request failed"}`);
95
+ }
96
+ return response.body.json();
97
+ }
98
+
99
+ // src/bus.ts
100
+ var Bus = class extends EventEmitter {
101
+ logger;
102
+ pollInterval;
103
+ apis = /* @__PURE__ */ new Map();
104
+ clients = /* @__PURE__ */ new Map();
105
+ subscriptions = /* @__PURE__ */ new Map();
106
+ /**
107
+ * Creates a new Bus instance.
108
+ *
109
+ * @param options - Bus options
110
+ * @param options.logger - Logger instance
111
+ * @param options.pollInterval - Poll interval in milliseconds (default: 15_000)
112
+ *
113
+ * @remarks
114
+ * Changing `pollInterval` after construction only affects subscriptions created afterwards;
115
+ * timers for already-active poll subscriptions keep their original interval until recreated.
116
+ */
117
+ constructor(options) {
118
+ super();
119
+ this.logger = options.logger;
120
+ this.pollInterval = options.pollInterval ?? DEFAULT_POLL_INTERVAL;
121
+ }
122
+ /**
123
+ * Register a new API with the bus.
124
+ *
125
+ * An API is an object composed of various methods that can be called by clients.
126
+ * APIs can operate in two modes:
127
+ * - `poll`: Methods are called periodically at a fixed interval
128
+ * - `push`: Methods push data to clients when available
129
+ *
130
+ * @param id - Unique identifier for the API
131
+ * @param apiRegistration - Function that returns an object with API methods
132
+ * @param mode - API mode: 'poll' (default) or 'push'
133
+ * @throws {Error} If the API mode is not 'poll' or 'push'
134
+ * @throws {Error} If an API with the same ID is already registered
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * bus.registerApi('weather', ({ logger, request }) => ({
139
+ * getCurrentWeather: async () => {
140
+ * return request({ url: 'https://api.weather.com/current' })
141
+ * }
142
+ * }), 'poll')
143
+ * ```
144
+ */
145
+ registerApi(id, apiRegistration, mode = "poll") {
146
+ if (mode !== "poll" && mode !== "push") {
147
+ const errorMessage = `Invalid API mode: ${String(mode)}. Must be 'poll' or 'push'`;
148
+ this.logger.error(errorMessage);
149
+ throw new Error(errorMessage);
150
+ }
151
+ if (this.apis.has(id)) {
152
+ const errorMessage = `API '${id}' is already registered`;
153
+ this.logger.error(errorMessage);
154
+ throw new Error(errorMessage);
155
+ }
156
+ const logger = this.logger.child({ api: id });
157
+ const methods = apiRegistration({
158
+ logger,
159
+ request,
160
+ createPushInterval: createPushIntervalFactory(logger, id)
161
+ });
162
+ this.apis.set(id, { mode, methods });
163
+ this.logger.info(`Registered API '${id}' (mode: ${mode})`);
164
+ }
165
+ /**
166
+ * Get all registered API identifiers.
167
+ *
168
+ * Returns an array of all API IDs that have been registered with the bus,
169
+ * sorted alphabetically. Useful for debugging, introspection, or displaying
170
+ * available APIs.
171
+ *
172
+ * @returns Array of registered API IDs in alphabetical order
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * const apis = bus.listApis()
177
+ * console.log(apis) // ['github', 'news', 'weather']
178
+ *
179
+ * // Check if a specific API is registered
180
+ * if (bus.listApis().includes('github')) {
181
+ * console.log('GitHub API is available')
182
+ * }
183
+ * ```
184
+ */
185
+ listApis() {
186
+ return Array.from(this.apis.keys()).sort((a, b) => a.localeCompare(b));
187
+ }
188
+ /**
189
+ * Register a new client connection with the bus.
190
+ *
191
+ * This should be called when a client establishes a WebSocket connection. The client
192
+ * is tracked with its socket, an empty subscriptions set, and a connection timestamp.
193
+ * Once registered, the client can subscribe to API methods.
194
+ *
195
+ * @param socket - Socket.IO socket instance representing the connected client
196
+ * @throws {Error} If a client with the same socket ID is already registered
197
+ *
198
+ * @example
199
+ * ```ts
200
+ * // In your Socket.IO connection handler
201
+ * io.on('connection', (socket) => {
202
+ * bus.addClient(socket)
203
+ *
204
+ * socket.on('disconnect', () => {
205
+ * bus.removeClient(socket.id)
206
+ * })
207
+ * })
208
+ * ```
209
+ */
210
+ addClient(socket) {
211
+ if (this.clients.has(socket.id)) {
212
+ const errorMessage = `Client with id ${socket.id} already registered`;
213
+ this.logger.error(errorMessage);
214
+ throw new Error(errorMessage);
215
+ }
216
+ this.clients.set(socket.id, {
217
+ socket,
218
+ subscriptions: /* @__PURE__ */ new Set(),
219
+ connectedAt: /* @__PURE__ */ new Date()
220
+ });
221
+ this.logger.info(`Client with id ${socket.id} connected`);
222
+ }
223
+ /**
224
+ * Remove a client and clean up all associated resources.
225
+ *
226
+ * This method removes the client from all subscriptions it was part of. If a subscription
227
+ * has no remaining clients after removal, the subscription is cleaned up and any polling
228
+ * timers are cleared. This should be called when a client disconnects to prevent memory leaks.
229
+ *
230
+ * @param clientId - The unique identifier of the client to remove (socket.id)
231
+ * @throws {Error} If the client is not found
232
+ *
233
+ * @example
234
+ * ```ts
235
+ * // In your Socket.IO disconnect handler
236
+ * socket.on('disconnect', () => {
237
+ * bus.removeClient(socket.id)
238
+ * console.log(`Client ${socket.id} removed`)
239
+ * })
240
+ * ```
241
+ */
242
+ removeClient(clientId) {
243
+ const clientInfo = this.clients.get(clientId);
244
+ if (!clientInfo) {
245
+ const errorMessage = `Client with id ${clientId} not found`;
246
+ this.logger.error(errorMessage);
247
+ throw new Error(errorMessage);
248
+ }
249
+ for (const subscriptionId of clientInfo.subscriptions) {
250
+ const subscription = this.subscriptions.get(subscriptionId);
251
+ if (!subscription) {
252
+ continue;
253
+ }
254
+ subscription.clients.delete(clientId);
255
+ if (subscription.clients.size === 0) {
256
+ this.teardownSubscription(subscriptionId);
257
+ }
258
+ }
259
+ this.clients.delete(clientId);
260
+ this.logger.info(`Client with id ${clientId} disconnected`);
261
+ }
262
+ /**
263
+ * Get the current number of connected clients.
264
+ *
265
+ * Returns the real-time count of active client connections. Useful for monitoring,
266
+ * health checks, or displaying connection statistics.
267
+ *
268
+ * @returns Number of currently connected clients
269
+ *
270
+ * @example
271
+ * ```ts
272
+ * // Display connection stats
273
+ * const count = bus.clientCount()
274
+ * console.log(`${count} client(s) connected`)
275
+ *
276
+ * // Health check endpoint
277
+ * app.get('/health', (req, res) => {
278
+ * res.json({
279
+ * status: 'ok',
280
+ * clients: bus.clientCount()
281
+ * })
282
+ * })
283
+ * ```
284
+ */
285
+ clientCount() {
286
+ return this.clients.size;
287
+ }
288
+ /**
289
+ * Subscribe a client to an API method call.
290
+ *
291
+ * This creates or reuses a subscription for the specified API method and adds the client
292
+ * to receive updates. For poll mode APIs, this starts periodic polling. For push mode APIs,
293
+ * this sets up the push producer. If cached data exists, it's immediately sent to the client.
294
+ *
295
+ * Routing is resolved from `subscription.api` (the registered API) and `subscription.endpoint`
296
+ * (the method on that API). The `subscription.id` is used as the key for deduping subscriptions,
297
+ * caching, and client messages; by convention it is `${api}.${endpoint}` (e.g. 'github.repos'),
298
+ * but this format is not enforced.
299
+ *
300
+ * @param clientId - The unique identifier of the connected client
301
+ * @param subscription - Subscription details including `id`, `api`, `endpoint`, and optional `params`
302
+ * @throws {Error} If the API ID (`subscription.api`) is empty or undefined
303
+ * @throws {Error} If the endpoint (`subscription.endpoint`) is empty or undefined
304
+ * @throws {Error} If the API is not registered
305
+ * @throws {Error} If the API method does not exist or is not a function
306
+ *
307
+ * @remarks
308
+ * If the client is not registered, the call returns silently (logging a warning) instead of
309
+ * throwing. If a subscription with the same `id` already exists, the incoming `params` are
310
+ * ignored: the subscription keeps the `params` supplied by its first subscriber.
311
+ *
312
+ * @example
313
+ * ```ts
314
+ * // Subscribe to a GitHub API method
315
+ * bus.subscribe('client-123', {
316
+ * id: 'github.repos',
317
+ * api: 'github',
318
+ * endpoint: 'repos',
319
+ * params: { user: 'octocat' }
320
+ * })
321
+ *
322
+ * // Subscribe without parameters
323
+ * bus.subscribe('client-456', {
324
+ * id: 'weather.current',
325
+ * api: 'weather',
326
+ * endpoint: 'current'
327
+ * })
328
+ * ```
329
+ */
330
+ subscribe(clientId, subscription) {
331
+ const clientInfo = this.clients.get(clientId);
332
+ if (!clientInfo) {
333
+ this.logger.warn(`Cannot subscribe: client with id ${clientId} not found`);
334
+ return;
335
+ }
336
+ const subscriptionId = subscription.id;
337
+ const apiId = subscription.api;
338
+ const methodName = subscription.endpoint;
339
+ if (!apiId) {
340
+ const errorMessage = `Invalid API ID in subscription: ${subscriptionId}`;
341
+ this.logger.error(errorMessage);
342
+ throw new Error(errorMessage);
343
+ }
344
+ if (!methodName) {
345
+ const errorMessage = `Invalid endpoint in subscription: ${subscriptionId}`;
346
+ this.logger.error(errorMessage);
347
+ throw new Error(errorMessage);
348
+ }
349
+ const api = this.apis.get(apiId);
350
+ if (!api) {
351
+ const errorMessage = `API not found: ${apiId}`;
352
+ this.logger.error(errorMessage);
353
+ throw new Error(errorMessage);
354
+ }
355
+ const method = api.methods[methodName];
356
+ if (!method || typeof method !== "function") {
357
+ const errorMessage = `API method not found: ${apiId}.${methodName}`;
358
+ this.logger.error(errorMessage);
359
+ throw new Error(errorMessage);
360
+ }
361
+ if (!this.subscriptions.has(subscriptionId)) {
362
+ this.subscriptions.set(subscriptionId, {
363
+ id: subscriptionId,
364
+ api: apiId,
365
+ endpoint: methodName,
366
+ params: subscription.params,
367
+ clients: /* @__PURE__ */ new Set()
368
+ });
369
+ this.logger.info(`Created subscription ${subscriptionId}`);
370
+ if (api.mode === "poll") {
371
+ void this.processApiCall(subscriptionId, method, subscription.params);
372
+ const timer = setInterval(() => {
373
+ void this.processApiCall(subscriptionId, method, subscription.params);
374
+ }, this.pollInterval);
375
+ const sub2 = this.subscriptions.get(subscriptionId);
376
+ if (sub2) {
377
+ sub2.timer = timer;
378
+ }
379
+ this.logger.info(
380
+ `Started polling for subscription ${subscriptionId} every ${this.pollInterval}ms`
381
+ );
382
+ } else if (api.mode === "push") {
383
+ const producerResult = method((data) => {
384
+ const message = { id: subscriptionId, data };
385
+ const sub2 = this.subscriptions.get(subscriptionId);
386
+ if (sub2) {
387
+ sub2.cached = message;
388
+ }
389
+ this.send(subscriptionId, message, WebSocketEvent.API_DATA);
390
+ }, subscription.params);
391
+ Promise.resolve(producerResult).then((result) => {
392
+ if (typeof result !== "function") {
393
+ return;
394
+ }
395
+ const dispose = result;
396
+ const sub2 = this.subscriptions.get(subscriptionId);
397
+ if (sub2) {
398
+ sub2.pushDispose = dispose;
399
+ } else {
400
+ dispose();
401
+ }
402
+ }).catch((error) => {
403
+ this.logger.error(
404
+ `Push producer for ${subscriptionId} failed: ${getErrorMessage(error)}`
405
+ );
406
+ });
407
+ this.logger.info(`Created push producer for subscription ${subscriptionId}`);
408
+ }
409
+ }
410
+ const sub = this.subscriptions.get(subscriptionId);
411
+ if (!sub) {
412
+ this.logger.warn(`Cannot subscribe: subscription ${subscriptionId} not found`);
413
+ return;
414
+ }
415
+ sub.clients.add(clientId);
416
+ clientInfo.subscriptions.add(subscriptionId);
417
+ if (sub.cached) {
418
+ this.logger.info(
419
+ `Sending cached data to client with id ${clientId} for subscription ${subscriptionId}`
420
+ );
421
+ clientInfo.socket.emit(WebSocketEvent.API_DATA, sub.cached);
422
+ }
423
+ }
424
+ /**
425
+ * Unsubscribe a client from an API method subscription.
426
+ *
427
+ * This removes the client from the specified subscription. If this was the last client
428
+ * subscribed to this API method, the subscription is cleaned up entirely and any polling
429
+ * timers are cleared to prevent unnecessary API calls and memory leaks.
430
+ *
431
+ * If the client or subscription doesn't exist, the method returns silently without error.
432
+ * This is safe to call multiple times or for non-existent subscriptions.
433
+ *
434
+ * @param clientId - The unique identifier of the client to unsubscribe
435
+ * @param subscriptionId - The subscription ID to unsubscribe from (matches `subscription.id`, conventionally 'api.method')
436
+ *
437
+ * @example
438
+ * ```ts
439
+ * // Unsubscribe from a specific API
440
+ * socket.on('unsubscribe', (subscriptionId) => {
441
+ * bus.unsubscribe(socket.id, subscriptionId)
442
+ * })
443
+ *
444
+ * // Unsubscribe when widget is removed
445
+ * bus.unsubscribe('client-123', 'github.repos')
446
+ * ```
447
+ */
448
+ unsubscribe(clientId, subscriptionId) {
449
+ const clientInfo = this.clients.get(clientId);
450
+ if (!clientInfo) {
451
+ this.logger.warn(`Cannot unsubscribe: client with id ${clientId} not found`);
452
+ return;
453
+ }
454
+ const subscription = this.subscriptions.get(subscriptionId);
455
+ if (!subscription) {
456
+ this.logger.warn(`Cannot unsubscribe: subscription ${subscriptionId} not found`);
457
+ return;
458
+ }
459
+ subscription.clients.delete(clientId);
460
+ clientInfo.subscriptions.delete(subscriptionId);
461
+ if (subscription.clients.size === 0) {
462
+ this.teardownSubscription(subscriptionId);
463
+ }
464
+ }
465
+ /**
466
+ * Get detailed information about all active subscriptions.
467
+ *
468
+ * Returns an array of subscription metadata including subscription IDs, client counts,
469
+ * cache status, and timer status. Useful for monitoring, debugging, and displaying
470
+ * real-time statistics about API subscriptions.
471
+ *
472
+ * @returns Array of subscription info objects, each containing:
473
+ * - `id`: Subscription identifier (conventionally 'api.method')
474
+ * - `clientCount`: Number of clients subscribed to this API method
475
+ * - `hasCachedData`: Whether cached data is available for immediate delivery
476
+ * - `hasTimer`: Whether a polling timer is active (poll mode only)
477
+ *
478
+ * @remarks
479
+ * Subscriptions are torn down automatically when their last client leaves, so a
480
+ * `clientCount` of `0` should not appear during normal operation.
481
+ *
482
+ * @example
483
+ * ```ts
484
+ * // Get all subscription stats
485
+ * const subs = bus.getSubscriptionsInfo()
486
+ * console.log(subs)
487
+ * // [
488
+ * // {
489
+ * // id: 'github.repos',
490
+ * // clientCount: 3,
491
+ * // hasCachedData: true,
492
+ * // hasTimer: true
493
+ * // },
494
+ * // {
495
+ * // id: 'weather.current',
496
+ * // clientCount: 1,
497
+ * // hasCachedData: false,
498
+ * // hasTimer: false
499
+ * // }
500
+ * // ]
501
+ *
502
+ * // Debug endpoint for monitoring
503
+ * app.get('/debug/subscriptions', (req, res) => {
504
+ * res.json({
505
+ * total: bus.getSubscriptionsInfo().length,
506
+ * subscriptions: bus.getSubscriptionsInfo()
507
+ * })
508
+ * })
509
+ * ```
510
+ */
511
+ getSubscriptionsInfo() {
512
+ return Array.from(this.subscriptions.entries()).map(([id, sub]) => ({
513
+ id,
514
+ clientCount: sub.clients.size,
515
+ hasCachedData: !!sub.cached,
516
+ hasTimer: !!sub.timer
517
+ }));
518
+ }
519
+ /**
520
+ * Execute an API method call and distribute results to subscribed clients.
521
+ *
522
+ * This internal method is called either periodically (for poll mode) or on-demand (for push mode).
523
+ * It executes the API method with the provided parameters, caches the result, and sends the data
524
+ * to all clients subscribed to this API method. If the API call fails, an error message is sent
525
+ * instead and logged.
526
+ *
527
+ * @param subscriptionId - The subscription identifier (conventionally 'api.method')
528
+ * @param method - The API method function to execute
529
+ * @param params - Optional parameters to pass to the API method
530
+ *
531
+ * @remarks
532
+ * This method handles:
533
+ * - Executing the API method with error handling
534
+ * - Caching successful responses for immediate delivery to new subscribers
535
+ * - Broadcasting data to all subscribed clients via 'api.data' event
536
+ * - Broadcasting errors to all subscribed clients via 'api.error' event
537
+ * - Logging execution details for debugging
538
+ *
539
+ * @internal
540
+ */
541
+ async processApiCall(subscriptionId, method, params) {
542
+ try {
543
+ this.logger.debug(`Calling subscription ${subscriptionId}`);
544
+ const data = await method(params);
545
+ const message = { id: subscriptionId, data };
546
+ const subscription = this.subscriptions.get(subscriptionId);
547
+ if (subscription) {
548
+ this.logger.info(`Caching response for subscription ${subscriptionId}`);
549
+ subscription.cached = message;
550
+ }
551
+ this.send(subscriptionId, message, WebSocketEvent.API_DATA);
552
+ } catch (error) {
553
+ const errorMessage = {
554
+ id: subscriptionId,
555
+ error: {
556
+ message: getErrorMessage(error)
557
+ }
558
+ };
559
+ this.logger.error({ err: error, subscriptionId }, "Subscription execution error");
560
+ this.send(subscriptionId, errorMessage, WebSocketEvent.API_ERROR);
561
+ }
562
+ }
563
+ /**
564
+ * Broadcast data to all clients subscribed to a specific API method.
565
+ *
566
+ * This internal method iterates through all clients subscribed to the given subscription
567
+ * and emits the data via Socket.IO. It safely handles cases where clients may have
568
+ * disconnected but haven't been cleaned up yet.
569
+ *
570
+ * @param subscriptionId - The subscription identifier (conventionally 'api.method')
571
+ * @param data - The data payload to send (API response or error message)
572
+ * @param type - The message type: 'api.data' for successful responses, 'api.error' for errors
573
+ *
574
+ * @remarks
575
+ * This method:
576
+ * - Returns silently if the subscription doesn't exist (already cleaned up)
577
+ * - Skips clients that no longer exist (race condition handling)
578
+ * - Uses Socket.IO's emit to send data to each client
579
+ *
580
+ * @internal
581
+ */
582
+ send(subscriptionId, data, type) {
583
+ const subscription = this.subscriptions.get(subscriptionId);
584
+ if (!subscription) {
585
+ this.logger.warn(`Cannot send data to subscription ${subscriptionId}: not found`);
586
+ return;
587
+ }
588
+ for (const clientId of subscription.clients) {
589
+ const clientInfo = this.clients.get(clientId);
590
+ if (clientInfo) {
591
+ clientInfo.socket.emit(type, data);
592
+ }
593
+ }
594
+ }
595
+ /**
596
+ * Tear down a subscription that has no clients remaining.
597
+ *
598
+ * Clears any poll-mode timer and invokes the push-mode disposer (if either is set),
599
+ * then removes the subscription entry. Safe to call when the subscription is missing.
600
+ *
601
+ * @param subscriptionId - The subscription identifier (conventionally 'api.method')
602
+ *
603
+ * @internal
604
+ */
605
+ teardownSubscription(subscriptionId) {
606
+ const subscription = this.subscriptions.get(subscriptionId);
607
+ if (!subscription) {
608
+ return;
609
+ }
610
+ if (subscription.timer) {
611
+ clearInterval(subscription.timer);
612
+ }
613
+ if (subscription.pushDispose) {
614
+ try {
615
+ subscription.pushDispose();
616
+ } catch (error) {
617
+ this.logger.error(`Push dispose for ${subscriptionId} failed: ${getErrorMessage(error)}`);
618
+ }
619
+ }
620
+ this.subscriptions.delete(subscriptionId);
621
+ this.logger.info(`Removed subscription ${subscriptionId} (no clients)`);
622
+ }
623
+ };
624
+ var WidgetConfigSchema = z.object({
625
+ extension: z.string(),
626
+ widget: z.string(),
627
+ title: z.string().optional(),
628
+ columns: z.number().positive(),
629
+ rows: z.number().positive(),
630
+ x: z.number().nonnegative(),
631
+ y: z.number().nonnegative()
632
+ }).passthrough();
633
+ var DashboardConfigSchema = z.object({
634
+ name: z.string().optional(),
635
+ title: z.string().optional(),
636
+ columns: z.number().positive(),
637
+ rows: z.number().positive(),
638
+ widgets: z.array(WidgetConfigSchema)
639
+ });
640
+ var DashfyConfigSchema = z.object({
641
+ port: z.number().positive().default(DEFAULT_PORT),
642
+ host: z.string().default(DEFAULT_HOST),
643
+ baseDir: z.string().optional(),
644
+ theme: z.string().optional(),
645
+ rotationDuration: z.number().positive().optional(),
646
+ dashboards: z.array(DashboardConfigSchema).min(1),
647
+ apis: z.object({
648
+ pollInterval: z.number().positive().optional()
649
+ }).passthrough().optional()
650
+ });
651
+ var ConfigError = class extends Error {
652
+ constructor(message) {
653
+ super(message);
654
+ this.name = "ConfigError";
655
+ }
656
+ };
657
+ function detectConfigFormat(filePath) {
658
+ const extension = extname(filePath).toLowerCase();
659
+ switch (extension) {
660
+ case ".json":
661
+ return "json" /* JSON */;
662
+ case ".yml":
663
+ case ".yaml":
664
+ return "yaml" /* YAML */;
665
+ default:
666
+ throw new ConfigError(
667
+ `Unsupported configuration file format: ${extension}. Supported formats: .json, .yml, .yaml`
668
+ );
669
+ }
670
+ }
671
+ function parseConfigContent(content, format) {
672
+ try {
673
+ switch (format) {
674
+ case "json" /* JSON */:
675
+ return JSON.parse(content);
676
+ case "yaml" /* YAML */:
677
+ return parse(content);
678
+ /* v8 ignore next 3 */
679
+ default:
680
+ throw new Error(`Unsupported format: ${format}`);
681
+ }
682
+ } catch (error) {
683
+ throw new ConfigError(
684
+ `Failed to parse ${format.toUpperCase()} content: ${getErrorMessage(error)}`
685
+ );
686
+ }
687
+ }
688
+ async function loadConfig(path2) {
689
+ try {
690
+ const format = detectConfigFormat(path2);
691
+ const content = await readFile(path2, "utf-8");
692
+ const rawConfig = parseConfigContent(content, format);
693
+ const config = DashfyConfigSchema.parse(rawConfig);
694
+ return config;
695
+ } catch (error) {
696
+ if (error instanceof z.ZodError) {
697
+ const issues = error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
698
+ throw new ConfigError(`Configuration validation failed:
699
+ ${issues}`);
700
+ }
701
+ if (error instanceof ConfigError) {
702
+ throw error;
703
+ }
704
+ throw new Error(`Failed to load configuration from "${path2}": ${getErrorMessage(error)}`);
705
+ }
706
+ }
707
+ function createLogger(options) {
708
+ const isDevelopment = process.env.NODE_ENV !== "production";
709
+ return pino({
710
+ level: process.env.LOG_LEVEL ?? (isDevelopment ? "info" : "warn"),
711
+ transport: isDevelopment ? {
712
+ target: "pino-pretty",
713
+ options: {
714
+ colorize: true,
715
+ translateTime: "HH:MM:ss",
716
+ ignore: "pid,hostname",
717
+ singleLine: false
718
+ }
719
+ } : void 0,
720
+ ...options
721
+ });
722
+ }
723
+
724
+ // src/dashfy.ts
725
+ var Dashfy = class {
726
+ app;
727
+ io;
728
+ bus;
729
+ config;
730
+ logger;
731
+ configPath;
732
+ /**
733
+ * Creates a new Dashfy instance.
734
+ *
735
+ * @param options - Dashfy options
736
+ * @param options.logger - Logger instance
737
+ * @param options.app - Existing Fastify instance. When provided,
738
+ * Dashfy will use this instance instead of creating a new one,
739
+ * allowing integration with existing Fastify middleware and routes.
740
+ *
741
+ * @example
742
+ * ```ts
743
+ * // Integrate Dashfy with existing Fastify app
744
+ * import Fastify from 'fastify'
745
+ * import { Dashfy } from '@getdashfy/server'
746
+ *
747
+ * const app = Fastify()
748
+ *
749
+ * // Add your custom routes
750
+ * app.get('/custom-api', async (request, reply) => {
751
+ * return { message: 'Custom endpoint' }
752
+ * })
753
+ *
754
+ * // Initialize Dashfy with existing app
755
+ * const dashfy = new Dashfy({ app })
756
+ * await dashfy.configureFromFile('./dashfy.config.json')
757
+ * await dashfy.start()
758
+ * ```
759
+ */
760
+ constructor(options = {}) {
761
+ this.logger = options.logger ?? createLogger();
762
+ if (options.app) {
763
+ this.app = options.app;
764
+ } else {
765
+ this.app = Fastify({
766
+ logger: options.logger ? false : true,
767
+ disableRequestLogging: true
768
+ });
769
+ }
770
+ this.bus = new Bus({
771
+ logger: this.logger.child({ component: "bus" })
772
+ });
773
+ this.registerCoreApi();
774
+ }
775
+ /**
776
+ * Configures the Dashfy server with the provided configuration object.
777
+ *
778
+ * This method accepts a configuration object and applies it to the server instance.
779
+ * If a global poll interval is specified in the configuration, it will be applied
780
+ * to the message bus for all poll-mode APIs.
781
+ *
782
+ * @param config - The Dashfy configuration object containing server settings,
783
+ * dashboards, and optional API configuration
784
+ *
785
+ * @remarks
786
+ * This method should be called before {@link start}. For loading configuration
787
+ * from a JSON or YAML file, use {@link configureFromFile} instead.
788
+ *
789
+ * @example
790
+ * ```ts
791
+ * const dashfy = new Dashfy();
792
+ *
793
+ * // Configure with a configuration object
794
+ * dashfy.configure({
795
+ * port: 5001,
796
+ * host: 'localhost',
797
+ * dashboards: [
798
+ * {
799
+ * id: 'main',
800
+ * title: 'Main Dashboard',
801
+ * widgets: []
802
+ * }
803
+ * ],
804
+ * apis: {
805
+ * pollInterval: 30000 // 30 seconds
806
+ * }
807
+ * });
808
+ *
809
+ * await dashfy.start();
810
+ * ```
811
+ */
812
+ configure(config) {
813
+ this.config = config;
814
+ if (config.apis?.pollInterval) {
815
+ this.bus.pollInterval = config.apis.pollInterval;
816
+ this.logger.info(`Set global poll interval to ${this.bus.pollInterval}ms`);
817
+ }
818
+ }
819
+ /**
820
+ * Loads and applies configuration from a file (JSON or YAML).
821
+ *
822
+ * This method reads a configuration file (supports .json, .yml, .yaml formats),
823
+ * validates it against the schema, and applies it to the server instance. It also
824
+ * optionally watches the file for changes and automatically reloads the configuration
825
+ * when the file is modified. When changes are detected, all connected clients are
826
+ * notified via Socket.IO.
827
+ *
828
+ * @param configPath - Path to the configuration file (absolute or relative).
829
+ * Supports .json, .yml, .yaml extensions
830
+ * @param watchConfig - Whether to watch the configuration file for changes.
831
+ * When `true`, the configuration will be automatically reloaded
832
+ * on file changes. Defaults to `true`
833
+ *
834
+ * @throws {Error} If the configuration file cannot be read or fails validation
835
+ *
836
+ * @remarks
837
+ * This method should be called before {@link start}. If you already have a
838
+ * configuration object, use {@link configure} instead.
839
+ *
840
+ * When the configuration is reloaded due to file changes:
841
+ * - The new configuration is validated before applying
842
+ * - Connected clients receive the updated configuration (excluding sensitive API data)
843
+ * - If validation fails, the old configuration remains active
844
+ *
845
+ * @example
846
+ * ```ts
847
+ * const dashfy = new Dashfy();
848
+ *
849
+ * // Load configuration from JSON file with auto-reload
850
+ * await dashfy.configureFromFile('./dashfy.config.json');
851
+ *
852
+ * // Load configuration from YAML file with auto-reload
853
+ * await dashfy.configureFromFile('./dashfy.config.yaml');
854
+ *
855
+ * // Load without watching for changes (production)
856
+ * await dashfy.configureFromFile('./dashfy.config.yaml', false);
857
+ *
858
+ * await dashfy.start();
859
+ * ```
860
+ */
861
+ async configureFromFile(configPath, watchConfig = true) {
862
+ this.configPath = configPath;
863
+ const config = await loadConfig(configPath);
864
+ this.configure(config);
865
+ if (watchConfig) {
866
+ const watcher = watch(configPath);
867
+ watcher.on("change", async () => {
868
+ this.logger.info("Configuration file changed, reloading...");
869
+ try {
870
+ const newConfig = await loadConfig(configPath);
871
+ this.configure(newConfig);
872
+ if (this.io) {
873
+ this.io.emit("configuration", this.getPublicConfig(newConfig));
874
+ }
875
+ this.logger.info("Configuration reloaded successfully");
876
+ } catch (error) {
877
+ this.logger.error({ err: error }, "Failed to reload configuration");
878
+ }
879
+ });
880
+ this.logger.info(`Watching configuration file: ${configPath}`);
881
+ }
882
+ }
883
+ /**
884
+ * Registers an API with the message bus for data retrieval.
885
+ *
886
+ * This method allows you to register custom APIs that widgets can subscribe to
887
+ * for fetching data. APIs can operate in either poll mode (periodic fetching)
888
+ * or push mode (real-time updates via callbacks).
889
+ *
890
+ * @param id - Unique identifier for the API (e.g., 'github', 'weather', 'analytics')
891
+ * @param api - Factory function that returns an object containing API methods.
892
+ * Each method should be async and return data that widgets can consume
893
+ * @param mode - The API mode: `poll` (default) for periodic polling,
894
+ * or `push` for real-time push-based updates
895
+ *
896
+ * @throws {Error} If an API with the same ID is already registered
897
+ * @throws {Error} If the API mode is invalid
898
+ *
899
+ * @remarks
900
+ * - In **poll mode**, the bus automatically calls API methods at regular intervals
901
+ * - In **push mode**, API methods receive a callback to push data when available
902
+ * - APIs should be registered before calling {@link start}
903
+ * - The core `dashfy` API is automatically registered and provides system information
904
+ *
905
+ * @example
906
+ * ```ts
907
+ * const dashfy = new Dashfy();
908
+ *
909
+ * // Register a poll-mode API (default)
910
+ * dashfy.registerApi('github', () => ({
911
+ * async stars(params: { owner: string; repo: string }) {
912
+ * const response = await fetch(
913
+ * `https://api.github.com/repos/${params.owner}/${params.repo}`
914
+ * );
915
+ * const data = await response.json();
916
+ * return { stars: data.stargazers_count };
917
+ * }
918
+ * }));
919
+ *
920
+ * // Register a push-mode API for real-time updates
921
+ * dashfy.registerApi('metrics', () => ({
922
+ * async cpuUsage(callback: (data: unknown) => void) {
923
+ * const interval = setInterval(() => {
924
+ * const usage = process.cpuUsage();
925
+ * callback({ cpu: usage });
926
+ * }, 1000);
927
+ *
928
+ * return () => clearInterval(interval);
929
+ * }
930
+ * }), 'push');
931
+ * ```
932
+ */
933
+ registerApi(id, api, mode = "poll") {
934
+ this.bus.registerApi(id, api, mode);
935
+ }
936
+ /**
937
+ * Starts the Dashfy server and begins listening for connections.
938
+ *
939
+ * This method initializes and starts all server components including the HTTP server,
940
+ * WebSocket connections, static file serving, and API endpoints. It must be called
941
+ * after configuration is loaded via {@link configure} or {@link configureFromFile}.
942
+ *
943
+ * @throws {Error} If configuration has not been set before calling this method
944
+ *
945
+ * @remarks
946
+ * **What this method does:**
947
+ * 1. Validates that configuration is loaded
948
+ * 2. Sets up CORS for cross-origin requests
949
+ * 3. Serves static files from the `build` directory (if available)
950
+ * 4. Registers HTTP endpoints:
951
+ * - `GET /config` - Returns public configuration (excludes sensitive API data)
952
+ * - `GET /health` - Health check endpoint with uptime and status
953
+ * - `GET /api/info` - Returns registered APIs and connection info
954
+ * 5. Initializes Socket.IO for real-time WebSocket communication
955
+ * 6. Sets up Socket.IO event handlers:
956
+ * - `connection` - Client connects, receives configuration
957
+ * - `api.subscription` - Widget subscribes to data
958
+ * - `api.unsubscription` - Widget unsubscribes from data
959
+ * - `disconnect` - Client disconnects, cleanup performed
960
+ * - `error` - Socket error handling
961
+ * 7. Starts listening on configured host and port
962
+ *
963
+ * **Port and Host Resolution:**
964
+ * - Port: `process.env.PORT` → `config.port` → `5001` (default)
965
+ * - Host: `process.env.HOST` → `config.host` → `0.0.0.0` (default)
966
+ *
967
+ * @example
968
+ * ```ts
969
+ * const dashfy = new Dashfy();
970
+ *
971
+ * // Configure the server
972
+ * await dashfy.configureFromFile('./dashfy.config.json');
973
+ *
974
+ * // Register custom APIs
975
+ * dashfy.registerApi('github', () => ({
976
+ * async stars(params: { owner: string; repo: string }) {
977
+ * // ... fetch GitHub stars
978
+ * }
979
+ * }));
980
+ *
981
+ * // Start the server
982
+ * await dashfy.start();
983
+ * // Server is now running and accepting connections
984
+ * ```
985
+ *
986
+ * @example
987
+ * ```ts
988
+ * // With environment variables
989
+ * process.env.PORT = '3000';
990
+ * process.env.HOST = 'localhost';
991
+ *
992
+ * const dashfy = new Dashfy();
993
+ * await dashfy.configureFromFile('./dashfy.config.json');
994
+ * await dashfy.start();
995
+ * // Server running on http://localhost:3000
996
+ * ```
997
+ */
998
+ async start() {
999
+ if (!this.config) {
1000
+ const errorMessage = "Configuration required. Call configure() or configureFromFile() before starting.";
1001
+ this.logger.error(errorMessage);
1002
+ throw new Error(errorMessage);
1003
+ }
1004
+ await this.app.register(cors, {
1005
+ origin: true,
1006
+ credentials: true
1007
+ });
1008
+ const baseDir = this.config.baseDir ?? process.cwd();
1009
+ const staticPath = path.join(baseDir, "build");
1010
+ try {
1011
+ await this.app.register(fastifyStatic, {
1012
+ root: staticPath,
1013
+ prefix: "/"
1014
+ });
1015
+ this.logger.info(`Serving static files from ${staticPath}`);
1016
+ } catch (_error) {
1017
+ this.logger.warn("Static files directory not found, skipping static serve");
1018
+ }
1019
+ this.app.get("/config", () => {
1020
+ return this.getPublicConfig(this.config);
1021
+ });
1022
+ this.app.get("/health", () => ({
1023
+ status: "ok",
1024
+ uptime: process.uptime(),
1025
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1026
+ }));
1027
+ this.app.get("/api/info", () => ({
1028
+ apis: this.bus.listApis(),
1029
+ clientCount: this.bus.clientCount(),
1030
+ subscriptions: this.bus.getSubscriptionsInfo()
1031
+ }));
1032
+ this.io = new Server(this.app.server, {
1033
+ cors: {
1034
+ origin: "*",
1035
+ credentials: true
1036
+ },
1037
+ transports: ["websocket", "polling"],
1038
+ pingTimeout: SOCKET_PING_TIMEOUT,
1039
+ pingInterval: SOCKET_PING_INTERVAL
1040
+ });
1041
+ this.io.on("error", (error) => {
1042
+ this.logger.error({ err: error }, "Socket.IO server error");
1043
+ });
1044
+ this.io.on("connection", (socket) => {
1045
+ this.logger.info(`Socket connected: ${socket.id}`);
1046
+ this.bus.addClient(socket);
1047
+ if (this.config) {
1048
+ socket.emit("configuration", this.getPublicConfig(this.config));
1049
+ }
1050
+ socket.on(WebSocketEvent.API_SUBSCRIPTION, (subscription) => {
1051
+ this.logger.debug(`Subscription request: ${subscription.id} from ${socket.id}`);
1052
+ try {
1053
+ this.bus.subscribe(socket.id, subscription);
1054
+ } catch (error) {
1055
+ this.logger.error({ err: error }, "Subscription error");
1056
+ socket.emit(WebSocketEvent.API_ERROR, {
1057
+ id: subscription.id,
1058
+ error: {
1059
+ message: getErrorMessage(error)
1060
+ }
1061
+ });
1062
+ }
1063
+ });
1064
+ socket.on(WebSocketEvent.API_UNSUBSCRIPTION, (subscription) => {
1065
+ this.logger.debug(`Unsubscription request: ${subscription.id} from ${socket.id}`);
1066
+ this.bus.unsubscribe(socket.id, subscription.id);
1067
+ });
1068
+ socket.on(WebSocketEvent.DISCONNECT, (reason) => {
1069
+ this.logger.info(`Socket disconnected: ${socket.id} (${reason})`);
1070
+ this.bus.removeClient(socket.id);
1071
+ });
1072
+ socket.on(WebSocketEvent.ERROR, (error) => {
1073
+ this.logger.error({ err: error, socketId: socket.id }, "Socket error");
1074
+ });
1075
+ });
1076
+ const port = process.env.PORT ? Number(process.env.PORT) : this.config.port ?? DEFAULT_PORT;
1077
+ const host = process.env.HOST ?? this.config.host ?? DEFAULT_HOST;
1078
+ await this.app.listen({ port, host });
1079
+ this.logger.info(`\u{1F680} Dashfy server started`);
1080
+ this.logger.info(` - HTTP: http://${host}:${port}`);
1081
+ this.logger.info(` - WebSocket: ws://${host}:${port}`);
1082
+ this.logger.info(` - Registered APIs: ${this.bus.listApis().join(", ")}`);
1083
+ }
1084
+ /**
1085
+ * Gracefully stops the Dashfy server and cleans up all resources.
1086
+ *
1087
+ * This method performs a graceful shutdown of the server, closing all active
1088
+ * connections and releasing resources. It closes the Socket.IO server (if initialized)
1089
+ * and the underlying Fastify HTTP server.
1090
+ *
1091
+ * @remarks
1092
+ * **Shutdown process:**
1093
+ * 1. Logs shutdown initiation
1094
+ * 2. Closes Socket.IO server (disconnects all WebSocket clients)
1095
+ * 3. Closes Fastify HTTP server (stops accepting new connections)
1096
+ * 4. Waits for all pending requests to complete
1097
+ * 5. Logs shutdown completion
1098
+ *
1099
+ * This method is safe to call even if the server was never started or is already stopped.
1100
+ * All connected clients will be disconnected, and their subscriptions will be cleaned up
1101
+ * automatically by the bus.
1102
+ *
1103
+ * @example
1104
+ * ```ts
1105
+ * const dashfy = new Dashfy();
1106
+ * await dashfy.configureFromFile('./dashfy.config.json');
1107
+ * await dashfy.start();
1108
+ *
1109
+ * // Later, gracefully shut down the server
1110
+ * await dashfy.stop();
1111
+ * ```
1112
+ *
1113
+ * @example
1114
+ * ```ts
1115
+ * // Graceful shutdown on process signals
1116
+ * const dashfy = new Dashfy();
1117
+ * await dashfy.configureFromFile('./dashfy.config.json');
1118
+ * await dashfy.start();
1119
+ *
1120
+ * process.on('SIGTERM', async () => {
1121
+ * console.log('SIGTERM received, shutting down gracefully...');
1122
+ * await dashfy.stop();
1123
+ * process.exit(0);
1124
+ * });
1125
+ *
1126
+ * process.on('SIGINT', async () => {
1127
+ * console.log('SIGINT received, shutting down gracefully...');
1128
+ * await dashfy.stop();
1129
+ * process.exit(0);
1130
+ * });
1131
+ * ```
1132
+ */
1133
+ async stop() {
1134
+ this.logger.info("Stopping Dashfy server...");
1135
+ if (this.io) {
1136
+ await new Promise((resolve, reject) => {
1137
+ void this.io.close((error) => {
1138
+ if (error) {
1139
+ reject(error);
1140
+ } else {
1141
+ resolve();
1142
+ }
1143
+ });
1144
+ });
1145
+ }
1146
+ await this.app.close();
1147
+ this.logger.info("Server stopped");
1148
+ }
1149
+ /**
1150
+ * Returns a sanitized configuration object without sensitive API data.
1151
+ *
1152
+ * Removes the `apis` property from the configuration before sending it to clients,
1153
+ * as it may contain sensitive server-side settings like polling intervals or API keys.
1154
+ *
1155
+ * @param config - The full Dashfy configuration object
1156
+ * @returns Configuration object without the `apis` property
1157
+ *
1158
+ * @internal
1159
+ */
1160
+ getPublicConfig(config) {
1161
+ const { apis, ...publicConfig } = config;
1162
+ return publicConfig;
1163
+ }
1164
+ /**
1165
+ * Registers the core `dashfy` API that provides system introspection and monitoring.
1166
+ *
1167
+ * This internal method is automatically called during construction to register
1168
+ * a built-in API that exposes server health, statistics, and debugging information.
1169
+ * The `dashfy` API is available to all widgets for monitoring server state.
1170
+ *
1171
+ * @remarks
1172
+ * **Available methods:**
1173
+ * - `inspector()` - Returns comprehensive system information including:
1174
+ * - `apis`: List of all registered API identifiers
1175
+ * - `clientCount`: Number of currently connected WebSocket clients
1176
+ * - `subscriptions`: Detailed information about all active subscriptions
1177
+ * - `uptime`: Server process uptime in seconds
1178
+ * - `version`: Dashfy server version (from package.json)
1179
+ * - `nodeVersion`: Node.js runtime version
1180
+ *
1181
+ * This API is useful for:
1182
+ * - Building admin/monitoring dashboards
1183
+ * - Debugging subscription and connection issues
1184
+ * - Health checks and system monitoring
1185
+ * - Displaying server statistics to users
1186
+ *
1187
+ * @example
1188
+ * ```ts
1189
+ * // Widgets can subscribe to the dashfy API
1190
+ * // In a widget configuration:
1191
+ * {
1192
+ * api: 'dashfy',
1193
+ * method: 'inspector'
1194
+ * }
1195
+ *
1196
+ * // This returns:
1197
+ * {
1198
+ * apis: ['dashfy', 'github', 'weather'],
1199
+ * clientCount: 5,
1200
+ * subscriptions: [...],
1201
+ * uptime: 3600.5,
1202
+ * version: '0.1.0',
1203
+ * nodeVersion: 'v20.10.0'
1204
+ * }
1205
+ * ```
1206
+ *
1207
+ * @internal
1208
+ */
1209
+ registerCoreApi() {
1210
+ this.registerApi("dashfy", () => ({
1211
+ inspector: async () => Promise.resolve({
1212
+ apis: this.bus.listApis(),
1213
+ clientCount: this.bus.clientCount(),
1214
+ subscriptions: this.bus.getSubscriptionsInfo(),
1215
+ uptime: process.uptime(),
1216
+ version: process.env.npm_package_version ?? "unknown",
1217
+ nodeVersion: process.version
1218
+ })
1219
+ }));
1220
+ }
1221
+ };
1222
+
1223
+ export { Dashfy };
1224
+ //# sourceMappingURL=index.js.map
1225
+ //# sourceMappingURL=index.js.map