@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 +143 -65
- package/dist/index.d.ts +158 -14
- package/dist/index.js +292 -237
- package/dist/index.js.map +1 -1
- package/package.json +3 -7
package/README.md
CHANGED
|
@@ -3,11 +3,28 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@rawdash/server)
|
|
4
4
|
[](https://github.com/rawdash/rawdash/blob/main/LICENSE)
|
|
5
5
|
|
|
6
|
-
|
|
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`
|
|
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
|
-
##
|
|
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 {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
##
|
|
68
|
+
## The wire contract
|
|
47
69
|
|
|
48
|
-
|
|
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
|
-
|
|
79
|
+
Paths are exported as constants from `ROUTES`. Use them in adapters (and in clients) instead of hard-coding.
|
|
51
80
|
|
|
52
|
-
|
|
53
|
-
| --------- | --------------- | --------- | -------------------------------------- |
|
|
54
|
-
| `port` | `number` | `8080` | Port to listen on |
|
|
55
|
-
| `storage` | `ServerStorage` | in-memory | Storage backend (e.g. `LibsqlStorage`) |
|
|
81
|
+
### `SyncState`
|
|
56
82
|
|
|
57
|
-
|
|
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
|
-
|
|
94
|
+
Transitions:
|
|
60
95
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
+
### `CachedWidget.syncState`
|
|
69
103
|
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
+
## Engine without HTTP
|
|
89
134
|
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
+
## Widget cache (optional)
|
|
95
146
|
|
|
96
|
-
|
|
147
|
+
`listWidgets` and `getWidget` accept an optional `WidgetCache` so deployments can avoid hitting storage for every widget on every request:
|
|
97
148
|
|
|
98
|
-
```
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
`
|
|
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,
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
this.
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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/
|
|
97
|
+
// src/sync.ts
|
|
98
|
+
import { instantiateConnector } from "@rawdash/core";
|
|
129
99
|
var FULL_SYNC_TIMEOUT_MS = 3e5;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
}
|
|
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
|
-
|
|
188
|
-
|
|
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/
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
|
290
|
+
import { InMemoryStorage, isSyncActive as isSyncActive2, resolveWidget as resolveWidget2 } from "@rawdash/core";
|
|
265
291
|
function createEngine(config, options = {}) {
|
|
266
|
-
const storage = options.storage ?? new
|
|
267
|
-
const
|
|
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,
|
|
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,
|
|
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
|
|
299
|
-
return {
|
|
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
|
|
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 {
|
|
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
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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":"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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 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.
|
|
4
|
-
"description": "
|
|
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
|
-
"@
|
|
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"
|