@rawdash/server 0.13.0 → 0.15.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,160 @@ 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
36
+
37
+ Each pure handler takes an `EngineContext` (and any path parameters) and returns the response body or throws a `RawdashError`. Your adapter:
38
+
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.
19
43
 
20
44
  ```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
- );
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
+ });
44
66
  ```
45
67
 
46
- ## API
68
+ ## The wire contract
47
69
 
48
- ### `serve(config, options?)`
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) |
49
78
 
50
- Starts the HTTP server on Node.js (via `@hono/node-server`). Options:
79
+ Paths are exported as constants from `ROUTES`. Use them in adapters (and in clients) instead of hard-coding.
51
80
 
52
- | Option | Type | Default | Description |
53
- | --------- | --------------- | --------- | -------------------------------------- |
54
- | `port` | `number` | `8080` | Port to listen on |
55
- | `storage` | `ServerStorage` | in-memory | Storage backend (e.g. `LibsqlStorage`) |
81
+ ### `SyncState`
56
82
 
57
- ### `createServer(config, options?)`
83
+ ```ts
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
+ }
92
+ ```
58
93
 
59
- Returns the Hono app without binding to a port. Use when you need the app object directly.
94
+ Transitions:
60
95
 
61
- ```ts
62
- import { createServer } from '@rawdash/server';
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.
63
99
 
64
- const app = createServer(config);
65
- export default app; // deploy to Cloudflare Workers, Bun, Deno, etc.
66
- ```
100
+ Clients (`@rawdash/client`) poll `/sync/state` and wait for `!isSyncActive(status)` to settle.
67
101
 
68
- ## HTTP endpoints
102
+ ### `CachedWidget.syncState`
69
103
 
70
- ### `GET /dashboards/:dashboardId/widgets`
104
+ `listWidgets` and `getWidget` populate `syncState` (and `meta.connectorStatus`) on each `CachedWidget` from the underlying `StorageHandle.getHealth?()`. When storage doesn't implement `getHealth`, `syncState` falls back to `'unsynced'` (no data) or `'fresh'` (data exists).
71
105
 
72
- Returns all cached widget entries for a dashboard.
106
+ | Value | Meaning |
107
+ | ------------ | ------------------------------------------------------------------------------------ |
108
+ | `'fresh'` | Data exists and the connector's `lastSyncAt` is within `2 × syncIntervalSeconds` |
109
+ | `'stale'` | Data exists but the connector hasn't synced inside its freshness window |
110
+ | `'unsynced'` | No successful sync yet for this connector |
111
+ | `'syncing'` | A sync is actively in progress for the connector backing this widget |
112
+ | `'failing'` | Connector is in `error` / `auth_failed` / `paused` — surface a reauthorize CTA in UI |
73
113
 
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
- ]
114
+ Storage adapters implement `getHealth?(): Promise<ConnectorHealth | null>` per `StorageHandle` to expose `status`, `lastSyncAt`, `lastError`, and `syncIntervalSeconds`. `InMemoryStorage` provides a minimal implementation (last-write time as `lastSyncAt`, `syncIntervalSeconds: 0`); adapters with first-class per-connector status (e.g. cloud, libSQL) populate it richly.
115
+
116
+ ### `triggerSync` modes
117
+
118
+ `triggerSync(ctx, opts?)` accepts an optional `opts.mode`:
119
+
120
+ - **`'in-process'`** (default): the handler records the `queued` transition and then fires `runSync(config, storage)` as a background promise that iterates `config.connectors`. Right for self-hosted, single-process OSS deployments.
121
+ - **`'deferred'`**: the handler only records the `queued` transition. `runSync` is not invoked, and `getConfig` is not called (and may be omitted from `ctx`). The `running → succeeded/failed` transitions are the responsibility of an external runner — typically a queue consumer worker that decrypts credentials, applies retries, and drives storage directly.
122
+
123
+ ```ts
124
+ // Self-hosted, in-process (default):
125
+ await triggerSync({ getConfig, getStorage });
126
+
127
+ // Queue-backed runner:
128
+ await triggerSync({ getStorage }, { mode: 'deferred' });
84
129
  ```
85
130
 
86
- ### `GET /dashboards/:dashboardId/widgets/:widgetId`
131
+ In deferred mode, the wire response is unchanged: `{queued: true}` if `markSyncQueued()` accepted the transition, `{queued: false}` if a sync was already active.
87
132
 
88
- Returns a single widget by ID. Returns `404` if not found.
133
+ ## Engine without HTTP
89
134
 
90
- ### `POST /sync`
135
+ ```ts
136
+ import { createEngine } from '@rawdash/server';
137
+
138
+ const engine = createEngine(config, { storage });
139
+ const widgets = await engine.getWidgets('engineering');
140
+ const state = await engine.getSyncState();
141
+ ```
91
142
 
92
- Triggers an immediate sync across all configured connectors. Returns `{ "triggered": false }` if a sync is already in progress.
143
+ `createEngine` exposes the same shape as the handlers but bypasses HTTP entirely useful for jobs, CLI tools, or the MCP server.
93
144
 
94
- ### `GET /health`
145
+ ## Widget cache (optional)
95
146
 
96
- Returns the current sync state.
147
+ `listWidgets` and `getWidget` accept an optional `WidgetCache` so deployments can avoid hitting storage for every widget on every request:
97
148
 
98
- ```json
99
- {
100
- "status": "idle",
101
- "lastSyncAt": "2026-04-11T10:00:00.000Z",
102
- "lastError": null
149
+ ```ts
150
+ import type { WidgetCache } from '@rawdash/server';
151
+
152
+ class LruWidgetCache implements WidgetCache {
153
+ private store = new Map<string, { value: CachedWidget; expiresAt: number }>();
154
+ async get({ dashboardId, widgetId }) {
155
+ const hit = this.store.get(`${dashboardId}/${widgetId}`);
156
+ if (!hit || hit.expiresAt < Date.now()) return undefined;
157
+ return hit.value;
158
+ }
159
+ async set({ dashboardId, widgetId, widget }, value) {
160
+ const ttlMs = ttlForWidget(widget); // e.g. derive from connector syncIntervalSeconds
161
+ this.store.set(`${dashboardId}/${widgetId}`, {
162
+ value,
163
+ expiresAt: Date.now() + ttlMs,
164
+ });
165
+ }
103
166
  }
167
+
168
+ const cache = new LruWidgetCache();
169
+ await listWidgets(ctx, 'engineering', cache);
104
170
  ```
105
171
 
106
- `status` is one of `"idle"`, `"syncing"`, or `"error"`.
172
+ The cache impl owns TTL, eviction, and the backing store (LRU, KV, Redis…). If `cache` is omitted, behavior is identical to the no-cache path. Errors thrown from `cache.get` fall through to a fresh resolution; errors from `cache.set` are logged via `console.warn` and do not affect the response.
173
+
174
+ `@rawdash/hono`'s `createWidgetsRouter` accepts a `cache: (c: Context) => WidgetCache` factory invoked once per request, so the cache can be scoped to the request's tenant/auth context.
175
+
176
+ ## Storage
177
+
178
+ Provide any `ServerStorage` implementation:
179
+
180
+ - `InMemoryStorage` (re-exported here) — dev/test.
181
+ - [`@rawdash/adapter-libsql`](https://www.npmjs.com/package/@rawdash/adapter-libsql) — durable libSQL/Turso/SQLite backend.
182
+ - Roll your own by implementing the [`ServerStorage`](https://github.com/rawdash/rawdash/blob/main/packages/core/src/server-storage.ts) interface.
107
183
 
108
184
  ## Links
109
185
 
110
186
  - [rawdash docs](https://rawdash.dev)
187
+ - [`@rawdash/hono`](https://www.npmjs.com/package/@rawdash/hono) — Hono adapter
188
+ - [`@rawdash/client`](https://www.npmjs.com/package/@rawdash/client) — typed HTTP client
111
189
  - [GitHub](https://github.com/rawdash/rawdash)
112
190
  - [Issues](https://github.com/rawdash/rawdash/issues)
113
191
 
package/dist/index.d.ts CHANGED
@@ -1,31 +1,175 @@
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, Widget, CachedWidget, ConnectorRegistry, SecretsResolver, HealthResponse, SyncState, WidgetsListResponse, TriggerSyncResponse, RetentionConfig } from '@rawdash/core';
2
+ export { ACTIVE_SYNC_STATUSES, CachedWidget, ConfiguredConnector, ConnectorClass, ConnectorHealth, ConnectorRegistry, DashboardConfig, HealthResponse, InMemoryStorage, SecretsResolver, ServerStorage, SyncState, SyncStatus, TriggerSyncResponse, Widget, WidgetSyncState, WidgetsListResponse, computeMetric, instantiateConnector, 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;
13
30
 
14
- declare function createServer(routers: RouterMount[]): Hono;
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
+ };
15
48
 
16
- declare function createEngineRouters(config: DashboardConfig, storage?: ServerStorage): RouterMount[];
49
+ interface WidgetCacheKey {
50
+ dashboardId: string;
51
+ widgetId: string;
52
+ widget: Widget;
53
+ }
54
+ interface WidgetCache {
55
+ get(key: WidgetCacheKey): Promise<CachedWidget | undefined>;
56
+ set(key: WidgetCacheKey, value: CachedWidget): Promise<void>;
57
+ }
58
+
59
+ /**
60
+ * Per-request lookup shape accepted by `triggerSync` in deferred mode.
61
+ * `getConfig` is optional because the trigger handler never calls
62
+ * `runSync` — deployments that delegate the actual sync work to an
63
+ * external runner may not be able to materialize a `DashboardConfig` at
64
+ * request time.
65
+ */
66
+ interface DeferredTriggerSyncContext {
67
+ getConfig?: () => DashboardConfig | Promise<DashboardConfig>;
68
+ getStorage: () => ServerStorage | Promise<ServerStorage>;
69
+ }
70
+ /**
71
+ * Per-request lookup shape accepted by `triggerSync` in in-process
72
+ * mode. `getConfig` is required because the trigger handler kicks off
73
+ * `runSync(config, storage)` in the background. `connectorRegistry` is
74
+ * required so the background runner can instantiate connector
75
+ * implementations on demand from the declarative `DashboardConfig`.
76
+ */
77
+ interface InProcessTriggerSyncContext {
78
+ getConfig: () => DashboardConfig | Promise<DashboardConfig>;
79
+ getStorage: () => ServerStorage | Promise<ServerStorage>;
80
+ connectorRegistry: ConnectorRegistry;
81
+ secretsResolver?: SecretsResolver;
82
+ }
83
+ /**
84
+ * @deprecated Prefer `InProcessTriggerSyncContext` /
85
+ * `DeferredTriggerSyncContext`. Retained as the union for callers that
86
+ * need a single type covering both modes.
87
+ */
88
+ type TriggerSyncContext = DeferredTriggerSyncContext;
89
+ type TriggerSyncMode = 'in-process' | 'deferred';
90
+ interface TriggerSyncOptions {
91
+ /**
92
+ * `'in-process'` (default): the trigger handler also runs the sync in
93
+ * the background by invoking `runSync(config, storage)`. Suitable for
94
+ * self-hosted, single-process deployments.
95
+ *
96
+ * `'deferred'`: the trigger handler only persists the `queued`
97
+ * transition and returns. The `running → succeeded/failed` transitions
98
+ * are the responsibility of an external runner (e.g. a queue consumer
99
+ * worker), which must drive the storage accordingly.
100
+ */
101
+ mode?: TriggerSyncMode;
102
+ }
103
+ /**
104
+ * Framework-agnostic request handlers for the rawdash wire contract.
105
+ *
106
+ * Each function takes an `EngineContext` (providing per-request access to
107
+ * the config + storage) and returns the response body, or throws a
108
+ * `RawdashError` on a client-visible failure. HTTP adapters
109
+ * (`@rawdash/hono`, etc.) wrap these in their framework's request/response
110
+ * cycle and translate `RawdashError` into a structured error response.
111
+ */
112
+ declare function getHealth(): HealthResponse;
113
+ declare function getSyncStateHandler(ctx: EngineContext): Promise<SyncState>;
114
+ declare function triggerSync(ctx: InProcessTriggerSyncContext, opts?: {
115
+ mode?: 'in-process';
116
+ }): Promise<TriggerSyncResponse>;
117
+ declare function triggerSync(ctx: DeferredTriggerSyncContext, opts: {
118
+ mode: 'deferred';
119
+ }): Promise<TriggerSyncResponse>;
120
+ declare function listWidgets(ctx: EngineContext, dashboardId: string, cache?: WidgetCache): Promise<WidgetsListResponse>;
121
+ declare function getWidget(ctx: EngineContext, dashboardId: string, widgetId: string, cache?: WidgetCache): Promise<CachedWidget>;
122
+ declare function runRetentionOnce(ctx: EngineContext): Promise<void>;
123
+
124
+ declare const FULL_SYNC_TIMEOUT_MS = 300000;
125
+ interface RunSyncOptions {
126
+ connectorRegistry: ConnectorRegistry;
127
+ secretsResolver?: SecretsResolver;
128
+ }
129
+ /**
130
+ * Run a full sync across all connectors in the config in parallel via
131
+ * `Promise.allSettled`, so one failure doesn't abort the others. Each
132
+ * connector run is wrapped in a hard timeout (`FULL_SYNC_TIMEOUT_MS`)
133
+ * raced against the connector's own `Promise`, so a connector that
134
+ * ignores `AbortSignal` (or a storage call that hangs) can still not
135
+ * pin sync state in `running` indefinitely.
136
+ *
137
+ * The per-run storage handle is bound to the same `AbortController`, so
138
+ * once the timeout fires every subsequent write call on that handle
139
+ * becomes a no-op. That makes tail writes from a timed-out connector
140
+ * invisible to the next sync even if the connector itself keeps running
141
+ * — see `withAbortSignal` in `@rawdash/core` and the safety-net note in
142
+ * `docs/authoring-a-connector.md`.
143
+ *
144
+ * Transitions storage through `queued` → `running` → `succeeded`/`failed`.
145
+ * The `queued` step is a no-op if the caller (typically `triggerSync`)
146
+ * already marked the run as queued.
147
+ *
148
+ * Returns silently if another sync acquired the `running` lock first.
149
+ */
150
+ declare function runSync(config: DashboardConfig, storage: ServerStorage, options: RunSyncOptions): Promise<void>;
151
+
152
+ declare const DEFAULT_RETENTION_INTERVAL_MS: number;
153
+ declare function hasPruningPolicy(config: RetentionConfig): boolean;
154
+ /**
155
+ * Apply the retention policy in `config` to every connector's stored data.
156
+ * No-op if the config has no pruning policy. Throws an aggregated error if
157
+ * any connector fails.
158
+ */
159
+ declare function runRetention(config: DashboardConfig, storage: ServerStorage): Promise<void>;
17
160
 
18
161
  interface EngineOptions {
19
162
  storage?: ServerStorage;
163
+ connectorRegistry?: ConnectorRegistry;
164
+ secretsResolver?: SecretsResolver;
20
165
  }
21
166
  interface Engine {
22
167
  getWidget(dashboardId: string, widgetId: string): Promise<CachedWidget | undefined>;
23
168
  getWidgets(dashboardId: string): Promise<CachedWidget[]>;
24
- getHealth(): Promise<SyncState>;
169
+ getHealth(): Promise<HealthResponse>;
170
+ getSyncState(): Promise<SyncState>;
25
171
  triggerSync(): Promise<TriggerSyncResponse>;
26
172
  }
27
173
  declare function createEngine(config: DashboardConfig, options?: EngineOptions): Engine;
28
174
 
29
- declare function serve(config: DashboardConfig, options?: ServeOptions): void;
30
-
31
- export { type Engine, type EngineOptions, type RouterMount, type ServeOptions, createEngine, createEngineRouters, createServer, serve };
175
+ export { DEFAULT_RETENTION_INTERVAL_MS, type DeferredTriggerSyncContext, type Engine, type EngineContext, type EngineOptions, FULL_SYNC_TIMEOUT_MS, type InProcessTriggerSyncContext, ROUTES, RawdashError, type RunSyncOptions, type TriggerSyncContext, type TriggerSyncMode, type TriggerSyncOptions, type WidgetCache, type WidgetCacheKey, 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 (entry) => {
45
+ const handle = storage.getStorageHandle(entry.name);
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,203 @@ 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
98
+ import { instantiateConnector } from "@rawdash/core";
129
99
  var FULL_SYNC_TIMEOUT_MS = 3e5;
130
- var SyncRouter = class {
131
- constructor(config, storage) {
132
- this.config = config;
133
- this.storage = storage;
100
+ async function runSync(config, storage, options) {
101
+ await storage.markSyncQueued();
102
+ const acquired = await storage.markSyncRunning();
103
+ if (!acquired) {
104
+ return;
134
105
  }
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
106
+ const errors = [];
107
+ await Promise.allSettled(
108
+ config.connectors.map(async (entry) => {
109
+ if (entry.enabled === false) {
110
+ return;
111
+ }
112
+ const controller = new AbortController();
113
+ const handle = storage.getStorageHandle(entry.name, {
114
+ signal: controller.signal
115
+ });
116
+ let timer;
117
+ try {
118
+ const connector = instantiateConnector(
119
+ entry,
120
+ options.connectorRegistry,
121
+ options.secretsResolver
150
122
  );
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(
165
- `${connector.id} timed out after ${FULL_SYNC_TIMEOUT_MS}ms`
123
+ const syncPromise = connector.sync(
124
+ { mode: "full" },
125
+ handle,
126
+ controller.signal
127
+ );
128
+ const timeoutPromise = new Promise((_resolve, reject) => {
129
+ timer = setTimeout(() => {
130
+ controller.abort();
131
+ const err = new Error(
132
+ `${entry.name} timed out after ${FULL_SYNC_TIMEOUT_MS}ms`
166
133
  );
167
- } else {
168
- errors.push(err instanceof Error ? err.message : String(err));
169
- }
170
- } finally {
134
+ err.name = "AbortError";
135
+ reject(err);
136
+ }, FULL_SYNC_TIMEOUT_MS);
137
+ });
138
+ const result = await Promise.race([syncPromise, timeoutPromise]);
139
+ if (!result.done) {
140
+ errors.push(
141
+ `${entry.name} did not complete in one chunk (chunked syncs are only supported in cloud)`
142
+ );
143
+ }
144
+ } catch (err) {
145
+ if (err instanceof Error && err.name === "AbortError") {
146
+ errors.push(
147
+ `${entry.name} timed out after ${FULL_SYNC_TIMEOUT_MS}ms`
148
+ );
149
+ } else {
150
+ errors.push(err instanceof Error ? err.message : String(err));
151
+ }
152
+ } finally {
153
+ if (timer !== void 0) {
171
154
  clearTimeout(timer);
172
155
  }
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
156
  }
187
- void this.runSync();
188
- return c.json({ triggered: true });
189
- });
157
+ })
158
+ );
159
+ if (errors.length > 0) {
160
+ await storage.markSyncFailed(errors.join("; "));
161
+ } else {
162
+ await storage.markSyncSucceeded();
190
163
  }
191
- };
164
+ }
192
165
 
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;
166
+ // src/handlers.ts
167
+ async function cacheGetSafe(cache, dashboardId, widgetId, widget) {
168
+ try {
169
+ return await cache.get({ dashboardId, widgetId, widget });
170
+ } catch (err) {
171
+ console.warn("Rawdash widget cache get failed", err);
172
+ return void 0;
201
173
  }
202
- dashboardId;
203
- dashboard;
204
- connectors;
205
- storage;
206
- resolve(id, widget) {
207
- return resolveWidget(id, widget, this.connectors, this.storage);
174
+ }
175
+ async function cacheSetSafe(cache, dashboardId, widgetId, widget, value) {
176
+ try {
177
+ await cache.set({ dashboardId, widgetId, widget }, value);
178
+ } catch (err) {
179
+ console.warn("Rawdash widget cache set failed", err);
208
180
  }
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
181
+ }
182
+ async function resolveWithCache(dashboardId, widgetId, widget, connectorNames, storage, cache) {
183
+ if (cache) {
184
+ const hit = await cacheGetSafe(cache, dashboardId, widgetId, widget);
185
+ if (hit) {
186
+ return hit;
187
+ }
188
+ }
189
+ const fresh = await resolveWidget(widgetId, widget, connectorNames, storage);
190
+ if (fresh && cache) {
191
+ await cacheSetSafe(cache, dashboardId, widgetId, widget, fresh);
192
+ }
193
+ return fresh;
194
+ }
195
+ function getHealth() {
196
+ return { status: "ok" };
197
+ }
198
+ async function getSyncStateHandler(ctx) {
199
+ const storage = await ctx.getStorage();
200
+ return storage.getSyncState();
201
+ }
202
+ async function triggerSync(ctx, opts = {}) {
203
+ const mode = opts.mode ?? "in-process";
204
+ const storage = await ctx.getStorage();
205
+ const state = await storage.getSyncState();
206
+ if (isSyncActive(state.status)) {
207
+ return { queued: false };
208
+ }
209
+ let config;
210
+ if (mode === "in-process") {
211
+ if (!ctx.getConfig) {
212
+ throw new Error(
213
+ 'triggerSync: getConfig is required when mode is "in-process"'
218
214
  );
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
- });
215
+ }
216
+ config = await ctx.getConfig();
234
217
  }
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)
218
+ const queued = await storage.markSyncQueued();
219
+ if (!queued) {
220
+ return { queued: false };
221
+ }
222
+ if (mode === "deferred") {
223
+ return { queued: true };
224
+ }
225
+ const inProcessCtx = ctx;
226
+ void runSync(config, storage, {
227
+ connectorRegistry: inProcessCtx.connectorRegistry,
228
+ secretsResolver: inProcessCtx.secretsResolver
229
+ }).catch((err) => {
230
+ console.error("Rawdash sync failed", err);
231
+ });
232
+ return { queued: true };
233
+ }
234
+ async function listWidgets(ctx, dashboardId, cache) {
235
+ const config = await ctx.getConfig();
236
+ const dashboard = config.dashboards[dashboardId];
237
+ if (!dashboard) {
238
+ throw new RawdashError(404, "DASHBOARD_NOT_FOUND", "Dashboard not found");
239
+ }
240
+ const storage = await ctx.getStorage();
241
+ const connectorNames = config.connectors.map((c) => c.name);
242
+ const entries = Object.entries(dashboard.widgets);
243
+ const resolved = await Promise.all(
244
+ entries.map(
245
+ ([key, widget]) => resolveWithCache(
246
+ dashboardId,
247
+ key,
248
+ widget,
249
+ connectorNames,
250
+ storage,
251
+ cache
252
+ )
253
+ )
244
254
  );
245
- return [
246
- ...widgetRouters,
247
- new SyncRouter(config, storage),
248
- new RetentionRouter(config, storage),
249
- new HealthRouter(storage)
250
- ];
255
+ const widgets = resolved.filter((w) => w !== void 0);
256
+ return { widgets };
251
257
  }
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);
258
+ async function getWidget(ctx, dashboardId, widgetId, cache) {
259
+ const config = await ctx.getConfig();
260
+ const dashboard = config.dashboards[dashboardId];
261
+ if (!dashboard) {
262
+ throw new RawdashError(404, "DASHBOARD_NOT_FOUND", "Dashboard not found");
259
263
  }
260
- return app;
264
+ const widget = dashboard.widgets[widgetId];
265
+ if (!widget) {
266
+ throw new RawdashError(404, "WIDGET_NOT_FOUND", "Widget not found");
267
+ }
268
+ const storage = await ctx.getStorage();
269
+ const connectorNames = config.connectors.map((c) => c.name);
270
+ const result = await resolveWithCache(
271
+ dashboardId,
272
+ widgetId,
273
+ widget,
274
+ connectorNames,
275
+ storage,
276
+ cache
277
+ );
278
+ if (!result) {
279
+ throw new RawdashError(404, "WIDGET_NOT_FOUND", "Widget not found");
280
+ }
281
+ return result;
282
+ }
283
+ async function runRetentionOnce(ctx) {
284
+ const config = await ctx.getConfig();
285
+ const storage = await ctx.getStorage();
286
+ await runRetention(config, storage);
261
287
  }
262
288
 
263
289
  // src/engine.ts
264
- import { InMemoryStorage as InMemoryStorage2, resolveWidget as resolveWidget2 } from "@rawdash/core";
290
+ import { InMemoryStorage, isSyncActive as isSyncActive2, resolveWidget as resolveWidget2 } from "@rawdash/core";
265
291
  function createEngine(config, options = {}) {
266
- const storage = options.storage ?? new InMemoryStorage2();
267
- const syncRouter = new SyncRouter(config, storage);
292
+ const storage = options.storage ?? new InMemoryStorage();
293
+ const connectorNames = config.connectors.map((c) => c.name);
268
294
  return {
269
295
  async getWidget(dashboardId, widgetId) {
270
296
  const dashboard = config.dashboards[dashboardId];
@@ -275,7 +301,7 @@ function createEngine(config, options = {}) {
275
301
  if (!widget) {
276
302
  return void 0;
277
303
  }
278
- return resolveWidget2(widgetId, widget, config.connectors, storage);
304
+ return resolveWidget2(widgetId, widget, connectorNames, storage);
279
305
  },
280
306
  async getWidgets(dashboardId) {
281
307
  const dashboard = config.dashboards[dashboardId];
@@ -285,23 +311,38 @@ function createEngine(config, options = {}) {
285
311
  const entries = Object.entries(dashboard.widgets);
286
312
  const resolved = await Promise.all(
287
313
  entries.map(
288
- ([key, widget]) => resolveWidget2(key, widget, config.connectors, storage)
314
+ ([key, widget]) => resolveWidget2(key, widget, connectorNames, storage)
289
315
  )
290
316
  );
291
317
  return resolved.filter((w) => w !== void 0);
292
318
  },
293
319
  async getHealth() {
320
+ return { status: "ok" };
321
+ },
322
+ async getSyncState() {
294
323
  return storage.getSyncState();
295
324
  },
296
325
  async triggerSync() {
326
+ if (!options.connectorRegistry) {
327
+ throw new Error(
328
+ "createEngine: connectorRegistry is required to triggerSync"
329
+ );
330
+ }
297
331
  const state = await storage.getSyncState();
298
- if (state.status === "syncing") {
299
- return { triggered: false };
332
+ if (isSyncActive2(state.status)) {
333
+ return { queued: false };
334
+ }
335
+ const queued = await storage.markSyncQueued();
336
+ if (!queued) {
337
+ return { queued: false };
300
338
  }
301
- void syncRouter.runSync().catch((error) => {
339
+ void runSync(config, storage, {
340
+ connectorRegistry: options.connectorRegistry,
341
+ secretsResolver: options.secretsResolver
342
+ }).catch((error) => {
302
343
  console.error("Rawdash sync failed", error);
303
344
  });
304
- return { triggered: true };
345
+ return { queued: true };
305
346
  }
306
347
  };
307
348
  }
@@ -309,18 +350,32 @@ function createEngine(config, options = {}) {
309
350
  // src/compute.ts
310
351
  import { computeMetric } from "@rawdash/core";
311
352
 
353
+ // src/storage.ts
354
+ import { InMemoryStorage as InMemoryStorage2 } from "@rawdash/core";
355
+
312
356
  // 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
- }
357
+ import { isSyncActive as isSyncActive3, ACTIVE_SYNC_STATUSES } from "@rawdash/core";
358
+ import { instantiateConnector as instantiateConnector2 } from "@rawdash/core";
318
359
  export {
319
- InMemoryStorage,
360
+ ACTIVE_SYNC_STATUSES,
361
+ DEFAULT_RETENTION_INTERVAL_MS,
362
+ FULL_SYNC_TIMEOUT_MS,
363
+ InMemoryStorage2 as InMemoryStorage,
364
+ ROUTES,
365
+ RawdashError,
320
366
  computeMetric,
321
367
  createEngine,
322
- createEngineRouters,
323
- createServer,
324
- serve
368
+ getHealth,
369
+ getSyncStateHandler,
370
+ getWidget,
371
+ hasPruningPolicy,
372
+ instantiateConnector2 as instantiateConnector,
373
+ isRawdashError,
374
+ isSyncActive3 as isSyncActive,
375
+ listWidgets,
376
+ runRetention,
377
+ runRetentionOnce,
378
+ runSync,
379
+ triggerSync
325
380
  };
326
381
  //# 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":";AAAA,SAAS,SAAS,iBAAiB;;;ACK5B,IAAM,eAAN,MAA0C;AAAA,EAC/C,YAAoB,SAAwB;AAAxB;AAAA,EAAyB;AAAA,EAAzB;AAAA,EAEpB,MAAM,KAAiB;AACrB,QAAI,IAAI,WAAW,OAAO,MAAM;AAC9B,aAAO,EAAE,KAAK,MAAM,KAAK,QAAQ,aAAa,CAAC;AAAA,IACjD,CAAC;AAAA,EACH;AACF;;;ACZA,SAAS,yBAAyB;AAMlC,IAAM,sBAAsB,KAAK,KAAK;AAEtC,SAAS,iBAAiB,QAAkC;AAC1D,SAAO,OAAO,WAAW,UAAa,OAAO,YAAY;AAC3D;AAEO,IAAM,kBAAN,MAA6C;AAAA,EAIlD,YACU,QACA,SACR;AAFQ;AACA;AAAA,EACP;AAAA,EAFO;AAAA,EACA;AAAA,EALF,WAAkD;AAAA,EAClD,WAAiC;AAAA,EAOzC,MAAM,eAA8B;AAClC,QAAI,KAAK,UAAU;AACjB,aAAO,KAAK;AAAA,IACd;AAEA,SAAK,WAAW,KAAK,iBAAiB,EAAE,QAAQ,MAAM;AACpD,WAAK,WAAW;AAAA,IAClB,CAAC;AAED,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAc,mBAAkC;AAC9C,UAAM,kBAAkB,KAAK,OAAO;AACpC,QAAI,CAAC,mBAAmB,CAAC,iBAAiB,eAAe,GAAG;AAC1D;AAAA,IACF;AAEA,UAAM,QAAQ,KAAK,IAAI;AAEvB,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC5B,KAAK,OAAO,WAAW,IAAI,OAAO,EAAE,UAAU,MAAM;AAClD,cAAM,SAAS,KAAK,QAAQ,iBAAiB,UAAU,EAAE;AAEzD,cAAM,CAAC,QAAQ,SAAS,aAAa,IAAI,MAAM,QAAQ,IAAI;AAAA,UACzD,OAAO,YAAY,CAAC,CAAC;AAAA,UACrB,OAAO,aAAa,CAAC,CAAC;AAAA,UACtB,OAAO,mBAAmB,CAAC,CAAC;AAAA,QAC9B,CAAC;AAED,cAAM;AAAA,UACJ;AAAA,UACA,CAAC,MAAM,EAAE;AAAA,UACT;AAAA,UACA;AAAA,UACA,CAAC,WAAW,UAAU,OAAO,OAAO,WAAW,EAAE,MAAM,CAAC;AAAA,QAC1D;AAEA,cAAM;AAAA,UACJ;AAAA,UACA,CAAC,MAAM,EAAE;AAAA,UACT;AAAA,UACA;AAAA,UACA,CAAC,WAAW,UAAU,OAAO,QAAQ,WAAW,EAAE,MAAM,CAAC;AAAA,QAC3D;AAEA,cAAM;AAAA,UACJ;AAAA,UACA,CAAC,MAAM,EAAE;AAAA,UACT;AAAA,UACA;AAAA,UACA,CAAC,WAAW,UAAU,OAAO,cAAc,WAAW,EAAE,MAAM,CAAC;AAAA,QACjE;AAAA,MACF,CAAC;AAAA,IACH;AAEA,UAAM,WAAW,QAAQ;AAAA,MACvB,CAAC,MAAkC,EAAE,WAAW;AAAA,IAClD;AACA,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,IAAI;AAAA,QACR,wBAAwB,SAAS,MAAM,kBAAkB,SAAS,IAAI,CAAC,MAAM,OAAO,EAAE,MAAM,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA,MAC3G;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,KAAiB;AACrB,QAAI,KAAK,WAAW,OAAO,MAAM;AAC/B,UAAI;AACF,cAAM,KAAK,aAAa;AACxB,eAAO,EAAE,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,MACnC,SAAS,KAAK;AACZ,gBAAQ,MAAM,wBAAwB,GAAG;AACzC,eAAO,EAAE,KAAK,EAAE,WAAW,MAAM,GAAG,GAAG;AAAA,MACzC;AAAA,IACF,CAAC;AAED,UAAM,kBAAkB,KAAK,OAAO;AACpC,QAAI,mBAAmB,iBAAiB,eAAe,GAAG;AACxD,YAAM,aAAa,gBAAgB,cAAc;AACjD,WAAK,WAAW,YAAY,MAAM;AAChC,aAAK,KAAK,aAAa,EAAE,MAAM,CAAC,QAAQ;AACtC,kBAAQ,MAAM,wBAAwB,GAAG;AAAA,QAC3C,CAAC;AAAA,MACH,GAAG,UAAU;AAAA,IACf;AAAA,EACF;AAAA,EAEA,OAAa;AACX,QAAI,KAAK,aAAa,MAAM;AAC1B,oBAAc,KAAK,QAAQ;AAC3B,WAAK,WAAW;AAAA,IAClB;AAAA,EACF;AACF;AAEA,eAAe,sBACb,MACA,OACA,QACA,OACA,gBACe;AACf,MAAI,KAAK,WAAW,GAAG;AACrB;AAAA,EACF;AAEA,QAAM,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,MAAM,CAAC,CAAC;AAC3D,QAAM,cAAc,IAAI,IAAI,kBAAkB,QAAQ,OAAO,QAAQ,KAAK,CAAC;AAE3E,MAAI,YAAY,SAAS,GAAG;AAC1B;AAAA,EACF;AAEA,QAAM,YAAY,OAAO,OAAO,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,CAAC;AAC1D,QAAM,WAAW,CAAC,GAAG,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAErD,QAAM,eAAe,WAAW,QAAQ;AAC1C;;;ACtIA,IAAM,uBAAuB;AAEtB,IAAM,aAAN,MAAwC;AAAA,EAC7C,YACU,QACA,SACR;AAFQ;AACA;AAAA,EACP;AAAA,EAFO;AAAA,EACA;AAAA,EAGV,MAAM,UAAyB;AAC7B,UAAM,WAAW,MAAM,KAAK,QAAQ,WAAW;AAC/C,QAAI,CAAC,UAAU;AACb;AAAA,IACF;AACA,UAAM,SAAmB,CAAC;AAC1B,UAAM,QAAQ;AAAA,MACZ,KAAK,OAAO,WAAW,IAAI,OAAO,EAAE,UAAU,MAAM;AAClD,cAAM,SAAS,KAAK,QAAQ,iBAAiB,UAAU,EAAE;AACzD,cAAM,aAAa,IAAI,gBAAgB;AACvC,cAAM,QAAQ;AAAA,UACZ,MAAM,WAAW,MAAM;AAAA,UACvB;AAAA,QACF;AACA,YAAI;AACF,gBAAM,SAAS,MAAM,UAAU;AAAA,YAC7B,EAAE,MAAM,OAAO;AAAA,YACf;AAAA,YACA,WAAW;AAAA,UACb;AACA,cAAI,CAAC,OAAO,MAAM;AAChB,mBAAO;AAAA,cACL,GAAG,UAAU,EAAE;AAAA,YACjB;AAAA,UACF;AAAA,QACF,SAAS,KAAK;AACZ,cAAI,eAAe,SAAS,IAAI,SAAS,cAAc;AACrD,mBAAO;AAAA,cACL,GAAG,UAAU,EAAE,oBAAoB,oBAAoB;AAAA,YACzD;AAAA,UACF,OAAO;AACL,mBAAO,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,UAC9D;AAAA,QACF,UAAE;AACA,uBAAa,KAAK;AAAA,QACpB;AAAA,MACF,CAAC;AAAA,IACH;AACA,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,KAAK,QAAQ,aAAa,OAAO,KAAK,IAAI,CAAC;AAAA,IACnD,OAAO;AACL,YAAM,KAAK,QAAQ,eAAe;AAAA,IACpC;AAAA,EACF;AAAA,EAEA,MAAM,KAAiB;AACrB,QAAI,KAAK,SAAS,OAAO,MAAM;AAC7B,YAAM,QAAQ,MAAM,KAAK,QAAQ,aAAa;AAC9C,UAAI,MAAM,WAAW,WAAW;AAC9B,eAAO,EAAE,KAAK,EAAE,WAAW,MAAM,CAAC;AAAA,MACpC;AACA,WAAK,KAAK,QAAQ;AAClB,aAAO,EAAE,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,IACnC,CAAC;AAAA,EACH;AACF;;;AC9DA,SAAS,qBAAqB;AAMvB,IAAM,gBAAN,MAA2C;AAAA,EAChD,YACU,aACA,WACA,YACA,SACR;AAJQ;AACA;AACA;AACA;AAAA,EACP;AAAA,EAJO;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAGF,QACN,IACA,QACmC;AACnC,WAAO,cAAc,IAAI,QAAQ,KAAK,YAAY,KAAK,OAAO;AAAA,EAChE;AAAA,EAEA,MAAM,KAAiB;AACrB,UAAM,OAAO,eAAe,KAAK,WAAW;AAE5C,QAAI,IAAI,MAAM,OAAO,MAAM;AACzB,YAAM,UAAU,OAAO,QAAQ,KAAK,UAAU,OAAO;AACrD,YAAM,WAAW,MAAM,QAAQ;AAAA,QAC7B,QAAQ,IAAI,CAAC,CAAC,KAAK,MAAM,MAAM,KAAK,QAAQ,KAAK,MAAM,CAAC;AAAA,MAC1D;AACA,YAAM,UAAU,SAAS;AAAA,QACvB,CAAC,MAAyB,MAAM;AAAA,MAClC;AACA,YAAM,WAAgC,EAAE,QAAQ;AAChD,aAAO,EAAE,KAAK,QAAQ;AAAA,IACxB,CAAC;AAED,QAAI,IAAI,GAAG,IAAI,cAAc,OAAO,MAAM;AACxC,YAAM,WAAW,EAAE,IAAI,MAAM,UAAU;AACvC,YAAM,SAAS,KAAK,UAAU,QAAQ,QAAQ;AAC9C,UAAI,CAAC,QAAQ;AACX,eAAO,EAAE,KAAK,EAAE,OAAO,mBAAmB,GAAG,GAAG;AAAA,MAClD;AACA,YAAM,SAAS,MAAM,KAAK,QAAQ,UAAU,MAAM;AAClD,UAAI,CAAC,QAAQ;AACX,eAAO,EAAE,KAAK,EAAE,OAAO,mBAAmB,GAAG,GAAG;AAAA,MAClD;AACA,aAAO,EAAE,KAAK,MAAM;AAAA,IACtB,CAAC;AAAA,EACH;AACF;;;ACxDA,SAAS,uBAAuB;;;ACUzB,SAAS,oBACd,QACA,UAAyB,IAAI,gBAAgB,GAC9B;AACf,QAAM,gBAAgB,OAAO,QAAQ,OAAO,UAAU,EAAE;AAAA,IACtD,CAAC,CAAC,aAAa,SAAS,MACtB,IAAI,cAAc,aAAa,WAAW,OAAO,YAAY,OAAO;AAAA,EACxE;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,IAAI,WAAW,QAAQ,OAAO;AAAA,IAC9B,IAAI,gBAAgB,QAAQ,OAAO;AAAA,IACnC,IAAI,aAAa,OAAO;AAAA,EAC1B;AACF;;;ACzBA,SAAS,YAAY;AAId,SAAS,aAAa,SAA8B;AACzD,QAAM,MAAM,IAAI,KAAK;AACrB,aAAW,UAAU,SAAS;AAC5B,WAAO,MAAM,GAAG;AAAA,EAClB;AACA,SAAO;AACT;;;ACJA,SAAS,mBAAAA,kBAAiB,iBAAAC,sBAAqB;AAmBxC,SAAS,aACd,QACA,UAAyB,CAAC,GAClB;AACR,QAAM,UAAyB,QAAQ,WAAW,IAAIC,iBAAgB;AACtE,QAAM,aAAa,IAAI,WAAW,QAAQ,OAAO;AAEjD,SAAO;AAAA,IACL,MAAM,UAAU,aAAa,UAAU;AACrC,YAAM,YAAY,OAAO,WAAW,WAAW;AAC/C,UAAI,CAAC,WAAW;AACd,eAAO;AAAA,MACT;AACA,YAAM,SAAS,UAAU,QAAQ,QAAQ;AACzC,UAAI,CAAC,QAAQ;AACX,eAAO;AAAA,MACT;AACA,aAAOC,eAAc,UAAU,QAAQ,OAAO,YAAY,OAAO;AAAA,IACnE;AAAA,IAEA,MAAM,WAAW,aAAa;AAC5B,YAAM,YAAY,OAAO,WAAW,WAAW;AAC/C,UAAI,CAAC,WAAW;AACd,eAAO,CAAC;AAAA,MACV;AACA,YAAM,UAAU,OAAO,QAAQ,UAAU,OAAO;AAChD,YAAM,WAAW,MAAM,QAAQ;AAAA,QAC7B,QAAQ;AAAA,UAAI,CAAC,CAAC,KAAK,MAAM,MACvBA,eAAc,KAAK,QAAQ,OAAO,YAAY,OAAO;AAAA,QACvD;AAAA,MACF;AACA,aAAO,SAAS,OAAO,CAAC,MAAyB,MAAM,MAAS;AAAA,IAClE;AAAA,IAEA,MAAM,YAAY;AAChB,aAAO,QAAQ,aAAa;AAAA,IAC9B;AAAA,IAEA,MAAM,cAAc;AAClB,YAAM,QAAQ,MAAM,QAAQ,aAAa;AACzC,UAAI,MAAM,WAAW,WAAW;AAC9B,eAAO,EAAE,WAAW,MAAM;AAAA,MAC5B;AACA,WAAK,WAAW,QAAQ,EAAE,MAAM,CAAC,UAAU;AACzC,gBAAQ,MAAM,uBAAuB,KAAK;AAAA,MAC5C,CAAC;AACD,aAAO,EAAE,WAAW,KAAK;AAAA,IAC3B;AAAA,EACF;AACF;;;AC1EA,SAAS,qBAAqB;;;ATevB,SAAS,MACd,QACA,UAAwB,CAAC,GACnB;AACN,QAAM,EAAE,OAAO,MAAM,QAAQ,IAAI;AACjC,QAAM,MAAM,aAAa,oBAAoB,QAAQ,OAAO,CAAC;AAC7D,YAAU,EAAE,OAAO,IAAI,OAAO,KAAK,CAAC;AACtC;","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 ConnectorRegistry,\n HealthResponse,\n SecretsResolver,\n SyncState,\n TriggerSyncResponse,\n Widget,\n WidgetsListResponse,\n} from '@rawdash/core';\nimport { isSyncActive, resolveWidget } from '@rawdash/core';\nimport type { DashboardConfig, ServerStorage } from '@rawdash/core';\n\nimport type { EngineContext } from './context';\nimport { RawdashError } from './errors';\nimport { runRetention } from './retention';\nimport { runSync } from './sync';\nimport type { WidgetCache } from './widget-cache';\n\nasync function cacheGetSafe(\n cache: WidgetCache,\n dashboardId: string,\n widgetId: string,\n widget: Widget,\n): Promise<CachedWidget | undefined> {\n try {\n return await cache.get({ dashboardId, widgetId, widget });\n } catch (err) {\n console.warn('Rawdash widget cache get failed', err);\n return undefined;\n }\n}\n\nasync function cacheSetSafe(\n cache: WidgetCache,\n dashboardId: string,\n widgetId: string,\n widget: Widget,\n value: CachedWidget,\n): Promise<void> {\n try {\n await cache.set({ dashboardId, widgetId, widget }, value);\n } catch (err) {\n console.warn('Rawdash widget cache set failed', err);\n }\n}\n\nasync function resolveWithCache(\n dashboardId: string,\n widgetId: string,\n widget: Widget,\n connectorNames: readonly string[],\n storage: ServerStorage,\n cache: WidgetCache | undefined,\n): Promise<CachedWidget | undefined> {\n if (cache) {\n const hit = await cacheGetSafe(cache, dashboardId, widgetId, widget);\n if (hit) {\n return hit;\n }\n }\n const fresh = await resolveWidget(widgetId, widget, connectorNames, storage);\n if (fresh && cache) {\n await cacheSetSafe(cache, dashboardId, widgetId, widget, fresh);\n }\n return fresh;\n}\n\n/**\n * Per-request lookup shape accepted by `triggerSync` in deferred mode.\n * `getConfig` is optional because the trigger handler never calls\n * `runSync` — deployments that delegate the actual sync work to an\n * external runner may not be able to materialize a `DashboardConfig` at\n * request time.\n */\nexport interface DeferredTriggerSyncContext {\n getConfig?: () => DashboardConfig | Promise<DashboardConfig>;\n getStorage: () => ServerStorage | Promise<ServerStorage>;\n}\n\n/**\n * Per-request lookup shape accepted by `triggerSync` in in-process\n * mode. `getConfig` is required because the trigger handler kicks off\n * `runSync(config, storage)` in the background. `connectorRegistry` is\n * required so the background runner can instantiate connector\n * implementations on demand from the declarative `DashboardConfig`.\n */\nexport interface InProcessTriggerSyncContext {\n getConfig: () => DashboardConfig | Promise<DashboardConfig>;\n getStorage: () => ServerStorage | Promise<ServerStorage>;\n connectorRegistry: ConnectorRegistry;\n secretsResolver?: SecretsResolver;\n}\n\n/**\n * @deprecated Prefer `InProcessTriggerSyncContext` /\n * `DeferredTriggerSyncContext`. Retained as the union for callers that\n * need a single type covering both modes.\n */\nexport type TriggerSyncContext = DeferredTriggerSyncContext;\n\nexport type TriggerSyncMode = 'in-process' | 'deferred';\n\nexport interface TriggerSyncOptions {\n /**\n * `'in-process'` (default): the trigger handler also runs the sync in\n * the background by invoking `runSync(config, storage)`. Suitable for\n * self-hosted, single-process deployments.\n *\n * `'deferred'`: the trigger handler only persists the `queued`\n * transition and returns. The `running → succeeded/failed` transitions\n * are the responsibility of an external runner (e.g. a queue consumer\n * worker), which must drive the storage accordingly.\n */\n mode?: TriggerSyncMode;\n}\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 function triggerSync(\n ctx: InProcessTriggerSyncContext,\n opts?: { mode?: 'in-process' },\n): Promise<TriggerSyncResponse>;\nexport function triggerSync(\n ctx: DeferredTriggerSyncContext,\n opts: { mode: 'deferred' },\n): Promise<TriggerSyncResponse>;\nexport async function triggerSync(\n ctx: InProcessTriggerSyncContext | DeferredTriggerSyncContext,\n opts: TriggerSyncOptions = {},\n): Promise<TriggerSyncResponse> {\n const mode: TriggerSyncMode = opts.mode ?? 'in-process';\n const storage = await ctx.getStorage();\n const state = await storage.getSyncState();\n if (isSyncActive(state.status)) {\n return { queued: false };\n }\n let config: DashboardConfig | undefined;\n if (mode === 'in-process') {\n if (!ctx.getConfig) {\n throw new Error(\n 'triggerSync: getConfig is required when mode is \"in-process\"',\n );\n }\n config = await ctx.getConfig();\n }\n const queued = await storage.markSyncQueued();\n if (!queued) {\n return { queued: false };\n }\n if (mode === 'deferred') {\n return { queued: true };\n }\n const inProcessCtx = ctx as InProcessTriggerSyncContext;\n void runSync(config!, storage, {\n connectorRegistry: inProcessCtx.connectorRegistry,\n secretsResolver: inProcessCtx.secretsResolver,\n }).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 cache?: WidgetCache,\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 connectorNames = config.connectors.map((c) => c.name);\n const entries = Object.entries(dashboard.widgets);\n const resolved = await Promise.all(\n entries.map(([key, widget]) =>\n resolveWithCache(\n dashboardId,\n key,\n widget,\n connectorNames,\n storage,\n cache,\n ),\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 cache?: WidgetCache,\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 connectorNames = config.connectors.map((c) => c.name);\n const result = await resolveWithCache(\n dashboardId,\n widgetId,\n widget,\n connectorNames,\n storage,\n cache,\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 (entry) => {\n const handle = storage.getStorageHandle(entry.name);\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 {\n ConnectorRegistry,\n DashboardConfig,\n SecretsResolver,\n ServerStorage,\n} from '@rawdash/core';\nimport { instantiateConnector } from '@rawdash/core';\n\nexport const FULL_SYNC_TIMEOUT_MS = 300_000;\n\nexport interface RunSyncOptions {\n connectorRegistry: ConnectorRegistry;\n secretsResolver?: SecretsResolver;\n}\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 options: RunSyncOptions,\n): Promise<void> {\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 (entry) => {\n if (entry.enabled === false) {\n return;\n }\n const controller = new AbortController();\n const handle = storage.getStorageHandle(entry.name, {\n signal: controller.signal,\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n const connector = instantiateConnector(\n entry,\n options.connectorRegistry,\n options.secretsResolver,\n );\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 `${entry.name} 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 `${entry.name} 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 `${entry.name} 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 ConnectorRegistry,\n DashboardConfig,\n HealthResponse,\n SecretsResolver,\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 connectorRegistry?: ConnectorRegistry;\n secretsResolver?: SecretsResolver;\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 const connectorNames = config.connectors.map((c) => c.name);\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, connectorNames, 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, connectorNames, 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 if (!options.connectorRegistry) {\n throw new Error(\n 'createEngine: connectorRegistry is required to triggerSync',\n );\n }\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, {\n connectorRegistry: options.connectorRegistry,\n secretsResolver: options.secretsResolver,\n }).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 type {\n DeferredTriggerSyncContext,\n InProcessTriggerSyncContext,\n TriggerSyncContext,\n TriggerSyncMode,\n TriggerSyncOptions,\n} from './handlers';\nexport type { WidgetCache, WidgetCacheKey } from './widget-cache';\nexport { runSync, FULL_SYNC_TIMEOUT_MS } from './sync';\nexport type { RunSyncOptions } 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 ConnectorHealth,\n DashboardConfig,\n HealthResponse,\n ServerStorage,\n SyncState,\n SyncStatus,\n TriggerSyncResponse,\n Widget,\n WidgetSyncState,\n WidgetsListResponse,\n} from './types';\nexport { isSyncActive, ACTIVE_SYNC_STATUSES } from '@rawdash/core';\nexport { instantiateConnector } from '@rawdash/core';\nexport type {\n ConnectorClass,\n ConnectorRegistry,\n SecretsResolver,\n} from '@rawdash/core';\n"],"mappings":"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","names":["isSyncActive","resolveWidget","resolveWidget","isSyncActive","InMemoryStorage","isSyncActive","instantiateConnector"]}
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.15.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.15.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"