{"_id":"@getdashfy/server","_rev":"3-19db69da00064fd50a949de0fff75b94","name":"@getdashfy/server","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@getdashfy/server","version":"0.1.0","keywords":["dashfy","server","fastify","socket.io","dashboard"],"author":{"url":"https://github.com/brenopolanski","name":"Breno Polanski","email":"breno@dashfy.dev"},"license":"AGPL-3.0-or-later","_id":"@getdashfy/server@0.1.0","maintainers":[{"name":"breno.polanski","email":"breno.polanski@gmail.com"}],"homepage":"https://github.com/dashfy/dashfy#readme","bugs":{"url":"https://github.com/dashfy/dashfy/issues"},"dist":{"shasum":"a4b5bbf6f701826fb927c50c4a2af75d79b05513","tarball":"https://registry.npmjs.org/@getdashfy/server/-/server-0.1.0.tgz","fileCount":10,"integrity":"sha512-Zs5X2ur03kdMf3UYgBUU4PpmrD8FNp4llAEiFlUujoMhvMEW1Ab8+tlvyhYQcNHENWNQRVL0cdDD3uOFUM+ZUg==","signatures":[{"sig":"MEQCIH47IhMTRlFNS3NPVY0XOgveSu3HANY5HhED4o3E4JafAiAhKCHMP/acutz+ieJrH+N+PY63xY8png2DpLwlPp0XbA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":341859},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"06e9595c8a7156fb1c509fbcd605a454507add7c","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rm -rf .turbo dist node_modules","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","check:circular":"madge --circular --extensions ts src/"},"_npmUser":{"name":"breno.polanski","email":"breno.polanski@gmail.com"},"repository":{"url":"git+ssh://git@github.com/dashfy/dashfy.git","type":"git","directory":"packages/server"},"_npmVersion":"11.13.0","description":"Dashfy server with real-time data streaming and multi-dashboard support","directories":{},"_nodeVersion":"25.0.0","dependencies":{"zod":"^3.24.1","pino":"^9.5.0","yaml":"^2.8.1","undici":"^6.22.0","fastify":"^5.1.0","chokidar":"^4.0.3","socket.io":"^4.8.1","pino-pretty":"^13.0.0","@fastify/cors":"^10.0.1","@fastify/static":"^8.0.2","@getdashfy/utils":"workspace:*"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.8","typescript":"^5.7.2","@types/node":"^20.17.10","@getdashfy/types":"workspace:*","@getdashfy/tsconfig":"workspace:*","@vitest/coverage-v8":"^2.1.9"},"_npmOperationalInternal":{"tmp":"tmp/server_0.1.0_1784641872011_0.7443623141333287","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@getdashfy/server","version":"0.1.1","keywords":["dashfy","server","fastify","socket.io","dashboard"],"author":{"url":"https://github.com/brenopolanski","name":"Breno Polanski","email":"breno@dashfy.dev"},"license":"AGPL-3.0-or-later","_id":"@getdashfy/server@0.1.1","maintainers":[{"name":"breno.polanski","email":"breno.polanski@gmail.com"}],"homepage":"https://github.com/dashfy/dashfy#readme","bugs":{"url":"https://github.com/dashfy/dashfy/issues"},"dist":{"shasum":"1006eeb79cc09c8dda7a0d1bcc597fc9144e973e","tarball":"https://registry.npmjs.org/@getdashfy/server/-/server-0.1.1.tgz","fileCount":10,"integrity":"sha512-nIMpLpWZLHP2tb3/pajoh+zmyAK11wu86lnZ006NW/yzMowwa8MoRDY+lGAx/f84r5gL7151VjbZ2gohJ7hGjg==","signatures":[{"sig":"MEUCIQCFeCxhj+RYkuhSfVcjlx2L5EnhA9fa0I0EU/6dUf8OYQIgS52va2meJRtRGggSp827NaNc9fA0LJNczVOHUN+3kZk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":341996},"main":"./dist/index.cjs","type":"module","_from":"file:getdashfy-server-0.1.1.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rm -rf .turbo dist node_modules","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","check:circular":"madge --circular --extensions ts src/"},"_npmUser":{"name":"breno.polanski","email":"breno.polanski@gmail.com"},"_resolved":"/private/var/folders/40/yqjd7mn110s9hm89bcd98dfc0000gn/T/c7725017a3e0ed9a2424bcdf1e9526a5/getdashfy-server-0.1.1.tgz","_integrity":"sha512-nIMpLpWZLHP2tb3/pajoh+zmyAK11wu86lnZ006NW/yzMowwa8MoRDY+lGAx/f84r5gL7151VjbZ2gohJ7hGjg==","repository":{"url":"git+ssh://git@github.com/dashfy/dashfy.git","type":"git","directory":"packages/server"},"_npmVersion":"11.13.0","description":"Dashfy server with real-time data streaming and multi-dashboard support","directories":{},"_nodeVersion":"25.0.0","dependencies":{"zod":"^3.24.1","pino":"^9.5.0","yaml":"^2.8.1","undici":"^6.22.0","fastify":"^5.1.0","chokidar":"^4.0.3","socket.io":"^4.8.1","pino-pretty":"^13.0.0","@fastify/cors":"^10.0.1","@fastify/static":"^8.0.2","@getdashfy/utils":"0.1.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.8","typescript":"^5.7.2","@types/node":"^20.17.10","@getdashfy/types":"0.1.1","@getdashfy/tsconfig":"0.0.0","@vitest/coverage-v8":"^2.1.9"},"_npmOperationalInternal":{"tmp":"tmp/server_0.1.1_1784685609593_0.8321038354316719","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@getdashfy/server","version":"0.2.0","description":"Dashfy server with real-time data streaming and multi-dashboard support","keywords":["dashfy","server","fastify","socket.io","dashboard"],"homepage":"https://github.com/dashfy/dashfy#readme","bugs":{"url":"https://github.com/dashfy/dashfy/issues"},"repository":{"type":"git","url":"git+ssh://git@github.com/dashfy/dashfy.git","directory":"packages/server"},"license":"AGPL-3.0-or-later","author":{"name":"Breno Polanski","email":"breno@dashfy.dev","url":"https://github.com/brenopolanski"},"type":"module","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","publishConfig":{"access":"public"},"engines":{"node":">=20.0.0"},"dependencies":{"@fastify/cors":"^10.0.1","@fastify/static":"^8.0.2","chokidar":"^4.0.3","fastify":"^5.1.0","pino":"^9.5.0","pino-pretty":"^13.0.0","socket.io":"^4.8.1","undici":"^6.22.0","yaml":"^2.8.1","zod":"^3.24.1","@getdashfy/utils":"0.1.1"},"devDependencies":{"@types/node":"^20.17.10","@vitest/coverage-v8":"^2.1.9","tsup":"^8.3.5","typescript":"^5.7.2","vitest":"^2.1.8","@getdashfy/types":"0.3.0","@getdashfy/tsconfig":"0.0.0"},"scripts":{"build":"tsup","check:circular":"madge --circular --extensions ts src/ --ts-config tsconfig.json","clean":"rm -rf .turbo dist node_modules","dev":"tsup --watch","test":"vitest run","test:coverage":"vitest run --coverage","test:watch":"vitest","typecheck":"tsc --noEmit"},"_id":"@getdashfy/server@0.2.0","_integrity":"sha512-75XE1mWVqUTrPn8DxpiedfTyWucj+51kwzloDjclwpKp4gdS1oIkfdMXjfuigYSmxb5ULqTSrk7UrMM9v5b1kQ==","_resolved":"/private/var/folders/40/yqjd7mn110s9hm89bcd98dfc0000gn/T/c0fc0f9b3ef164f9f255a9e04fbff4c6/getdashfy-server-0.2.0.tgz","_from":"file:getdashfy-server-0.2.0.tgz","_nodeVersion":"25.0.0","_npmVersion":"11.18.0","dist":{"integrity":"sha512-75XE1mWVqUTrPn8DxpiedfTyWucj+51kwzloDjclwpKp4gdS1oIkfdMXjfuigYSmxb5ULqTSrk7UrMM9v5b1kQ==","shasum":"ba86e180bc7bf4b1bbe6ed58fd0e329bf6d6fdda","tarball":"https://registry.npmjs.org/@getdashfy/server/-/server-0.2.0.tgz","fileCount":10,"unpackedSize":343671,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCD23cVuy57c4oCjQGksq/THOnGciCUSarLVvPgHpVOdgIgFmx4SrGxRMAMdS78Nx4dFfZGvqlAEoYUJkjezeE9DXY="}]},"_npmUser":{"name":"breno.polanski","email":"breno.polanski@gmail.com"},"directories":{},"maintainers":[{"name":"breno.polanski","email":"breno.polanski@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/server_0.2.0_1785938985855_0.18395387385765916"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-21T13:51:11.785Z","modified":"2026-08-05T14:09:46.175Z","0.1.0":"2026-07-21T13:51:12.262Z","0.1.1":"2026-07-22T02:00:09.729Z","0.2.0":"2026-08-05T14:09:46.003Z"},"bugs":{"url":"https://github.com/dashfy/dashfy/issues"},"author":{"name":"Breno Polanski","email":"breno@dashfy.dev","url":"https://github.com/brenopolanski"},"license":"AGPL-3.0-or-later","homepage":"https://github.com/dashfy/dashfy#readme","keywords":["dashfy","server","fastify","socket.io","dashboard"],"repository":{"type":"git","url":"git+ssh://git@github.com/dashfy/dashfy.git","directory":"packages/server"},"description":"Dashfy server with real-time data streaming and multi-dashboard support","maintainers":[{"name":"breno.polanski","email":"breno.polanski@gmail.com"}],"readme":"# `@getdashfy/server`\n\n> Dashfy server with real-time data streaming and multi-dashboard support.\n\n## Introduction\n\n`@getdashfy/server` is the backend runtime for Dashfy dashboards. It handles configuration loading, API registration, real-time data streaming, and WebSocket communication with clients.\n\nThe server acts as the central orchestrator that:\n\n- Loads dashboard configuration from `TypeScript` object / `JSON` / `YAML` files\n- Connects to APIs and data sources through extensions\n- Manages subscriptions and real-time updates\n- Streams data to connected clients via WebSockets\n- Provides HTTP endpoints for health checks and configuration\n\n## Installation\n\nInstall with your favorite package manager:\n\n#### `npm`\n\n```bash\nnpm install @getdashfy/server\n```\n\n#### `pnpm`\n\n```bash\npnpm add @getdashfy/server\n```\n\n#### `yarn`\n\n```bash\nyarn add @getdashfy/server\n```\n\n#### `bun`\n\n```bash\nbun add @getdashfy/server\n```\n\n## Quick Start\n\nCreate a Dashfy server and load a dashboard configuration:\n\n```ts\nimport { createJsonClient } from '@getdashfy/ext-json/client'\nimport { createGitHubClient } from '@getdashfy/ext-github/client'\nimport { Dashfy } from '@getdashfy/server'\n\n// Create server instance\nconst dashfy = new Dashfy()\n\n// Load dashboard configuration from a file (JSON or YAML):\nawait dashfy.configureFromFile('./dashfy.config.yml')\n\n// or from a TypeScript object:\n// import type { DashfyConfig } from '@getdashfy/types'\n// const dashfyConfig: DashfyConfig = {...}\n// dashfy.configure(dashfyConfig)\n\n// Register JSON API\ndashfy.registerApi('json', createJsonClient())\n\n// Register GitHub API\ndashfy.registerApi(\n  'github',\n  createGitHubClient({\n    token: process.env.GITHUB_TOKEN!,\n  }),\n)\n\n// Start server\nawait dashfy.start()\n// Server running at http://0.0.0.0:5001\n```\n\n## Core Features\n\n#### » Configuration Management\n\nLoad dashboard configuration from `TypeScript` object, `JSON` or `YAML` files with automatic format detection:\n\n```ts\n// Load from file\nawait dashfy.configureFromFile('./dashfy.config.json')\n\n// Or provide config directly\ndashfy.configure({\n  dashboards: [\n    {\n      title: 'My Dashboard',\n      columns: 3,\n      rows: 2,\n      widgets: [\n        /* ... */\n      ],\n    },\n  ],\n})\n```\n\n**Hot-reload support**: Configuration changes are automatically detected and broadcast to connected clients.\n\n```ts\n// Enable hot-reload (default)\nawait dashfy.configureFromFile('./dashfy.config.yml', true)\n\n// Disable hot-reload (production)\nawait dashfy.configureFromFile('./dashfy.config.yml', false)\n```\n\n#### » API Registration\n\nRegister custom APIs that widgets can subscribe to:\n\n```ts\ndashfy.registerApi('github', ({ logger, request }) => ({\n  async repos(params: { user: string }) {\n    logger.info({ user: params.user }, 'Fetching repositories')\n    return request({\n      url: `https://api.github.com/users/${params.user}/repos`,\n    })\n  },\n\n  async stars(params: { owner: string; repo: string }) {\n    const data = await request({\n      url: `https://api.github.com/repos/${params.owner}/${params.repo}`,\n    })\n    return { stars: data.stargazers_count }\n  },\n}))\n```\n\n**API modes**:\n\n- **Poll mode** (default): Periodically fetches data at configured intervals\n- **Push mode**: Real-time streaming with callback-based producers\n\n```ts\n// Poll mode - fetches every 15 seconds (configurable)\ndashfy.registerApi('weather', weatherApi, 'poll')\n\n// Push mode - streams data as it arrives\ndashfy.registerApi(\n  'metrics',\n  ({ logger }) => ({\n    cpuUsage(callback: (data: unknown) => void) {\n      const interval = setInterval(() => {\n        callback({\n          usage: process.cpuUsage(),\n          timestamp: Date.now(),\n        })\n      }, 1_000)\n\n      return () => clearInterval(interval)\n    },\n  }),\n  'push',\n)\n```\n\n#### » Real-time Updates\n\nBuilt on [Socket.IO](https://github.com/socketio/socket.io) for bidirectional [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) communication:\n\n**Server → Client Events**:\n\n- `configuration` - Dashboard config updates\n- `api.data` - API response data\n- `api.error` - API error messages\n\n**Client → Server Events**:\n\n- `api.subscription` - Subscribe to API method\n- `api.unsubscription` - Unsubscribe from API method\n\n**Subscription lifecycle**:\n\n1. Client subscribes with `api`, `endpoint`, and an `id` (conventionally `api.method`, e.g. `github.stars`); routing uses `api` + `endpoint`, while `id` is the dedup/cache key\n2. Server creates subscription if it doesn't exist (reusing it for later subscribers)\n3. Immediate data fetch + periodic polling (poll mode) or callback setup (push mode)\n4. Data is cached and broadcast to all subscribed clients\n5. When last client unsubscribes, subscription is cleaned up\n\n#### » Built-in Inspector API\n\nThe server automatically provides a `dashfy` API for system introspection:\n\n```ts\n// Widgets can subscribe to dashfy.inspector for monitoring\n{\n  apis: ['github', 'json', 'dashfy'],\n  clientCount: 3,\n  subscriptions: [\n    {\n      id: 'github.repos',\n      clientCount: 2,\n      hasCachedData: true,\n      hasTimer: true\n    }\n  ],\n  uptime: 3600,\n  version: '0.1.0',\n  nodeVersion: 'v20.11.0'\n}\n```\n\n#### » HTTP Endpoints\n\nWhen started, the server exposes:\n\n| Endpoint    | Method | Description                                                    |\n| ----------- | ------ | -------------------------------------------------------------- |\n| `/config`   | GET    | Returns public configuration (excludes sensitive `apis` field) |\n| `/health`   | GET    | Health check with uptime and timestamp                         |\n| `/api/info` | GET    | Server info: registered APIs, client count, subscriptions      |\n\n#### » Logging\n\nStructured logging with [Pino](https://github.com/pinojs/pino):\n\n```ts\nimport { Dashfy } from '@getdashfy/server'\nimport pino from 'pino'\n\n// Custom logger\nconst logger = pino({ level: 'debug' })\nconst dashfy = new Dashfy({ logger })\n```\n\n**Environment-aware defaults**:\n\n- **Development**: Pretty-printed output (level: `info`)\n- **Production**: JSON logs (level: `warn`)\n- Respects `LOG_LEVEL` environment variable\n\n#### » Integration with Existing Apps\n\nUse an existing [Fastify](https://github.com/fastify/fastify) instance:\n\n```ts\nimport Fastify from 'fastify'\nimport { Dashfy } from '@getdashfy/server'\n\nconst app = Fastify()\n\n// Add custom routes\napp.get('/custom', async () => ({ message: 'Hello' }))\n\n// Initialize Dashfy with existing app\nconst dashfy = new Dashfy({ app })\nawait dashfy.configureFromFile('./dashfy.config.json')\nawait dashfy.start()\n```\n\n## API Reference\n\n### `Dashfy` Class\n\n#### » Constructor\n\n```ts\nnew Dashfy(options?: DashfyOptions)\n```\n\n**Options**:\n\n- `logger?: Logger` - Custom Pino logger instance\n- `app?: FastifyInstance` - Existing Fastify app to integrate with\n\n#### » Methods\n\n##### `configure(config: DashfyConfig): void`\n\nApply configuration object directly.\n\n```ts\ndashfy.configure({\n  port: 3000,\n  dashboards: [\n    /* ... */\n  ],\n})\n```\n\n##### `configureFromFile(configPath: string, watchConfig = true): Promise<void>`\n\nLoad configuration from `JSON` or `YAML` file.\n\n**Parameters**:\n\n- `configPath` - Path to configuration file\n- `watchConfig` - Enable hot-reload (default: `true`)\n\n```ts\nawait dashfy.configureFromFile('./dashfy.config.yml', true)\n```\n\n##### `registerApi(id: string, api: APIRegistration, mode: PollMode = 'poll'): void`\n\nRegister a custom API for widgets to consume.\n\n**Parameters**:\n\n- `id` - Unique API identifier (e.g., `'github'`, `'weather'`)\n- `api` - Factory function that returns API methods\n- `mode` - `'poll'` (periodic) or `'push'` (real-time)\n\n```ts\ndashfy.registerApi('github', ({ logger, request }) => ({\n  async repos(params: { user: string }) {\n    return request({ url: `https://api.github.com/users/${params.user}/repos` })\n  },\n}))\n```\n\n##### `start(): Promise<void>`\n\nStart HTTP and WebSocket servers.\n\n**Port resolution** (in order):\n\n1. `process.env.PORT`\n2. `config.port`\n3. Default: `5001`\n\n**Host resolution** (in order):\n\n1. `process.env.HOST`\n2. `config.host`\n3. Default: `0.0.0.0`\n\n```ts\nawait dashfy.start()\n// Server running at http://0.0.0.0:5001\n```\n\n##### `stop(): Promise<void>`\n\nGraceful shutdown of all connections.\n\n```ts\nprocess.on('SIGTERM', async () => {\n  await dashfy.stop()\n  process.exit(0)\n})\n```\n\n## Configuration Schema\n\n```ts\ninterface DashfyConfig {\n  port?: number // Server port (default: 5001)\n  host?: string // Server host (default: '0.0.0.0')\n  baseDir?: string // Base directory for static files\n  staticDir?: string // Static files directory, resolved against baseDir (default: 'build')\n  rotationDuration?: number // Dashboard rotation interval (ms)\n  theme?: string // Default theme ID\n  dashboards: Array<{\n    title?: string\n    name?: string\n    columns: number // Grid columns\n    rows: number // Grid rows\n    widgets: Array<{\n      extension: string // Extension ID (e.g., 'github')\n      widget: string // Widget name (e.g., 'RepoBadge')\n      x: number // Grid X position\n      y: number // Grid Y position\n      columns: number // Widget width in columns\n      rows: number // Widget height in rows\n      title?: string // Widget title\n      // ... widget-specific properties\n    }>\n  }>\n  apis?: {\n    pollInterval?: number // Global poll interval in ms (default: 15_000)\n  }\n}\n```\n\n## API Registration\n\n#### » API Factory Function\n\n```ts\ntype APIClient = Record<string, (...args: any[]) => Promise<unknown>>\n\ntype CreatePushInterval = (options?: {\n  interval?: number\n}) => (\n  key: string,\n  callback: (data: unknown) => void,\n  fetchFn: () => Promise<unknown>,\n) => () => void\n\ntype APIRegistration = (dashfy: {\n  logger: Logger\n  request?: (options: RequestOptions) => Promise<unknown>\n  createPushInterval?: CreatePushInterval\n}) => APIClient\n```\n\nThe factory receives three helpers:\n\n- `logger` - Child [Pino](https://github.com/pinojs/pino) logger scoped to the API id\n- `request` - HTTP client for fetching data (see [HTTP Request Utility](#http-request-utility))\n- `createPushInterval` - Helper for building push-mode producers that poll a `fetchFn` on an interval (see [Push Mode](#-push-mode))\n\n#### » Poll Mode (Default)\n\nPeriodic data fetching at configured intervals:\n\n```ts\ndashfy.registerApi(\n  'weather',\n  ({ request }) => ({\n    async current(params: { city: string }) {\n      return request({\n        url: `https://api.weather.com/current?city=${params.city}`,\n      })\n    },\n  }),\n  'poll',\n)\n```\n\n**Behavior**:\n\n- Fetches data immediately on first subscription\n- Polls at `pollInterval` (default: 15 seconds, configurable in config)\n- Caches responses for instant delivery to new subscribers\n- Stops polling when last client unsubscribes\n\n#### » Push Mode\n\nReal-time streaming with callback-based producers:\n\n```ts\ndashfy.registerApi(\n  'metrics',\n  ({ logger }) => ({\n    cpuUsage(callback: (data: unknown) => void) {\n      logger.info('Starting CPU monitoring')\n\n      const interval = setInterval(() => {\n        callback({\n          usage: process.cpuUsage(),\n          timestamp: Date.now(),\n        })\n      }, 1_000)\n\n      // Return cleanup function\n      return () => {\n        clearInterval(interval)\n        logger.info('Stopped CPU monitoring')\n      }\n    },\n  }),\n  'push',\n)\n```\n\n**Behavior**:\n\n- Calls producer function on first subscription\n- Producer receives callback to push data\n- Data is broadcast to all subscribed clients\n- Cleanup function called when last client unsubscribes\n\n**Using `createPushInterval`**: Instead of managing timers by hand, use the injected `createPushInterval` helper to poll a `fetchFn` on an interval and push each result. It returns a disposer used for cleanup:\n\n```ts\ndashfy.registerApi(\n  'metrics',\n  ({ request, createPushInterval }) => {\n    // Push every 2 seconds (default interval)\n    const startPushInterval = createPushInterval({ interval: 2_000 })\n\n    return {\n      async prices(callback: (data: unknown) => void, params: { symbol: string }) {\n        return startPushInterval(`prices:${params.symbol}`, callback, () =>\n          request({ url: `https://api.example.com/price/${params.symbol}` }),\n        )\n      },\n    }\n  },\n  'push',\n)\n```\n\n## HTTP Request Utility\n\nThe `request` utility provided to APIs is built on [Undici](https://github.com/nodejs/undici):\n\n```ts\ninterface RequestOptions {\n  url: string\n  method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'\n  headers?: Record<string, string>\n  body?: unknown\n  timeout?: number // Default: 10_000ms\n}\n```\n\n**Features**:\n\n- Automatic JSON parsing\n- Configurable timeout\n- Error handling with status codes\n- TypeScript-friendly\n\n## Examples\n\n#### » Basic Server\n\n```ts\nimport { Dashfy } from '@getdashfy/server'\n\nconst dashfy = new Dashfy()\nawait dashfy.configureFromFile('./dashfy.config.json')\nawait dashfy.start()\n```\n\n#### » With Custom Logger\n\n```ts\nimport { Dashfy } from '@getdashfy/server'\nimport pino from 'pino'\n\nconst logger = pino({ level: 'debug' })\nconst dashfy = new Dashfy({ logger })\n\nawait dashfy.configureFromFile('./dashfy.config.yml')\nawait dashfy.start()\n```\n\n#### » Multiple APIs\n\n```ts\nimport { createJsonClient } from '@getdashfy/ext-json/client'\nimport { createGitHubClient } from '@getdashfy/ext-github/client'\nimport { createNbaClient } from '@getdashfy/ext-nba/client'\nimport { Dashfy } from '@getdashfy/server'\n\nconst dashfy = new Dashfy()\nawait dashfy.configureFromFile('./dashfy.config.yml')\n\n// Register multiple APIs\ndashfy.registerApi('json', createJsonClient())\ndashfy.registerApi('github', createGitHubClient({ token: process.env.GITHUB_TOKEN! }))\ndashfy.registerApi('nba', createNbaClient())\n\nawait dashfy.start()\n```\n\n#### » Custom API with Authentication\n\n```ts\ndashfy.registerApi('myapi', ({ request }) => ({\n  async getData(params: { id: string }) {\n    return request({\n      url: `https://api.example.com/data/${params.id}`,\n      headers: {\n        Authorization: `Bearer ${process.env.API_TOKEN}`,\n        'Content-Type': 'application/json',\n      },\n      timeout: 5_000,\n    })\n  },\n}))\n```\n\n#### » Graceful Shutdown\n\n```ts\nconst dashfy = new Dashfy()\nawait dashfy.configureFromFile('./dashfy.config.yml')\nawait dashfy.start()\n\n// Handle shutdown signals\nprocess.on('SIGTERM', async () => {\n  console.log('Shutting down gracefully...')\n  await dashfy.stop()\n  process.exit(0)\n})\n\nprocess.on('SIGINT', async () => {\n  console.log('Shutting down gracefully...')\n  await dashfy.stop()\n  process.exit(0)\n})\n```\n\n## Environment Variables\n\n| Variable    | Description                                                        | Default                     |\n| ----------- | ------------------------------------------------------------------ | --------------------------- |\n| `PORT`      | Server port                                                        | `5001`                      |\n| `HOST`      | Server host                                                        | `0.0.0.0`                   |\n| `LOG_LEVEL` | Logging level (`trace`, `debug`, `info`, `warn`, `error`, `fatal`) | `info` (dev), `warn` (prod) |\n| `NODE_ENV`  | Environment (`development`, `production`)                          | `development`               |\n\n## Architecture\n\nThe server consists of several key components working together to power real-time dashboards:\n\n```mermaid\nflowchart TB\n    ConfigFile[📄 Config File<br/>TypeScript / JSON / YAML]\n\n    subgraph DashfyServer[\"🖥️ Dashfy Server\"]\n        direction TB\n\n        subgraph Core[\"Core Orchestrator\"]\n            DashfyClass[Dashfy Class<br/><i>Main Controller</i>]\n        end\n\n        subgraph Components[\"Internal Components\"]\n            direction TB\n            ConfigLoader[Configuration Loader<br/><i>Zod Validation</i>]\n            MessageBus[Message Bus<br/><i>Pub/Sub System</i>]\n            HTTPServer[HTTP Server<br/><i>Fastify</i>]\n            WSServer[WebSocket Server<br/><i>Socket.IO</i>]\n        end\n\n        DashfyClass --> ConfigLoader\n        DashfyClass --> MessageBus\n        DashfyClass --> HTTPServer\n        DashfyClass --> WSServer\n    end\n\n    subgraph APIs[\"🔌 Registered APIs\"]\n        direction LR\n        API1[GitHub API]\n        API2[JSON API]\n        API3[Custom APIs]\n    end\n\n    subgraph Clients[\"🌐 Connected Clients\"]\n        direction LR\n        Client1[Client 1<br/><i>Browser/App</i>]\n        Client2[Client 2<br/><i>Browser/App</i>]\n        Client3[Client N<br/><i>Browser/App</i>]\n    end\n\n    subgraph DataFlow[\"Data Flow\"]\n        direction TB\n        Sub[📥 Subscription Request<br/><i>api.subscription</i>]\n        Poll[⏱️ Poll/Push Data<br/><i>15s interval or real-time</i>]\n        Cache[💾 Cache Response]\n        Broadcast[📤 Broadcast to Clients<br/><i>api.data event</i>]\n        Sub --> Poll --> Cache --> Broadcast\n    end\n\n    ConfigFile -->|Load & Watch| ConfigLoader\n    ConfigLoader -->|Validate & Apply| MessageBus\n\n    APIs -->|Register| MessageBus\n    MessageBus <-->|Manage Subscriptions| WSServer\n\n    HTTPServer -->|REST Endpoints| Clients\n    WSServer <-->|WebSocket Events| Clients\n\n    MessageBus -.->|Poll/Push| APIs\n    APIs -.->|Data| MessageBus\n\n    style DashfyServer fill:#9b59b6,stroke:#7d3c98,color:#fff\n    style APIs fill:#f39c12,stroke:#d68910,color:#fff\n    style Clients fill:#27ae60,stroke:#1e8449,color:#fff\n    style DataFlow fill:#3498db,stroke:#2874a6,color:#fff\n    style ConfigFile fill:#e74c3c,stroke:#c0392b,color:#fff\n```\n\n### Components Overview\n\n#### 1. **Dashfy Class**\n\nMain orchestrator that manages HTTP server, WebSocket server, and message bus.\n\n#### 2. **Message Bus**\n\nCentral pub/sub system that:\n\n- Manages API registrations\n- Tracks client connections\n- Handles subscriptions\n- Coordinates data streaming\n- Caches responses\n\n#### 3. **HTTP Server (Fastify)**\n\nProvides REST endpoints and static file serving with CORS support.\n\n#### 4. **WebSocket Server (Socket.IO)**\n\nReal-time bidirectional communication with clients.\n\n#### 5. **Configuration Loader**\n\nParses and validates `TypeScript` object / `JSON` / `YAML` configuration with Zod schema validation.\n\n## Performance\n\nThe server is optimized for efficiency:\n\n- **Shared subscriptions**: Multiple clients subscribing to the same API method share a single data stream\n- **Response caching**: New subscribers receive cached data immediately\n- **Automatic cleanup**: Unused subscriptions are removed to free resources\n- **Configurable polling**: Adjust poll intervals per dashboard or globally\n- **Connection pooling**: Efficient HTTP client (Undici) for API requests\n\n## Error Handling\n\nThe server provides robust error handling:\n\n- API failures don't crash the server\n- Errors are logged with context\n- Clients receive error messages via `api.error` events\n- Type-safe error extraction with `getErrorMessage()`\n\n## TypeScript Support\n\nFully typed with TypeScript:\n\n```ts\nimport type { DashfyConfig, APIRegistration, PollMode } from '@getdashfy/types'\n\nconst myApi: APIRegistration = ({ request }) => ({\n  async fetchData(params: { id: string }) {\n    return request({ url: `https://api.example.com/${params.id}` })\n  },\n})\n```\n\n## Development\n\n```bash\n# Install dependencies\npnpm install\n\n# Build\npnpm build\n\n# Watch mode\npnpm dev\n\n# Run tests\npnpm test\n\n# Run tests with coverage\npnpm test:coverage\n\n# Type check\npnpm typecheck\n```\n\n## Community\n\nJoin the community on [Dashfy's Discord server](https://dashfy.dev/discord) to discuss the project, ask questions, or get help.\n\nJoin the conversation on X (Twitter) and follow [@dashfydev](https://x.com/dashfydev) for updates and announcements.\n\n## License\n\nThis project is licensed under the AGPL-3.0 License - see the [LICENSE](./LICENSE) file for details.\n","readmeFilename":"README.md"}