@rawdash/server 0.13.0 → 0.14.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/README.md CHANGED
@@ -3,11 +3,28 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@rawdash/server)](https://www.npmjs.com/package/@rawdash/server)
4
4
  [![license](https://img.shields.io/npm/l/@rawdash/server)](https://github.com/rawdash/rawdash/blob/main/LICENSE)
5
5
 
6
- Standalone Hono HTTP server hosting the rawdash data API.
6
+ Framework-agnostic rawdash request handlers, sync engine, and wire-contract types. **No HTTP framework dependency** wrap with [`@rawdash/hono`](https://www.npmjs.com/package/@rawdash/hono) (or another adapter) to serve over HTTP.
7
7
 
8
8
  ## What it is
9
9
 
10
- `@rawdash/server` takes a rawdash config, runs the sync engine in-process, and exposes a REST API for widget data. Deploy it as a standalone service alongside your frontend, or use `createServer` to get the raw Hono app and deploy it to any JS runtime (Cloudflare Workers, Bun, Deno, etc.).
10
+ `@rawdash/server` is the engine half of rawdash:
11
+
12
+ - The **sync engine** (`runSync`, `createEngine`) that drives connectors and writes to storage.
13
+ - **Pure HTTP handlers** (`listWidgets`, `getWidget`, `triggerSync`, `getSyncStateHandler`, `getHealth`, `runRetentionOnce`) — async functions you can call from any framework.
14
+ - **`EngineContext`** — the per-request interface adapters use to inject `DashboardConfig` and `ServerStorage`. The handler doesn't care whether those come from a static config or are looked up fresh per request — that decision belongs to the adapter.
15
+ - **`ROUTES`** — canonical URL paths, the single source of truth for the wire contract.
16
+ - **`RawdashError`** — structured errors with `status` and `code` for adapters to translate.
17
+ - The **`SyncState` types** and `InMemoryStorage` (re-exported from `@rawdash/core`).
18
+
19
+ This package does **not** know about Hono, Express, Node's `http`, or any HTTP framework. Pick an adapter.
20
+
21
+ ## When to use what
22
+
23
+ | You want to… | Use |
24
+ | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
25
+ | Serve rawdash over HTTP in a Hono / Workers / Bun / Deno app | [`@rawdash/hono`](https://www.npmjs.com/package/@rawdash/hono) (depends on `@rawdash/server`) |
26
+ | Build a different framework adapter (Express, NestJS, etc.) | This package directly — wrap the pure handlers |
27
+ | Use the engine without HTTP (background job, CLI, MCP) | This package — `createEngine` / `runSync` |
11
28
 
12
29
  ## Install
13
30
 
@@ -15,99 +32,98 @@ Standalone Hono HTTP server hosting the rawdash data API.
15
32
  npm install @rawdash/server
16
33
  ```
17
34
 
18
- ## Quick example
35
+ ## The contract for adapter authors
19
36
 
20
- ```ts
21
- import { GitHubConnector } from '@rawdash/connector-github';
22
- import { defineConfig, defineDashboard, secret } from '@rawdash/core';
23
- import { serve } from '@rawdash/server';
24
-
25
- const github = new GitHubConnector(
26
- {
27
- owner: 'my-org',
28
- repo: 'my-repo',
29
- },
30
- {
31
- token: secret('GITHUB_TOKEN'),
32
- },
33
- );
34
-
35
- serve(
36
- defineConfig({
37
- connectors: [{ connector: github }],
38
- dashboards: {
39
- engineering: defineDashboard({ widgets: {} }),
40
- },
41
- }),
42
- { port: 8080 },
43
- );
44
- ```
37
+ Each pure handler takes an `EngineContext` (and any path parameters) and returns the response body or throws a `RawdashError`. Your adapter:
45
38
 
46
- ## API
39
+ 1. Routes the HTTP request to the matching handler.
40
+ 2. Constructs an `EngineContext` from the request — `getConfig` and `getStorage` can return constants or values derived from the request (e.g. read from a database keyed by a path param or auth header).
41
+ 3. Awaits the handler and serializes the result as JSON.
42
+ 4. Catches `RawdashError` and maps `status` + `code` to a structured HTTP response.
47
43
 
48
- ### `serve(config, options?)`
44
+ ```ts
45
+ import { RawdashError, isRawdashError, listWidgets } from '@rawdash/server';
46
+
47
+ // example: a hypothetical Express adapter
48
+ app.get('/dashboards/:id/widgets', async (req, res) => {
49
+ try {
50
+ const body = await listWidgets(
51
+ {
52
+ getConfig: () => loadConfig(),
53
+ getStorage: () => loadStorage(),
54
+ },
55
+ req.params.id,
56
+ );
57
+ res.json(body);
58
+ } catch (err) {
59
+ if (isRawdashError(err)) {
60
+ res.status(err.status).json({ error: err.message, code: err.code });
61
+ return;
62
+ }
63
+ throw err;
64
+ }
65
+ });
66
+ ```
49
67
 
50
- Starts the HTTP server on Node.js (via `@hono/node-server`). Options:
68
+ ## The wire contract
51
69
 
52
- | Option | Type | Default | Description |
53
- | --------- | --------------- | --------- | -------------------------------------- |
54
- | `port` | `number` | `8080` | Port to listen on |
55
- | `storage` | `ServerStorage` | in-memory | Storage backend (e.g. `LibsqlStorage`) |
70
+ | Route | Method | Handler | Response |
71
+ | -------------------------------------------- | ------ | --------------------- | --------------------------------------------- |
72
+ | `/health` | GET | `getHealth` | `{status:'ok'}` (liveness, no storage access) |
73
+ | `/sync/state` | GET | `getSyncStateHandler` | `SyncState` |
74
+ | `/sync` | POST | `triggerSync` | `{queued: boolean}` — returns immediately |
75
+ | `/dashboards/:dashboardId/widgets` | GET | `listWidgets` | `WidgetsListResponse` |
76
+ | `/dashboards/:dashboardId/widgets/:widgetId` | GET | `getWidget` | `CachedWidget` |
77
+ | `/retention/retain` | POST | `runRetentionOnce` | `{triggered: true}` (synchronous) |
56
78
 
57
- ### `createServer(config, options?)`
79
+ Paths are exported as constants from `ROUTES`. Use them in adapters (and in clients) instead of hard-coding.
58
80
 
59
- Returns the Hono app without binding to a port. Use when you need the app object directly.
81
+ ### `SyncState`
60
82
 
61
83
  ```ts
62
- import { createServer } from '@rawdash/server';
63
-
64
- const app = createServer(config);
65
- export default app; // deploy to Cloudflare Workers, Bun, Deno, etc.
84
+ type SyncStatus = 'idle' | 'queued' | 'running' | 'succeeded' | 'failed';
85
+ interface SyncState {
86
+ status: SyncStatus;
87
+ queuedAt: string | null;
88
+ startedAt: string | null;
89
+ lastSyncAt: string | null;
90
+ lastError: string | null;
91
+ }
66
92
  ```
67
93
 
68
- ## HTTP endpoints
69
-
70
- ### `GET /dashboards/:dashboardId/widgets`
71
-
72
- Returns all cached widget entries for a dashboard.
94
+ Transitions:
73
95
 
74
- ```json
75
- [
76
- {
77
- "id": "github:pull_requests",
78
- "connectorId": "github",
79
- "widgetId": "pull_requests",
80
- "data": [],
81
- "cachedAt": "2026-04-11T10:00:00.000Z"
82
- }
83
- ]
84
- ```
96
+ - `idle → queued → running → succeeded` (happy path; cloud may use `queued`, OSS skips it)
97
+ - `running → failed` (sets `lastError`)
98
+ - Any terminal state can transition back to `queued` / `running` on the next trigger.
85
99
 
86
- ### `GET /dashboards/:dashboardId/widgets/:widgetId`
100
+ Clients (`@rawdash/client`) poll `/sync/state` and wait for `!isSyncActive(status)` to settle.
87
101
 
88
- Returns a single widget by ID. Returns `404` if not found.
102
+ ## Engine without HTTP
89
103
 
90
- ### `POST /sync`
104
+ ```ts
105
+ import { createEngine } from '@rawdash/server';
91
106
 
92
- Triggers an immediate sync across all configured connectors. Returns `{ "triggered": false }` if a sync is already in progress.
107
+ const engine = createEngine(config, { storage });
108
+ const widgets = await engine.getWidgets('engineering');
109
+ const state = await engine.getSyncState();
110
+ ```
93
111
 
94
- ### `GET /health`
112
+ `createEngine` exposes the same shape as the handlers but bypasses HTTP entirely — useful for jobs, CLI tools, or the MCP server.
95
113
 
96
- Returns the current sync state.
114
+ ## Storage
97
115
 
98
- ```json
99
- {
100
- "status": "idle",
101
- "lastSyncAt": "2026-04-11T10:00:00.000Z",
102
- "lastError": null
103
- }
104
- ```
116
+ Provide any `ServerStorage` implementation:
105
117
 
106
- `status` is one of `"idle"`, `"syncing"`, or `"error"`.
118
+ - `InMemoryStorage` (re-exported here) dev/test.
119
+ - [`@rawdash/adapter-libsql`](https://www.npmjs.com/package/@rawdash/adapter-libsql) — durable libSQL/Turso/SQLite backend.
120
+ - Roll your own by implementing the [`ServerStorage`](https://github.com/rawdash/rawdash/blob/main/packages/core/src/server-storage.ts) interface.
107
121
 
108
122
  ## Links
109
123
 
110
124
  - [rawdash docs](https://rawdash.dev)
125
+ - [`@rawdash/hono`](https://www.npmjs.com/package/@rawdash/hono) — Hono adapter
126
+ - [`@rawdash/client`](https://www.npmjs.com/package/@rawdash/client) — typed HTTP client
111
127
  - [GitHub](https://github.com/rawdash/rawdash)
112
128
  - [Issues](https://github.com/rawdash/rawdash/issues)
113
129
 
package/dist/index.d.ts CHANGED
@@ -1,19 +1,99 @@
1
- import { ServerStorage, DashboardConfig, CachedWidget, SyncState, TriggerSyncResponse } from '@rawdash/core';
2
- export { DashboardConfig, InMemoryStorage, ServerStorage, computeMetric } from '@rawdash/core';
3
- import { Hono } from 'hono';
1
+ import { DashboardConfig, ServerStorage, HealthResponse, SyncState, CachedWidget, WidgetsListResponse, TriggerSyncResponse, RetentionConfig } from '@rawdash/core';
2
+ export { ACTIVE_SYNC_STATUSES, CachedWidget, ConfiguredConnector, DashboardConfig, HealthResponse, InMemoryStorage, ServerStorage, SyncState, SyncStatus, TriggerSyncResponse, WidgetsListResponse, computeMetric, isSyncActive } from '@rawdash/core';
4
3
 
5
- interface ServeOptions {
6
- port?: number;
7
- storage?: ServerStorage;
4
+ /**
5
+ * Per-request lookup functions an HTTP adapter passes to engine handlers.
6
+ *
7
+ * Adapters can close over per-request data (e.g. an id extracted from a
8
+ * path parameter or auth header) when constructing `getConfig` /
9
+ * `getStorage`. The handlers themselves only know how to operate on a
10
+ * given `DashboardConfig` and `ServerStorage` — they don't know or care
11
+ * how those are obtained.
12
+ */
13
+ interface EngineContext {
14
+ getConfig: () => DashboardConfig | Promise<DashboardConfig>;
15
+ getStorage: () => ServerStorage | Promise<ServerStorage>;
8
16
  }
9
17
 
10
- interface RouterMount {
11
- mount(app: Hono): void;
18
+ /**
19
+ * Thrown by `@rawdash/server` handlers when a request fails in a way the
20
+ * HTTP adapter should translate to a structured response (e.g. 404, 400).
21
+ * Framework adapters (`@rawdash/hono`, etc.) should catch this and map
22
+ * `status` to the appropriate HTTP status code.
23
+ */
24
+ declare class RawdashError extends Error {
25
+ readonly status: number;
26
+ readonly code: string;
27
+ constructor(status: number, code: string, message: string);
12
28
  }
29
+ declare function isRawdashError(err: unknown): err is RawdashError;
30
+
31
+ /**
32
+ * Canonical URL path conventions for the rawdash HTTP wire contract.
33
+ *
34
+ * Framework adapters (`@rawdash/hono`, etc.) and clients
35
+ * (`@rawdash/client`) should use these constants instead of hard-coding
36
+ * paths, so the contract stays in one place.
37
+ */
38
+ declare const ROUTES: {
39
+ readonly health: "/health";
40
+ readonly syncState: "/sync/state";
41
+ readonly sync: "/sync";
42
+ readonly retention: "/retention/retain";
43
+ readonly widgets: {
44
+ readonly list: (dashboardId: string) => string;
45
+ readonly single: (dashboardId: string, widgetId: string) => string;
46
+ };
47
+ };
13
48
 
14
- declare function createServer(routers: RouterMount[]): Hono;
49
+ /**
50
+ * Framework-agnostic request handlers for the rawdash wire contract.
51
+ *
52
+ * Each function takes an `EngineContext` (providing per-request access to
53
+ * the config + storage) and returns the response body, or throws a
54
+ * `RawdashError` on a client-visible failure. HTTP adapters
55
+ * (`@rawdash/hono`, etc.) wrap these in their framework's request/response
56
+ * cycle and translate `RawdashError` into a structured error response.
57
+ */
58
+ declare function getHealth(): HealthResponse;
59
+ declare function getSyncStateHandler(ctx: EngineContext): Promise<SyncState>;
60
+ declare function triggerSync(ctx: EngineContext): Promise<TriggerSyncResponse>;
61
+ declare function listWidgets(ctx: EngineContext, dashboardId: string): Promise<WidgetsListResponse>;
62
+ declare function getWidget(ctx: EngineContext, dashboardId: string, widgetId: string): Promise<CachedWidget>;
63
+ declare function runRetentionOnce(ctx: EngineContext): Promise<void>;
15
64
 
16
- declare function createEngineRouters(config: DashboardConfig, storage?: ServerStorage): RouterMount[];
65
+ declare const FULL_SYNC_TIMEOUT_MS = 300000;
66
+ /**
67
+ * Run a full sync across all connectors in the config in parallel via
68
+ * `Promise.allSettled`, so one failure doesn't abort the others. Each
69
+ * connector run is wrapped in a hard timeout (`FULL_SYNC_TIMEOUT_MS`)
70
+ * raced against the connector's own `Promise`, so a connector that
71
+ * ignores `AbortSignal` (or a storage call that hangs) can still not
72
+ * pin sync state in `running` indefinitely.
73
+ *
74
+ * The per-run storage handle is bound to the same `AbortController`, so
75
+ * once the timeout fires every subsequent write call on that handle
76
+ * becomes a no-op. That makes tail writes from a timed-out connector
77
+ * invisible to the next sync even if the connector itself keeps running
78
+ * — see `withAbortSignal` in `@rawdash/core` and the safety-net note in
79
+ * `docs/authoring-a-connector.md`.
80
+ *
81
+ * Transitions storage through `queued` → `running` → `succeeded`/`failed`.
82
+ * The `queued` step is a no-op if the caller (typically `triggerSync`)
83
+ * already marked the run as queued.
84
+ *
85
+ * Returns silently if another sync acquired the `running` lock first.
86
+ */
87
+ declare function runSync(config: DashboardConfig, storage: ServerStorage): Promise<void>;
88
+
89
+ declare const DEFAULT_RETENTION_INTERVAL_MS: number;
90
+ declare function hasPruningPolicy(config: RetentionConfig): boolean;
91
+ /**
92
+ * Apply the retention policy in `config` to every connector's stored data.
93
+ * No-op if the config has no pruning policy. Throws an aggregated error if
94
+ * any connector fails.
95
+ */
96
+ declare function runRetention(config: DashboardConfig, storage: ServerStorage): Promise<void>;
17
97
 
18
98
  interface EngineOptions {
19
99
  storage?: ServerStorage;
@@ -21,11 +101,10 @@ interface EngineOptions {
21
101
  interface Engine {
22
102
  getWidget(dashboardId: string, widgetId: string): Promise<CachedWidget | undefined>;
23
103
  getWidgets(dashboardId: string): Promise<CachedWidget[]>;
24
- getHealth(): Promise<SyncState>;
104
+ getHealth(): Promise<HealthResponse>;
105
+ getSyncState(): Promise<SyncState>;
25
106
  triggerSync(): Promise<TriggerSyncResponse>;
26
107
  }
27
108
  declare function createEngine(config: DashboardConfig, options?: EngineOptions): Engine;
28
109
 
29
- declare function serve(config: DashboardConfig, options?: ServeOptions): void;
30
-
31
- export { type Engine, type EngineOptions, type RouterMount, type ServeOptions, createEngine, createEngineRouters, createServer, serve };
110
+ export { DEFAULT_RETENTION_INTERVAL_MS, type Engine, type EngineContext, type EngineOptions, FULL_SYNC_TIMEOUT_MS, ROUTES, RawdashError, createEngine, getHealth, getSyncStateHandler, getWidget, hasPruningPolicy, isRawdashError, listWidgets, runRetention, runRetentionOnce, runSync, triggerSync };
package/dist/index.js CHANGED
@@ -1,116 +1,85 @@
1
- // src/index.ts
2
- import { serve as honoServe } from "@hono/node-server";
3
-
4
- // src/routers/health.ts
5
- var HealthRouter = class {
6
- constructor(storage) {
7
- this.storage = storage;
1
+ // src/errors.ts
2
+ var RawdashError = class extends Error {
3
+ constructor(status, code, message) {
4
+ super(message);
5
+ this.status = status;
6
+ this.code = code;
7
+ this.name = "RawdashError";
8
8
  }
9
- storage;
10
- mount(app) {
11
- app.get("/health", async (c) => {
12
- return c.json(await this.storage.getSyncState());
13
- });
9
+ status;
10
+ code;
11
+ };
12
+ function isRawdashError(err) {
13
+ return err instanceof RawdashError;
14
+ }
15
+
16
+ // src/routes.ts
17
+ var ROUTES = {
18
+ health: "/health",
19
+ syncState: "/sync/state",
20
+ sync: "/sync",
21
+ retention: "/retention/retain",
22
+ widgets: {
23
+ list: (dashboardId) => `/dashboards/${encodeURIComponent(dashboardId)}/widgets`,
24
+ single: (dashboardId, widgetId) => `/dashboards/${encodeURIComponent(dashboardId)}/widgets/${encodeURIComponent(widgetId)}`
14
25
  }
15
26
  };
16
27
 
17
- // src/routers/retention.ts
28
+ // src/handlers.ts
29
+ import { isSyncActive, resolveWidget } from "@rawdash/core";
30
+
31
+ // src/retention.ts
18
32
  import { selectForDeletion } from "@rawdash/core";
19
- var DEFAULT_INTERVAL_MS = 60 * 60 * 1e3;
33
+ var DEFAULT_RETENTION_INTERVAL_MS = 60 * 60 * 1e3;
20
34
  function hasPruningPolicy(config) {
21
35
  return config.maxAge !== void 0 || config.maxSize !== void 0;
22
36
  }
23
- var RetentionRouter = class {
24
- constructor(config, storage) {
25
- this.config = config;
26
- this.storage = storage;
27
- }
28
- config;
29
- storage;
30
- interval = null;
31
- inFlight = null;
32
- async runRetention() {
33
- if (this.inFlight) {
34
- return this.inFlight;
35
- }
36
- this.inFlight = this.runRetentionOnce().finally(() => {
37
- this.inFlight = null;
38
- });
39
- return this.inFlight;
37
+ async function runRetention(config, storage) {
38
+ const retentionConfig = config.retention;
39
+ if (!retentionConfig || !hasPruningPolicy(retentionConfig)) {
40
+ return;
40
41
  }
41
- async runRetentionOnce() {
42
- const retentionConfig = this.config.retention;
43
- if (!retentionConfig || !hasPruningPolicy(retentionConfig)) {
44
- return;
45
- }
46
- const nowMs = Date.now();
47
- const results = await Promise.allSettled(
48
- this.config.connectors.map(async ({ connector }) => {
49
- const handle = this.storage.getStorageHandle(connector.id);
50
- const [events, metrics, distributions] = await Promise.all([
51
- handle.queryEvents({}),
52
- handle.queryMetrics({}),
53
- handle.queryDistributions({})
54
- ]);
55
- await applyRetentionToShape(
56
- events,
57
- (e) => e.start_ts,
58
- retentionConfig,
59
- nowMs,
60
- (survivors, names) => handle.events(survivors, { names })
61
- );
62
- await applyRetentionToShape(
63
- metrics,
64
- (m) => m.ts,
65
- retentionConfig,
66
- nowMs,
67
- (survivors, names) => handle.metrics(survivors, { names })
68
- );
69
- await applyRetentionToShape(
70
- distributions,
71
- (d) => d.ts,
72
- retentionConfig,
73
- nowMs,
74
- (survivors, names) => handle.distributions(survivors, { names })
75
- );
76
- })
77
- );
78
- const failures = results.filter(
79
- (r) => r.status === "rejected"
80
- );
81
- if (failures.length > 0) {
82
- throw new Error(
83
- `Retention failed for ${failures.length} connector(s): ${failures.map((f) => String(f.reason)).join("; ")}`
42
+ const nowMs = Date.now();
43
+ const results = await Promise.allSettled(
44
+ config.connectors.map(async ({ connector }) => {
45
+ const handle = storage.getStorageHandle(connector.id);
46
+ const [events, metrics, distributions] = await Promise.all([
47
+ handle.queryEvents({}),
48
+ handle.queryMetrics({}),
49
+ handle.queryDistributions({})
50
+ ]);
51
+ await applyRetentionToShape(
52
+ events,
53
+ (e) => e.start_ts,
54
+ retentionConfig,
55
+ nowMs,
56
+ (survivors, names) => handle.events(survivors, { names })
84
57
  );
85
- }
86
- }
87
- mount(app) {
88
- app.post("/retain", async (c) => {
89
- try {
90
- await this.runRetention();
91
- return c.json({ triggered: true });
92
- } catch (err) {
93
- console.error("retention run failed", err);
94
- return c.json({ triggered: false }, 500);
95
- }
96
- });
97
- const retentionConfig = this.config.retention;
98
- if (retentionConfig && hasPruningPolicy(retentionConfig)) {
99
- const intervalMs = retentionConfig.intervalMs ?? DEFAULT_INTERVAL_MS;
100
- this.interval = setInterval(() => {
101
- void this.runRetention().catch((err) => {
102
- console.error("retention run failed", err);
103
- });
104
- }, intervalMs);
105
- }
106
- }
107
- stop() {
108
- if (this.interval !== null) {
109
- clearInterval(this.interval);
110
- this.interval = null;
111
- }
58
+ await applyRetentionToShape(
59
+ metrics,
60
+ (m) => m.ts,
61
+ retentionConfig,
62
+ nowMs,
63
+ (survivors, names) => handle.metrics(survivors, { names })
64
+ );
65
+ await applyRetentionToShape(
66
+ distributions,
67
+ (d) => d.ts,
68
+ retentionConfig,
69
+ nowMs,
70
+ (survivors, names) => handle.distributions(survivors, { names })
71
+ );
72
+ })
73
+ );
74
+ const failures = results.filter(
75
+ (r) => r.status === "rejected"
76
+ );
77
+ if (failures.length > 0) {
78
+ throw new Error(
79
+ `Retention failed for ${failures.length} connector(s): ${failures.map((f) => String(f.reason)).join("; ")}`
80
+ );
112
81
  }
113
- };
82
+ }
114
83
  async function applyRetentionToShape(rows, getTs, config, nowMs, writeSurvivors) {
115
84
  if (rows.length === 0) {
116
85
  return;
@@ -125,146 +94,138 @@ async function applyRetentionToShape(rows, getTs, config, nowMs, writeSurvivors)
125
94
  await writeSurvivors(survivors, allNames);
126
95
  }
127
96
 
128
- // src/routers/sync.ts
97
+ // src/sync.ts
129
98
  var FULL_SYNC_TIMEOUT_MS = 3e5;
130
- var SyncRouter = class {
131
- constructor(config, storage) {
132
- this.config = config;
133
- this.storage = storage;
99
+ async function runSync(config, storage) {
100
+ await storage.markSyncQueued();
101
+ const acquired = await storage.markSyncRunning();
102
+ if (!acquired) {
103
+ return;
134
104
  }
135
- config;
136
- storage;
137
- async runSync() {
138
- const acquired = await this.storage.setSyncing();
139
- if (!acquired) {
140
- return;
141
- }
142
- const errors = [];
143
- await Promise.allSettled(
144
- this.config.connectors.map(async ({ connector }) => {
145
- const handle = this.storage.getStorageHandle(connector.id);
146
- const controller = new AbortController();
147
- const timer = setTimeout(
148
- () => controller.abort(),
149
- FULL_SYNC_TIMEOUT_MS
105
+ const errors = [];
106
+ await Promise.allSettled(
107
+ config.connectors.map(async ({ connector }) => {
108
+ const controller = new AbortController();
109
+ const handle = storage.getStorageHandle(connector.id, {
110
+ signal: controller.signal
111
+ });
112
+ let timer;
113
+ try {
114
+ const syncPromise = connector.sync(
115
+ { mode: "full" },
116
+ handle,
117
+ controller.signal
150
118
  );
151
- try {
152
- const result = await connector.sync(
153
- { mode: "full" },
154
- handle,
155
- controller.signal
156
- );
157
- if (!result.done) {
158
- errors.push(
159
- `${connector.id} did not complete in one chunk (chunked syncs are only supported in cloud)`
160
- );
161
- }
162
- } catch (err) {
163
- if (err instanceof Error && err.name === "AbortError") {
164
- errors.push(
119
+ const timeoutPromise = new Promise((_resolve, reject) => {
120
+ timer = setTimeout(() => {
121
+ controller.abort();
122
+ const err = new Error(
165
123
  `${connector.id} timed out after ${FULL_SYNC_TIMEOUT_MS}ms`
166
124
  );
167
- } else {
168
- errors.push(err instanceof Error ? err.message : String(err));
169
- }
170
- } finally {
125
+ err.name = "AbortError";
126
+ reject(err);
127
+ }, FULL_SYNC_TIMEOUT_MS);
128
+ });
129
+ const result = await Promise.race([syncPromise, timeoutPromise]);
130
+ if (!result.done) {
131
+ errors.push(
132
+ `${connector.id} did not complete in one chunk (chunked syncs are only supported in cloud)`
133
+ );
134
+ }
135
+ } catch (err) {
136
+ if (err instanceof Error && err.name === "AbortError") {
137
+ errors.push(
138
+ `${connector.id} timed out after ${FULL_SYNC_TIMEOUT_MS}ms`
139
+ );
140
+ } else {
141
+ errors.push(err instanceof Error ? err.message : String(err));
142
+ }
143
+ } finally {
144
+ if (timer !== void 0) {
171
145
  clearTimeout(timer);
172
146
  }
173
- })
174
- );
175
- if (errors.length > 0) {
176
- await this.storage.setSyncError(errors.join("; "));
177
- } else {
178
- await this.storage.setSyncSuccess();
179
- }
180
- }
181
- mount(app) {
182
- app.post("/sync", async (c) => {
183
- const state = await this.storage.getSyncState();
184
- if (state.status === "syncing") {
185
- return c.json({ triggered: false });
186
147
  }
187
- void this.runSync();
188
- return c.json({ triggered: true });
189
- });
148
+ })
149
+ );
150
+ if (errors.length > 0) {
151
+ await storage.markSyncFailed(errors.join("; "));
152
+ } else {
153
+ await storage.markSyncSucceeded();
190
154
  }
191
- };
155
+ }
192
156
 
193
- // src/routers/widgets.ts
194
- import { resolveWidget } from "@rawdash/core";
195
- var WidgetsRouter = class {
196
- constructor(dashboardId, dashboard, connectors, storage) {
197
- this.dashboardId = dashboardId;
198
- this.dashboard = dashboard;
199
- this.connectors = connectors;
200
- this.storage = storage;
157
+ // src/handlers.ts
158
+ function getHealth() {
159
+ return { status: "ok" };
160
+ }
161
+ async function getSyncStateHandler(ctx) {
162
+ const storage = await ctx.getStorage();
163
+ return storage.getSyncState();
164
+ }
165
+ async function triggerSync(ctx) {
166
+ const storage = await ctx.getStorage();
167
+ const state = await storage.getSyncState();
168
+ if (isSyncActive(state.status)) {
169
+ return { queued: false };
201
170
  }
202
- dashboardId;
203
- dashboard;
204
- connectors;
205
- storage;
206
- resolve(id, widget) {
207
- return resolveWidget(id, widget, this.connectors, this.storage);
171
+ const config = await ctx.getConfig();
172
+ const queued = await storage.markSyncQueued();
173
+ if (!queued) {
174
+ return { queued: false };
208
175
  }
209
- mount(app) {
210
- const base = `/dashboards/${this.dashboardId}/widgets`;
211
- app.get(base, async (c) => {
212
- const entries = Object.entries(this.dashboard.widgets);
213
- const resolved = await Promise.all(
214
- entries.map(([key, widget]) => this.resolve(key, widget))
215
- );
216
- const widgets = resolved.filter(
217
- (w) => w !== void 0
218
- );
219
- const response = { widgets };
220
- return c.json(response);
221
- });
222
- app.get(`${base}/:widgetId`, async (c) => {
223
- const widgetId = c.req.param("widgetId");
224
- const widget = this.dashboard.widgets[widgetId];
225
- if (!widget) {
226
- return c.json({ error: "Widget not found" }, 404);
227
- }
228
- const result = await this.resolve(widgetId, widget);
229
- if (!result) {
230
- return c.json({ error: "Widget not found" }, 404);
231
- }
232
- return c.json(result);
233
- });
176
+ void runSync(config, storage).catch((err) => {
177
+ console.error("Rawdash sync failed", err);
178
+ });
179
+ return { queued: true };
180
+ }
181
+ async function listWidgets(ctx, dashboardId) {
182
+ const config = await ctx.getConfig();
183
+ const dashboard = config.dashboards[dashboardId];
184
+ if (!dashboard) {
185
+ throw new RawdashError(404, "DASHBOARD_NOT_FOUND", "Dashboard not found");
234
186
  }
235
- };
236
-
237
- // src/storage.ts
238
- import { InMemoryStorage } from "@rawdash/core";
239
-
240
- // src/engine-router.ts
241
- function createEngineRouters(config, storage = new InMemoryStorage()) {
242
- const widgetRouters = Object.entries(config.dashboards).map(
243
- ([dashboardId, dashboard]) => new WidgetsRouter(dashboardId, dashboard, config.connectors, storage)
187
+ const storage = await ctx.getStorage();
188
+ const entries = Object.entries(dashboard.widgets);
189
+ const resolved = await Promise.all(
190
+ entries.map(
191
+ ([key, widget]) => resolveWidget(key, widget, config.connectors, storage)
192
+ )
244
193
  );
245
- return [
246
- ...widgetRouters,
247
- new SyncRouter(config, storage),
248
- new RetentionRouter(config, storage),
249
- new HealthRouter(storage)
250
- ];
194
+ const widgets = resolved.filter((w) => w !== void 0);
195
+ return { widgets };
251
196
  }
252
-
253
- // src/server.ts
254
- import { Hono } from "hono";
255
- function createServer(routers) {
256
- const app = new Hono();
257
- for (const router of routers) {
258
- router.mount(app);
197
+ async function getWidget(ctx, dashboardId, widgetId) {
198
+ const config = await ctx.getConfig();
199
+ const dashboard = config.dashboards[dashboardId];
200
+ if (!dashboard) {
201
+ throw new RawdashError(404, "DASHBOARD_NOT_FOUND", "Dashboard not found");
259
202
  }
260
- return app;
203
+ const widget = dashboard.widgets[widgetId];
204
+ if (!widget) {
205
+ throw new RawdashError(404, "WIDGET_NOT_FOUND", "Widget not found");
206
+ }
207
+ const storage = await ctx.getStorage();
208
+ const result = await resolveWidget(
209
+ widgetId,
210
+ widget,
211
+ config.connectors,
212
+ storage
213
+ );
214
+ if (!result) {
215
+ throw new RawdashError(404, "WIDGET_NOT_FOUND", "Widget not found");
216
+ }
217
+ return result;
218
+ }
219
+ async function runRetentionOnce(ctx) {
220
+ const config = await ctx.getConfig();
221
+ const storage = await ctx.getStorage();
222
+ await runRetention(config, storage);
261
223
  }
262
224
 
263
225
  // src/engine.ts
264
- import { InMemoryStorage as InMemoryStorage2, resolveWidget as resolveWidget2 } from "@rawdash/core";
226
+ import { InMemoryStorage, isSyncActive as isSyncActive2, resolveWidget as resolveWidget2 } from "@rawdash/core";
265
227
  function createEngine(config, options = {}) {
266
- const storage = options.storage ?? new InMemoryStorage2();
267
- const syncRouter = new SyncRouter(config, storage);
228
+ const storage = options.storage ?? new InMemoryStorage();
268
229
  return {
269
230
  async getWidget(dashboardId, widgetId) {
270
231
  const dashboard = config.dashboards[dashboardId];
@@ -291,17 +252,24 @@ function createEngine(config, options = {}) {
291
252
  return resolved.filter((w) => w !== void 0);
292
253
  },
293
254
  async getHealth() {
255
+ return { status: "ok" };
256
+ },
257
+ async getSyncState() {
294
258
  return storage.getSyncState();
295
259
  },
296
260
  async triggerSync() {
297
261
  const state = await storage.getSyncState();
298
- if (state.status === "syncing") {
299
- return { triggered: false };
262
+ if (isSyncActive2(state.status)) {
263
+ return { queued: false };
300
264
  }
301
- void syncRouter.runSync().catch((error) => {
265
+ const queued = await storage.markSyncQueued();
266
+ if (!queued) {
267
+ return { queued: false };
268
+ }
269
+ void runSync(config, storage).catch((error) => {
302
270
  console.error("Rawdash sync failed", error);
303
271
  });
304
- return { triggered: true };
272
+ return { queued: true };
305
273
  }
306
274
  };
307
275
  }
@@ -309,18 +277,30 @@ function createEngine(config, options = {}) {
309
277
  // src/compute.ts
310
278
  import { computeMetric } from "@rawdash/core";
311
279
 
280
+ // src/storage.ts
281
+ import { InMemoryStorage as InMemoryStorage2 } from "@rawdash/core";
282
+
312
283
  // src/index.ts
313
- function serve(config, options = {}) {
314
- const { port = 8080, storage } = options;
315
- const app = createServer(createEngineRouters(config, storage));
316
- honoServe({ fetch: app.fetch, port });
317
- }
284
+ import { isSyncActive as isSyncActive3, ACTIVE_SYNC_STATUSES } from "@rawdash/core";
318
285
  export {
319
- InMemoryStorage,
286
+ ACTIVE_SYNC_STATUSES,
287
+ DEFAULT_RETENTION_INTERVAL_MS,
288
+ FULL_SYNC_TIMEOUT_MS,
289
+ InMemoryStorage2 as InMemoryStorage,
290
+ ROUTES,
291
+ RawdashError,
320
292
  computeMetric,
321
293
  createEngine,
322
- createEngineRouters,
323
- createServer,
324
- serve
294
+ getHealth,
295
+ getSyncStateHandler,
296
+ getWidget,
297
+ hasPruningPolicy,
298
+ isRawdashError,
299
+ isSyncActive3 as isSyncActive,
300
+ listWidgets,
301
+ runRetention,
302
+ runRetentionOnce,
303
+ runSync,
304
+ triggerSync
325
305
  };
326
306
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/routers/health.ts","../src/routers/retention.ts","../src/routers/sync.ts","../src/routers/widgets.ts","../src/storage.ts","../src/engine-router.ts","../src/server.ts","../src/engine.ts","../src/compute.ts"],"sourcesContent":["import { serve as honoServe } from '@hono/node-server';\n\nimport { createEngineRouters } from './engine-router';\nimport { createServer } from './server';\nimport type { DashboardConfig, ServeOptions } from './types';\n\nexport { createServer } from './server';\nexport { createEngineRouters } from './engine-router';\nexport { createEngine } from './engine';\nexport { computeMetric } from './compute';\nexport { InMemoryStorage } from './storage';\nexport type { RouterMount } from './router';\nexport type { DashboardConfig, ServeOptions, ServerStorage } from './types';\nexport type { Engine, EngineOptions } from './engine';\n\nexport function serve(\n config: DashboardConfig,\n options: ServeOptions = {},\n): void {\n const { port = 8080, storage } = options;\n const app = createServer(createEngineRouters(config, storage));\n honoServe({ fetch: app.fetch, port });\n}\n","import type { Hono } from 'hono';\n\nimport type { RouterMount } from '../router';\nimport type { ServerStorage } from '../types';\n\nexport class HealthRouter implements RouterMount {\n constructor(private storage: ServerStorage) {}\n\n mount(app: Hono): void {\n app.get('/health', async (c) => {\n return c.json(await this.storage.getSyncState());\n });\n }\n}\n","import type { DashboardConfig, RetentionConfig } from '@rawdash/core';\nimport { selectForDeletion } from '@rawdash/core';\nimport type { Hono } from 'hono';\n\nimport type { RouterMount } from '../router';\nimport type { ServerStorage } from '../types';\n\nconst DEFAULT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour\n\nfunction hasPruningPolicy(config: RetentionConfig): boolean {\n return config.maxAge !== undefined || config.maxSize !== undefined;\n}\n\nexport class RetentionRouter implements RouterMount {\n private interval: ReturnType<typeof setInterval> | null = null;\n private inFlight: Promise<void> | null = null;\n\n constructor(\n private config: DashboardConfig,\n private storage: ServerStorage,\n ) {}\n\n async runRetention(): Promise<void> {\n if (this.inFlight) {\n return this.inFlight;\n }\n\n this.inFlight = this.runRetentionOnce().finally(() => {\n this.inFlight = null;\n });\n\n return this.inFlight;\n }\n\n private async runRetentionOnce(): Promise<void> {\n const retentionConfig = this.config.retention;\n if (!retentionConfig || !hasPruningPolicy(retentionConfig)) {\n return;\n }\n\n const nowMs = Date.now();\n\n const results = await Promise.allSettled(\n this.config.connectors.map(async ({ connector }) => {\n const handle = this.storage.getStorageHandle(connector.id);\n\n const [events, metrics, distributions] = await Promise.all([\n handle.queryEvents({}),\n handle.queryMetrics({}),\n handle.queryDistributions({}),\n ]);\n\n await applyRetentionToShape(\n events,\n (e) => e.start_ts,\n retentionConfig,\n nowMs,\n (survivors, names) => handle.events(survivors, { names }),\n );\n\n await applyRetentionToShape(\n metrics,\n (m) => m.ts,\n retentionConfig,\n nowMs,\n (survivors, names) => handle.metrics(survivors, { names }),\n );\n\n await applyRetentionToShape(\n distributions,\n (d) => d.ts,\n retentionConfig,\n nowMs,\n (survivors, names) => handle.distributions(survivors, { names }),\n );\n }),\n );\n\n const failures = results.filter(\n (r): r is PromiseRejectedResult => r.status === 'rejected',\n );\n if (failures.length > 0) {\n throw new Error(\n `Retention failed for ${failures.length} connector(s): ${failures.map((f) => String(f.reason)).join('; ')}`,\n );\n }\n }\n\n mount(app: Hono): void {\n app.post('/retain', async (c) => {\n try {\n await this.runRetention();\n return c.json({ triggered: true });\n } catch (err) {\n console.error('retention run failed', err);\n return c.json({ triggered: false }, 500);\n }\n });\n\n const retentionConfig = this.config.retention;\n if (retentionConfig && hasPruningPolicy(retentionConfig)) {\n const intervalMs = retentionConfig.intervalMs ?? DEFAULT_INTERVAL_MS;\n this.interval = setInterval(() => {\n void this.runRetention().catch((err) => {\n console.error('retention run failed', err);\n });\n }, intervalMs);\n }\n }\n\n stop(): void {\n if (this.interval !== null) {\n clearInterval(this.interval);\n this.interval = null;\n }\n }\n}\n\nasync function applyRetentionToShape<T extends { name: string }>(\n rows: T[],\n getTs: (row: T) => number,\n config: RetentionConfig,\n nowMs: number,\n writeSurvivors: (survivors: T[], names: string[]) => Promise<void>,\n): Promise<void> {\n if (rows.length === 0) {\n return;\n }\n\n const sorted = [...rows].sort((a, b) => getTs(b) - getTs(a));\n const toDeleteSet = new Set(selectForDeletion(sorted, getTs, config, nowMs));\n\n if (toDeleteSet.size === 0) {\n return;\n }\n\n const survivors = sorted.filter((r) => !toDeleteSet.has(r));\n const allNames = [...new Set(rows.map((r) => r.name))];\n\n await writeSurvivors(survivors, allNames);\n}\n","import type { DashboardConfig } from '@rawdash/core';\nimport type { Hono } from 'hono';\n\nimport type { RouterMount } from '../router';\nimport type { ServerStorage } from '../types';\n\nconst FULL_SYNC_TIMEOUT_MS = 300_000;\n\nexport class SyncRouter implements RouterMount {\n constructor(\n private config: DashboardConfig,\n private storage: ServerStorage,\n ) {}\n\n async runSync(): Promise<void> {\n const acquired = await this.storage.setSyncing();\n if (!acquired) {\n return;\n }\n const errors: string[] = [];\n await Promise.allSettled(\n this.config.connectors.map(async ({ connector }) => {\n const handle = this.storage.getStorageHandle(connector.id);\n const controller = new AbortController();\n const timer = setTimeout(\n () => controller.abort(),\n FULL_SYNC_TIMEOUT_MS,\n );\n try {\n const result = await connector.sync(\n { mode: 'full' },\n handle,\n controller.signal,\n );\n if (!result.done) {\n errors.push(\n `${connector.id} did not complete in one chunk (chunked syncs are only supported in cloud)`,\n );\n }\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n errors.push(\n `${connector.id} timed out after ${FULL_SYNC_TIMEOUT_MS}ms`,\n );\n } else {\n errors.push(err instanceof Error ? err.message : String(err));\n }\n } finally {\n clearTimeout(timer);\n }\n }),\n );\n if (errors.length > 0) {\n await this.storage.setSyncError(errors.join('; '));\n } else {\n await this.storage.setSyncSuccess();\n }\n }\n\n mount(app: Hono): void {\n app.post('/sync', async (c) => {\n const state = await this.storage.getSyncState();\n if (state.status === 'syncing') {\n return c.json({ triggered: false });\n }\n void this.runSync();\n return c.json({ triggered: true });\n });\n }\n}\n","import type {\n CachedWidget,\n ConfiguredConnector,\n Dashboard,\n Widget,\n WidgetsListResponse,\n} from '@rawdash/core';\nimport { resolveWidget } from '@rawdash/core';\nimport type { Hono } from 'hono';\n\nimport type { RouterMount } from '../router';\nimport type { ServerStorage } from '../types';\n\nexport class WidgetsRouter implements RouterMount {\n constructor(\n private dashboardId: string,\n private dashboard: Dashboard,\n private connectors: ConfiguredConnector[],\n private storage: ServerStorage,\n ) {}\n\n private resolve(\n id: string,\n widget: Widget,\n ): Promise<CachedWidget | undefined> {\n return resolveWidget(id, widget, this.connectors, this.storage);\n }\n\n mount(app: Hono): void {\n const base = `/dashboards/${this.dashboardId}/widgets`;\n\n app.get(base, async (c) => {\n const entries = Object.entries(this.dashboard.widgets);\n const resolved = await Promise.all(\n entries.map(([key, widget]) => this.resolve(key, widget)),\n );\n const widgets = resolved.filter(\n (w): w is CachedWidget => w !== undefined,\n );\n const response: WidgetsListResponse = { widgets };\n return c.json(response);\n });\n\n app.get(`${base}/:widgetId`, async (c) => {\n const widgetId = c.req.param('widgetId');\n const widget = this.dashboard.widgets[widgetId];\n if (!widget) {\n return c.json({ error: 'Widget not found' }, 404);\n }\n const result = await this.resolve(widgetId, widget);\n if (!result) {\n return c.json({ error: 'Widget not found' }, 404);\n }\n return c.json(result);\n });\n }\n}\n","export { InMemoryStorage } from '@rawdash/core';\n","import type { DashboardConfig } from '@rawdash/core';\n\nimport type { RouterMount } from './router';\nimport { HealthRouter } from './routers/health';\nimport { RetentionRouter } from './routers/retention';\nimport { SyncRouter } from './routers/sync';\nimport { WidgetsRouter } from './routers/widgets';\nimport { InMemoryStorage } from './storage';\nimport type { ServerStorage } from './types';\n\nexport function createEngineRouters(\n config: DashboardConfig,\n storage: ServerStorage = new InMemoryStorage(),\n): RouterMount[] {\n const widgetRouters = Object.entries(config.dashboards).map(\n ([dashboardId, dashboard]) =>\n new WidgetsRouter(dashboardId, dashboard, config.connectors, storage),\n );\n\n return [\n ...widgetRouters,\n new SyncRouter(config, storage),\n new RetentionRouter(config, storage),\n new HealthRouter(storage),\n ];\n}\n","import { Hono } from 'hono';\n\nimport type { RouterMount } from './router';\n\nexport function createServer(routers: RouterMount[]): Hono {\n const app = new Hono();\n for (const router of routers) {\n router.mount(app);\n }\n return app;\n}\n","import type {\n CachedWidget,\n DashboardConfig,\n SyncState,\n TriggerSyncResponse,\n} from '@rawdash/core';\nimport { InMemoryStorage, resolveWidget } from '@rawdash/core';\n\nimport { SyncRouter } from './routers/sync';\nimport type { ServerStorage } from './types';\n\nexport interface EngineOptions {\n storage?: ServerStorage;\n}\n\nexport interface Engine {\n getWidget(\n dashboardId: string,\n widgetId: string,\n ): Promise<CachedWidget | undefined>;\n getWidgets(dashboardId: string): Promise<CachedWidget[]>;\n getHealth(): Promise<SyncState>;\n triggerSync(): Promise<TriggerSyncResponse>;\n}\n\nexport function createEngine(\n config: DashboardConfig,\n options: EngineOptions = {},\n): Engine {\n const storage: ServerStorage = options.storage ?? new InMemoryStorage();\n const syncRouter = new SyncRouter(config, storage);\n\n return {\n async getWidget(dashboardId, widgetId) {\n const dashboard = config.dashboards[dashboardId];\n if (!dashboard) {\n return undefined;\n }\n const widget = dashboard.widgets[widgetId];\n if (!widget) {\n return undefined;\n }\n return resolveWidget(widgetId, widget, config.connectors, storage);\n },\n\n async getWidgets(dashboardId) {\n const dashboard = config.dashboards[dashboardId];\n if (!dashboard) {\n return [];\n }\n const entries = Object.entries(dashboard.widgets);\n const resolved = await Promise.all(\n entries.map(([key, widget]) =>\n resolveWidget(key, widget, config.connectors, storage),\n ),\n );\n return resolved.filter((w): w is CachedWidget => w !== undefined);\n },\n\n async getHealth() {\n return storage.getSyncState();\n },\n\n async triggerSync() {\n const state = await storage.getSyncState();\n if (state.status === 'syncing') {\n return { triggered: false };\n }\n void syncRouter.runSync().catch((error) => {\n console.error('Rawdash sync failed', error);\n });\n return { triggered: true };\n },\n };\n}\n","export { computeMetric } from '@rawdash/core';\n"],"mappings":"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","names":["InMemoryStorage","resolveWidget","InMemoryStorage","resolveWidget"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/routes.ts","../src/handlers.ts","../src/retention.ts","../src/sync.ts","../src/engine.ts","../src/compute.ts","../src/storage.ts","../src/index.ts"],"sourcesContent":["/**\n * Thrown by `@rawdash/server` handlers when a request fails in a way the\n * HTTP adapter should translate to a structured response (e.g. 404, 400).\n * Framework adapters (`@rawdash/hono`, etc.) should catch this and map\n * `status` to the appropriate HTTP status code.\n */\nexport class RawdashError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n message: string,\n ) {\n super(message);\n this.name = 'RawdashError';\n }\n}\n\nexport function isRawdashError(err: unknown): err is RawdashError {\n return err instanceof RawdashError;\n}\n","/**\n * Canonical URL path conventions for the rawdash HTTP wire contract.\n *\n * Framework adapters (`@rawdash/hono`, etc.) and clients\n * (`@rawdash/client`) should use these constants instead of hard-coding\n * paths, so the contract stays in one place.\n */\nexport const ROUTES = {\n health: '/health',\n syncState: '/sync/state',\n sync: '/sync',\n retention: '/retention/retain',\n widgets: {\n list: (dashboardId: string): string =>\n `/dashboards/${encodeURIComponent(dashboardId)}/widgets`,\n single: (dashboardId: string, widgetId: string): string =>\n `/dashboards/${encodeURIComponent(dashboardId)}/widgets/${encodeURIComponent(widgetId)}`,\n },\n} as const;\n","import type {\n CachedWidget,\n HealthResponse,\n SyncState,\n TriggerSyncResponse,\n WidgetsListResponse,\n} from '@rawdash/core';\nimport { isSyncActive, resolveWidget } from '@rawdash/core';\n\nimport type { EngineContext } from './context';\nimport { RawdashError } from './errors';\nimport { runRetention } from './retention';\nimport { runSync } from './sync';\n\n/**\n * Framework-agnostic request handlers for the rawdash wire contract.\n *\n * Each function takes an `EngineContext` (providing per-request access to\n * the config + storage) and returns the response body, or throws a\n * `RawdashError` on a client-visible failure. HTTP adapters\n * (`@rawdash/hono`, etc.) wrap these in their framework's request/response\n * cycle and translate `RawdashError` into a structured error response.\n */\n\nexport function getHealth(): HealthResponse {\n return { status: 'ok' };\n}\n\nexport async function getSyncStateHandler(\n ctx: EngineContext,\n): Promise<SyncState> {\n const storage = await ctx.getStorage();\n return storage.getSyncState();\n}\n\nexport async function triggerSync(\n ctx: EngineContext,\n): Promise<TriggerSyncResponse> {\n const storage = await ctx.getStorage();\n const state = await storage.getSyncState();\n if (isSyncActive(state.status)) {\n return { queued: false };\n }\n // Load the config *before* marking queued — if config loading rejects\n // after we've persisted the queued transition, the sync state would be\n // stuck in `queued` with no background run to drain it.\n const config = await ctx.getConfig();\n // Persist the queued transition synchronously so clients polling\n // /sync/state right after the trigger see `queued` (with queuedAt),\n // not a stale terminal state.\n const queued = await storage.markSyncQueued();\n if (!queued) {\n return { queued: false };\n }\n void runSync(config, storage).catch((err) => {\n console.error('Rawdash sync failed', err);\n });\n return { queued: true };\n}\n\nexport async function listWidgets(\n ctx: EngineContext,\n dashboardId: string,\n): Promise<WidgetsListResponse> {\n const config = await ctx.getConfig();\n const dashboard = config.dashboards[dashboardId];\n if (!dashboard) {\n throw new RawdashError(404, 'DASHBOARD_NOT_FOUND', 'Dashboard not found');\n }\n const storage = await ctx.getStorage();\n const entries = Object.entries(dashboard.widgets);\n const resolved = await Promise.all(\n entries.map(([key, widget]) =>\n resolveWidget(key, widget, config.connectors, storage),\n ),\n );\n const widgets = resolved.filter((w): w is CachedWidget => w !== undefined);\n return { widgets };\n}\n\nexport async function getWidget(\n ctx: EngineContext,\n dashboardId: string,\n widgetId: string,\n): Promise<CachedWidget> {\n const config = await ctx.getConfig();\n const dashboard = config.dashboards[dashboardId];\n if (!dashboard) {\n throw new RawdashError(404, 'DASHBOARD_NOT_FOUND', 'Dashboard not found');\n }\n const widget = dashboard.widgets[widgetId];\n if (!widget) {\n throw new RawdashError(404, 'WIDGET_NOT_FOUND', 'Widget not found');\n }\n const storage = await ctx.getStorage();\n const result = await resolveWidget(\n widgetId,\n widget,\n config.connectors,\n storage,\n );\n if (!result) {\n throw new RawdashError(404, 'WIDGET_NOT_FOUND', 'Widget not found');\n }\n return result;\n}\n\nexport async function runRetentionOnce(ctx: EngineContext): Promise<void> {\n const config = await ctx.getConfig();\n const storage = await ctx.getStorage();\n await runRetention(config, storage);\n}\n","import type {\n DashboardConfig,\n RetentionConfig,\n ServerStorage,\n} from '@rawdash/core';\nimport { selectForDeletion } from '@rawdash/core';\n\nexport const DEFAULT_RETENTION_INTERVAL_MS = 60 * 60 * 1000; // 1 hour\n\nexport function hasPruningPolicy(config: RetentionConfig): boolean {\n return config.maxAge !== undefined || config.maxSize !== undefined;\n}\n\n/**\n * Apply the retention policy in `config` to every connector's stored data.\n * No-op if the config has no pruning policy. Throws an aggregated error if\n * any connector fails.\n */\nexport async function runRetention(\n config: DashboardConfig,\n storage: ServerStorage,\n): Promise<void> {\n const retentionConfig = config.retention;\n if (!retentionConfig || !hasPruningPolicy(retentionConfig)) {\n return;\n }\n\n const nowMs = Date.now();\n\n const results = await Promise.allSettled(\n config.connectors.map(async ({ connector }) => {\n const handle = storage.getStorageHandle(connector.id);\n\n const [events, metrics, distributions] = await Promise.all([\n handle.queryEvents({}),\n handle.queryMetrics({}),\n handle.queryDistributions({}),\n ]);\n\n await applyRetentionToShape(\n events,\n (e) => e.start_ts,\n retentionConfig,\n nowMs,\n (survivors, names) => handle.events(survivors, { names }),\n );\n\n await applyRetentionToShape(\n metrics,\n (m) => m.ts,\n retentionConfig,\n nowMs,\n (survivors, names) => handle.metrics(survivors, { names }),\n );\n\n await applyRetentionToShape(\n distributions,\n (d) => d.ts,\n retentionConfig,\n nowMs,\n (survivors, names) => handle.distributions(survivors, { names }),\n );\n }),\n );\n\n const failures = results.filter(\n (r): r is PromiseRejectedResult => r.status === 'rejected',\n );\n if (failures.length > 0) {\n throw new Error(\n `Retention failed for ${failures.length} connector(s): ${failures.map((f) => String(f.reason)).join('; ')}`,\n );\n }\n}\n\nasync function applyRetentionToShape<T extends { name: string }>(\n rows: T[],\n getTs: (row: T) => number,\n config: RetentionConfig,\n nowMs: number,\n writeSurvivors: (survivors: T[], names: string[]) => Promise<void>,\n): Promise<void> {\n if (rows.length === 0) {\n return;\n }\n\n const sorted = [...rows].sort((a, b) => getTs(b) - getTs(a));\n const toDeleteSet = new Set(selectForDeletion(sorted, getTs, config, nowMs));\n\n if (toDeleteSet.size === 0) {\n return;\n }\n\n const survivors = sorted.filter((r) => !toDeleteSet.has(r));\n const allNames = [...new Set(rows.map((r) => r.name))];\n\n await writeSurvivors(survivors, allNames);\n}\n","import type { DashboardConfig, ServerStorage } from '@rawdash/core';\n\nexport const FULL_SYNC_TIMEOUT_MS = 300_000;\n\n/**\n * Run a full sync across all connectors in the config in parallel via\n * `Promise.allSettled`, so one failure doesn't abort the others. Each\n * connector run is wrapped in a hard timeout (`FULL_SYNC_TIMEOUT_MS`)\n * raced against the connector's own `Promise`, so a connector that\n * ignores `AbortSignal` (or a storage call that hangs) can still not\n * pin sync state in `running` indefinitely.\n *\n * The per-run storage handle is bound to the same `AbortController`, so\n * once the timeout fires every subsequent write call on that handle\n * becomes a no-op. That makes tail writes from a timed-out connector\n * invisible to the next sync even if the connector itself keeps running\n * — see `withAbortSignal` in `@rawdash/core` and the safety-net note in\n * `docs/authoring-a-connector.md`.\n *\n * Transitions storage through `queued` → `running` → `succeeded`/`failed`.\n * The `queued` step is a no-op if the caller (typically `triggerSync`)\n * already marked the run as queued.\n *\n * Returns silently if another sync acquired the `running` lock first.\n */\nexport async function runSync(\n config: DashboardConfig,\n storage: ServerStorage,\n): Promise<void> {\n // Idempotent: if the caller already queued, this returns false and we\n // proceed to markSyncRunning anyway. If nothing queued us, we queue\n // ourselves now so the state machine still goes through `queued`.\n await storage.markSyncQueued();\n const acquired = await storage.markSyncRunning();\n if (!acquired) {\n return;\n }\n const errors: string[] = [];\n await Promise.allSettled(\n config.connectors.map(async ({ connector }) => {\n const controller = new AbortController();\n const handle = storage.getStorageHandle(connector.id, {\n signal: controller.signal,\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n const syncPromise = connector.sync(\n { mode: 'full' },\n handle,\n controller.signal,\n );\n const timeoutPromise = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n controller.abort();\n const err = new Error(\n `${connector.id} timed out after ${FULL_SYNC_TIMEOUT_MS}ms`,\n );\n err.name = 'AbortError';\n reject(err);\n }, FULL_SYNC_TIMEOUT_MS);\n });\n const result = await Promise.race([syncPromise, timeoutPromise]);\n if (!result.done) {\n errors.push(\n `${connector.id} did not complete in one chunk (chunked syncs are only supported in cloud)`,\n );\n }\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n errors.push(\n `${connector.id} timed out after ${FULL_SYNC_TIMEOUT_MS}ms`,\n );\n } else {\n errors.push(err instanceof Error ? err.message : String(err));\n }\n } finally {\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n }\n }),\n );\n if (errors.length > 0) {\n await storage.markSyncFailed(errors.join('; '));\n } else {\n await storage.markSyncSucceeded();\n }\n}\n","import type {\n CachedWidget,\n DashboardConfig,\n HealthResponse,\n ServerStorage,\n SyncState,\n TriggerSyncResponse,\n} from '@rawdash/core';\nimport { InMemoryStorage, isSyncActive, resolveWidget } from '@rawdash/core';\n\nimport { runSync } from './sync';\n\nexport interface EngineOptions {\n storage?: ServerStorage;\n}\n\nexport interface Engine {\n getWidget(\n dashboardId: string,\n widgetId: string,\n ): Promise<CachedWidget | undefined>;\n getWidgets(dashboardId: string): Promise<CachedWidget[]>;\n getHealth(): Promise<HealthResponse>;\n getSyncState(): Promise<SyncState>;\n triggerSync(): Promise<TriggerSyncResponse>;\n}\n\nexport function createEngine(\n config: DashboardConfig,\n options: EngineOptions = {},\n): Engine {\n const storage: ServerStorage = options.storage ?? new InMemoryStorage();\n\n return {\n async getWidget(dashboardId, widgetId) {\n const dashboard = config.dashboards[dashboardId];\n if (!dashboard) {\n return undefined;\n }\n const widget = dashboard.widgets[widgetId];\n if (!widget) {\n return undefined;\n }\n return resolveWidget(widgetId, widget, config.connectors, storage);\n },\n\n async getWidgets(dashboardId) {\n const dashboard = config.dashboards[dashboardId];\n if (!dashboard) {\n return [];\n }\n const entries = Object.entries(dashboard.widgets);\n const resolved = await Promise.all(\n entries.map(([key, widget]) =>\n resolveWidget(key, widget, config.connectors, storage),\n ),\n );\n return resolved.filter((w): w is CachedWidget => w !== undefined);\n },\n\n async getHealth() {\n return { status: 'ok' };\n },\n\n async getSyncState() {\n return storage.getSyncState();\n },\n\n async triggerSync() {\n const state = await storage.getSyncState();\n if (isSyncActive(state.status)) {\n return { queued: false };\n }\n const queued = await storage.markSyncQueued();\n if (!queued) {\n return { queued: false };\n }\n void runSync(config, storage).catch((error) => {\n console.error('Rawdash sync failed', error);\n });\n return { queued: true };\n },\n };\n}\n","export { computeMetric } from '@rawdash/core';\n","export { InMemoryStorage } from '@rawdash/core';\n","export type { EngineContext } from './context';\nexport { RawdashError, isRawdashError } from './errors';\nexport { ROUTES } from './routes';\nexport {\n getHealth,\n getSyncStateHandler,\n getWidget,\n listWidgets,\n runRetentionOnce,\n triggerSync,\n} from './handlers';\nexport { runSync, FULL_SYNC_TIMEOUT_MS } from './sync';\nexport {\n runRetention,\n hasPruningPolicy,\n DEFAULT_RETENTION_INTERVAL_MS,\n} from './retention';\nexport { createEngine } from './engine';\nexport type { Engine, EngineOptions } from './engine';\nexport { computeMetric } from './compute';\nexport { InMemoryStorage } from './storage';\nexport type {\n CachedWidget,\n ConfiguredConnector,\n DashboardConfig,\n HealthResponse,\n ServerStorage,\n SyncState,\n SyncStatus,\n TriggerSyncResponse,\n WidgetsListResponse,\n} from './types';\nexport { isSyncActive, ACTIVE_SYNC_STATUSES } from '@rawdash/core';\n"],"mappings":"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","names":["isSyncActive","resolveWidget","resolveWidget","isSyncActive","InMemoryStorage","isSyncActive"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rawdash/server",
3
- "version": "0.13.0",
4
- "description": "Rawdash standalone Hono server",
3
+ "version": "0.14.0",
4
+ "description": "Framework-agnostic rawdash request handlers, engine, and wire contract. Wrap with @rawdash/hono (or another adapter) to serve over HTTP.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "repository": {
@@ -22,18 +22,14 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@hono/node-server": "^1.13.7",
26
- "hono": "^4.7.7",
27
- "@rawdash/core": "0.13.0"
25
+ "@rawdash/core": "0.14.0"
28
26
  },
29
27
  "devDependencies": {
30
28
  "tsup": "^8.0.0",
31
- "tsx": "^4.19.2",
32
29
  "typescript": "^5.7.2"
33
30
  },
34
31
  "scripts": {
35
32
  "build": "tsup",
36
- "dev": "tsx src/dev.ts",
37
33
  "typecheck": "tsc --noEmit",
38
34
  "lint": "eslint src",
39
35
  "test": "vitest run"