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