@rawdash/server 0.14.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 +62 -0
- package/dist/index.d.ts +72 -7
- package/dist/index.js +95 -20
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -99,6 +99,37 @@ Transitions:
|
|
|
99
99
|
|
|
100
100
|
Clients (`@rawdash/client`) poll `/sync/state` and wait for `!isSyncActive(status)` to settle.
|
|
101
101
|
|
|
102
|
+
### `CachedWidget.syncState`
|
|
103
|
+
|
|
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).
|
|
105
|
+
|
|
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 |
|
|
113
|
+
|
|
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' });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
In deferred mode, the wire response is unchanged: `{queued: true}` if `markSyncQueued()` accepted the transition, `{queued: false}` if a sync was already active.
|
|
132
|
+
|
|
102
133
|
## Engine without HTTP
|
|
103
134
|
|
|
104
135
|
```ts
|
|
@@ -111,6 +142,37 @@ const state = await engine.getSyncState();
|
|
|
111
142
|
|
|
112
143
|
`createEngine` exposes the same shape as the handlers but bypasses HTTP entirely — useful for jobs, CLI tools, or the MCP server.
|
|
113
144
|
|
|
145
|
+
## Widget cache (optional)
|
|
146
|
+
|
|
147
|
+
`listWidgets` and `getWidget` accept an optional `WidgetCache` so deployments can avoid hitting storage for every widget on every request:
|
|
148
|
+
|
|
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
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const cache = new LruWidgetCache();
|
|
169
|
+
await listWidgets(ctx, 'engineering', cache);
|
|
170
|
+
```
|
|
171
|
+
|
|
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
|
+
|
|
114
176
|
## Storage
|
|
115
177
|
|
|
116
178
|
Provide any `ServerStorage` implementation:
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DashboardConfig, ServerStorage, HealthResponse, SyncState,
|
|
2
|
-
export { ACTIVE_SYNC_STATUSES, CachedWidget, ConfiguredConnector, DashboardConfig, HealthResponse, InMemoryStorage, ServerStorage, SyncState, SyncStatus, TriggerSyncResponse, WidgetsListResponse, computeMetric, isSyncActive } from '@rawdash/core';
|
|
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';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Per-request lookup functions an HTTP adapter passes to engine handlers.
|
|
@@ -46,6 +46,60 @@ declare const ROUTES: {
|
|
|
46
46
|
};
|
|
47
47
|
};
|
|
48
48
|
|
|
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
|
+
}
|
|
49
103
|
/**
|
|
50
104
|
* Framework-agnostic request handlers for the rawdash wire contract.
|
|
51
105
|
*
|
|
@@ -57,12 +111,21 @@ declare const ROUTES: {
|
|
|
57
111
|
*/
|
|
58
112
|
declare function getHealth(): HealthResponse;
|
|
59
113
|
declare function getSyncStateHandler(ctx: EngineContext): Promise<SyncState>;
|
|
60
|
-
declare function triggerSync(ctx:
|
|
61
|
-
|
|
62
|
-
|
|
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>;
|
|
63
122
|
declare function runRetentionOnce(ctx: EngineContext): Promise<void>;
|
|
64
123
|
|
|
65
124
|
declare const FULL_SYNC_TIMEOUT_MS = 300000;
|
|
125
|
+
interface RunSyncOptions {
|
|
126
|
+
connectorRegistry: ConnectorRegistry;
|
|
127
|
+
secretsResolver?: SecretsResolver;
|
|
128
|
+
}
|
|
66
129
|
/**
|
|
67
130
|
* Run a full sync across all connectors in the config in parallel via
|
|
68
131
|
* `Promise.allSettled`, so one failure doesn't abort the others. Each
|
|
@@ -84,7 +147,7 @@ declare const FULL_SYNC_TIMEOUT_MS = 300000;
|
|
|
84
147
|
*
|
|
85
148
|
* Returns silently if another sync acquired the `running` lock first.
|
|
86
149
|
*/
|
|
87
|
-
declare function runSync(config: DashboardConfig, storage: ServerStorage): Promise<void>;
|
|
150
|
+
declare function runSync(config: DashboardConfig, storage: ServerStorage, options: RunSyncOptions): Promise<void>;
|
|
88
151
|
|
|
89
152
|
declare const DEFAULT_RETENTION_INTERVAL_MS: number;
|
|
90
153
|
declare function hasPruningPolicy(config: RetentionConfig): boolean;
|
|
@@ -97,6 +160,8 @@ declare function runRetention(config: DashboardConfig, storage: ServerStorage):
|
|
|
97
160
|
|
|
98
161
|
interface EngineOptions {
|
|
99
162
|
storage?: ServerStorage;
|
|
163
|
+
connectorRegistry?: ConnectorRegistry;
|
|
164
|
+
secretsResolver?: SecretsResolver;
|
|
100
165
|
}
|
|
101
166
|
interface Engine {
|
|
102
167
|
getWidget(dashboardId: string, widgetId: string): Promise<CachedWidget | undefined>;
|
|
@@ -107,4 +172,4 @@ interface Engine {
|
|
|
107
172
|
}
|
|
108
173
|
declare function createEngine(config: DashboardConfig, options?: EngineOptions): Engine;
|
|
109
174
|
|
|
110
|
-
export { DEFAULT_RETENTION_INTERVAL_MS, type Engine, type EngineContext, type EngineOptions, FULL_SYNC_TIMEOUT_MS, ROUTES, RawdashError, createEngine, getHealth, getSyncStateHandler, getWidget, hasPruningPolicy, isRawdashError, listWidgets, runRetention, runRetentionOnce, runSync, triggerSync };
|
|
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
|
@@ -41,8 +41,8 @@ async function runRetention(config, storage) {
|
|
|
41
41
|
}
|
|
42
42
|
const nowMs = Date.now();
|
|
43
43
|
const results = await Promise.allSettled(
|
|
44
|
-
config.connectors.map(async (
|
|
45
|
-
const handle = storage.getStorageHandle(
|
|
44
|
+
config.connectors.map(async (entry) => {
|
|
45
|
+
const handle = storage.getStorageHandle(entry.name);
|
|
46
46
|
const [events, metrics, distributions] = await Promise.all([
|
|
47
47
|
handle.queryEvents({}),
|
|
48
48
|
handle.queryMetrics({}),
|
|
@@ -95,8 +95,9 @@ async function applyRetentionToShape(rows, getTs, config, nowMs, writeSurvivors)
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// src/sync.ts
|
|
98
|
+
import { instantiateConnector } from "@rawdash/core";
|
|
98
99
|
var FULL_SYNC_TIMEOUT_MS = 3e5;
|
|
99
|
-
async function runSync(config, storage) {
|
|
100
|
+
async function runSync(config, storage, options) {
|
|
100
101
|
await storage.markSyncQueued();
|
|
101
102
|
const acquired = await storage.markSyncRunning();
|
|
102
103
|
if (!acquired) {
|
|
@@ -104,13 +105,21 @@ async function runSync(config, storage) {
|
|
|
104
105
|
}
|
|
105
106
|
const errors = [];
|
|
106
107
|
await Promise.allSettled(
|
|
107
|
-
config.connectors.map(async (
|
|
108
|
+
config.connectors.map(async (entry) => {
|
|
109
|
+
if (entry.enabled === false) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
108
112
|
const controller = new AbortController();
|
|
109
|
-
const handle = storage.getStorageHandle(
|
|
113
|
+
const handle = storage.getStorageHandle(entry.name, {
|
|
110
114
|
signal: controller.signal
|
|
111
115
|
});
|
|
112
116
|
let timer;
|
|
113
117
|
try {
|
|
118
|
+
const connector = instantiateConnector(
|
|
119
|
+
entry,
|
|
120
|
+
options.connectorRegistry,
|
|
121
|
+
options.secretsResolver
|
|
122
|
+
);
|
|
114
123
|
const syncPromise = connector.sync(
|
|
115
124
|
{ mode: "full" },
|
|
116
125
|
handle,
|
|
@@ -120,7 +129,7 @@ async function runSync(config, storage) {
|
|
|
120
129
|
timer = setTimeout(() => {
|
|
121
130
|
controller.abort();
|
|
122
131
|
const err = new Error(
|
|
123
|
-
`${
|
|
132
|
+
`${entry.name} timed out after ${FULL_SYNC_TIMEOUT_MS}ms`
|
|
124
133
|
);
|
|
125
134
|
err.name = "AbortError";
|
|
126
135
|
reject(err);
|
|
@@ -129,13 +138,13 @@ async function runSync(config, storage) {
|
|
|
129
138
|
const result = await Promise.race([syncPromise, timeoutPromise]);
|
|
130
139
|
if (!result.done) {
|
|
131
140
|
errors.push(
|
|
132
|
-
`${
|
|
141
|
+
`${entry.name} did not complete in one chunk (chunked syncs are only supported in cloud)`
|
|
133
142
|
);
|
|
134
143
|
}
|
|
135
144
|
} catch (err) {
|
|
136
145
|
if (err instanceof Error && err.name === "AbortError") {
|
|
137
146
|
errors.push(
|
|
138
|
-
`${
|
|
147
|
+
`${entry.name} timed out after ${FULL_SYNC_TIMEOUT_MS}ms`
|
|
139
148
|
);
|
|
140
149
|
} else {
|
|
141
150
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
@@ -155,6 +164,34 @@ async function runSync(config, storage) {
|
|
|
155
164
|
}
|
|
156
165
|
|
|
157
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;
|
|
173
|
+
}
|
|
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);
|
|
180
|
+
}
|
|
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
|
+
}
|
|
158
195
|
function getHealth() {
|
|
159
196
|
return { status: "ok" };
|
|
160
197
|
}
|
|
@@ -162,39 +199,63 @@ async function getSyncStateHandler(ctx) {
|
|
|
162
199
|
const storage = await ctx.getStorage();
|
|
163
200
|
return storage.getSyncState();
|
|
164
201
|
}
|
|
165
|
-
async function triggerSync(ctx) {
|
|
202
|
+
async function triggerSync(ctx, opts = {}) {
|
|
203
|
+
const mode = opts.mode ?? "in-process";
|
|
166
204
|
const storage = await ctx.getStorage();
|
|
167
205
|
const state = await storage.getSyncState();
|
|
168
206
|
if (isSyncActive(state.status)) {
|
|
169
207
|
return { queued: false };
|
|
170
208
|
}
|
|
171
|
-
|
|
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"'
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
config = await ctx.getConfig();
|
|
217
|
+
}
|
|
172
218
|
const queued = await storage.markSyncQueued();
|
|
173
219
|
if (!queued) {
|
|
174
220
|
return { queued: false };
|
|
175
221
|
}
|
|
176
|
-
|
|
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) => {
|
|
177
230
|
console.error("Rawdash sync failed", err);
|
|
178
231
|
});
|
|
179
232
|
return { queued: true };
|
|
180
233
|
}
|
|
181
|
-
async function listWidgets(ctx, dashboardId) {
|
|
234
|
+
async function listWidgets(ctx, dashboardId, cache) {
|
|
182
235
|
const config = await ctx.getConfig();
|
|
183
236
|
const dashboard = config.dashboards[dashboardId];
|
|
184
237
|
if (!dashboard) {
|
|
185
238
|
throw new RawdashError(404, "DASHBOARD_NOT_FOUND", "Dashboard not found");
|
|
186
239
|
}
|
|
187
240
|
const storage = await ctx.getStorage();
|
|
241
|
+
const connectorNames = config.connectors.map((c) => c.name);
|
|
188
242
|
const entries = Object.entries(dashboard.widgets);
|
|
189
243
|
const resolved = await Promise.all(
|
|
190
244
|
entries.map(
|
|
191
|
-
([key, widget]) =>
|
|
245
|
+
([key, widget]) => resolveWithCache(
|
|
246
|
+
dashboardId,
|
|
247
|
+
key,
|
|
248
|
+
widget,
|
|
249
|
+
connectorNames,
|
|
250
|
+
storage,
|
|
251
|
+
cache
|
|
252
|
+
)
|
|
192
253
|
)
|
|
193
254
|
);
|
|
194
255
|
const widgets = resolved.filter((w) => w !== void 0);
|
|
195
256
|
return { widgets };
|
|
196
257
|
}
|
|
197
|
-
async function getWidget(ctx, dashboardId, widgetId) {
|
|
258
|
+
async function getWidget(ctx, dashboardId, widgetId, cache) {
|
|
198
259
|
const config = await ctx.getConfig();
|
|
199
260
|
const dashboard = config.dashboards[dashboardId];
|
|
200
261
|
if (!dashboard) {
|
|
@@ -205,11 +266,14 @@ async function getWidget(ctx, dashboardId, widgetId) {
|
|
|
205
266
|
throw new RawdashError(404, "WIDGET_NOT_FOUND", "Widget not found");
|
|
206
267
|
}
|
|
207
268
|
const storage = await ctx.getStorage();
|
|
208
|
-
const
|
|
269
|
+
const connectorNames = config.connectors.map((c) => c.name);
|
|
270
|
+
const result = await resolveWithCache(
|
|
271
|
+
dashboardId,
|
|
209
272
|
widgetId,
|
|
210
273
|
widget,
|
|
211
|
-
|
|
212
|
-
storage
|
|
274
|
+
connectorNames,
|
|
275
|
+
storage,
|
|
276
|
+
cache
|
|
213
277
|
);
|
|
214
278
|
if (!result) {
|
|
215
279
|
throw new RawdashError(404, "WIDGET_NOT_FOUND", "Widget not found");
|
|
@@ -226,6 +290,7 @@ async function runRetentionOnce(ctx) {
|
|
|
226
290
|
import { InMemoryStorage, isSyncActive as isSyncActive2, resolveWidget as resolveWidget2 } from "@rawdash/core";
|
|
227
291
|
function createEngine(config, options = {}) {
|
|
228
292
|
const storage = options.storage ?? new InMemoryStorage();
|
|
293
|
+
const connectorNames = config.connectors.map((c) => c.name);
|
|
229
294
|
return {
|
|
230
295
|
async getWidget(dashboardId, widgetId) {
|
|
231
296
|
const dashboard = config.dashboards[dashboardId];
|
|
@@ -236,7 +301,7 @@ function createEngine(config, options = {}) {
|
|
|
236
301
|
if (!widget) {
|
|
237
302
|
return void 0;
|
|
238
303
|
}
|
|
239
|
-
return resolveWidget2(widgetId, widget,
|
|
304
|
+
return resolveWidget2(widgetId, widget, connectorNames, storage);
|
|
240
305
|
},
|
|
241
306
|
async getWidgets(dashboardId) {
|
|
242
307
|
const dashboard = config.dashboards[dashboardId];
|
|
@@ -246,7 +311,7 @@ function createEngine(config, options = {}) {
|
|
|
246
311
|
const entries = Object.entries(dashboard.widgets);
|
|
247
312
|
const resolved = await Promise.all(
|
|
248
313
|
entries.map(
|
|
249
|
-
([key, widget]) => resolveWidget2(key, widget,
|
|
314
|
+
([key, widget]) => resolveWidget2(key, widget, connectorNames, storage)
|
|
250
315
|
)
|
|
251
316
|
);
|
|
252
317
|
return resolved.filter((w) => w !== void 0);
|
|
@@ -258,6 +323,11 @@ function createEngine(config, options = {}) {
|
|
|
258
323
|
return storage.getSyncState();
|
|
259
324
|
},
|
|
260
325
|
async triggerSync() {
|
|
326
|
+
if (!options.connectorRegistry) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
"createEngine: connectorRegistry is required to triggerSync"
|
|
329
|
+
);
|
|
330
|
+
}
|
|
261
331
|
const state = await storage.getSyncState();
|
|
262
332
|
if (isSyncActive2(state.status)) {
|
|
263
333
|
return { queued: false };
|
|
@@ -266,7 +336,10 @@ function createEngine(config, options = {}) {
|
|
|
266
336
|
if (!queued) {
|
|
267
337
|
return { queued: false };
|
|
268
338
|
}
|
|
269
|
-
void runSync(config, storage
|
|
339
|
+
void runSync(config, storage, {
|
|
340
|
+
connectorRegistry: options.connectorRegistry,
|
|
341
|
+
secretsResolver: options.secretsResolver
|
|
342
|
+
}).catch((error) => {
|
|
270
343
|
console.error("Rawdash sync failed", error);
|
|
271
344
|
});
|
|
272
345
|
return { queued: true };
|
|
@@ -282,6 +355,7 @@ import { InMemoryStorage as InMemoryStorage2 } from "@rawdash/core";
|
|
|
282
355
|
|
|
283
356
|
// src/index.ts
|
|
284
357
|
import { isSyncActive as isSyncActive3, ACTIVE_SYNC_STATUSES } from "@rawdash/core";
|
|
358
|
+
import { instantiateConnector as instantiateConnector2 } from "@rawdash/core";
|
|
285
359
|
export {
|
|
286
360
|
ACTIVE_SYNC_STATUSES,
|
|
287
361
|
DEFAULT_RETENTION_INTERVAL_MS,
|
|
@@ -295,6 +369,7 @@ export {
|
|
|
295
369
|
getSyncStateHandler,
|
|
296
370
|
getWidget,
|
|
297
371
|
hasPruningPolicy,
|
|
372
|
+
instantiateConnector2 as instantiateConnector,
|
|
298
373
|
isRawdashError,
|
|
299
374
|
isSyncActive3 as isSyncActive,
|
|
300
375
|
listWidgets,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/routes.ts","../src/handlers.ts","../src/retention.ts","../src/sync.ts","../src/engine.ts","../src/compute.ts","../src/storage.ts","../src/index.ts"],"sourcesContent":["/**\n * Thrown by `@rawdash/server` handlers when a request fails in a way the\n * HTTP adapter should translate to a structured response (e.g. 404, 400).\n * Framework adapters (`@rawdash/hono`, etc.) should catch this and map\n * `status` to the appropriate HTTP status code.\n */\nexport class RawdashError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n message: string,\n ) {\n super(message);\n this.name = 'RawdashError';\n }\n}\n\nexport function isRawdashError(err: unknown): err is RawdashError {\n return err instanceof RawdashError;\n}\n","/**\n * Canonical URL path conventions for the rawdash HTTP wire contract.\n *\n * Framework adapters (`@rawdash/hono`, etc.) and clients\n * (`@rawdash/client`) should use these constants instead of hard-coding\n * paths, so the contract stays in one place.\n */\nexport const ROUTES = {\n health: '/health',\n syncState: '/sync/state',\n sync: '/sync',\n retention: '/retention/retain',\n widgets: {\n list: (dashboardId: string): string =>\n `/dashboards/${encodeURIComponent(dashboardId)}/widgets`,\n single: (dashboardId: string, widgetId: string): string =>\n `/dashboards/${encodeURIComponent(dashboardId)}/widgets/${encodeURIComponent(widgetId)}`,\n },\n} as const;\n","import type {\n CachedWidget,\n HealthResponse,\n SyncState,\n TriggerSyncResponse,\n WidgetsListResponse,\n} from '@rawdash/core';\nimport { isSyncActive, resolveWidget } from '@rawdash/core';\n\nimport type { EngineContext } from './context';\nimport { RawdashError } from './errors';\nimport { runRetention } from './retention';\nimport { runSync } from './sync';\n\n/**\n * Framework-agnostic request handlers for the rawdash wire contract.\n *\n * Each function takes an `EngineContext` (providing per-request access to\n * the config + storage) and returns the response body, or throws a\n * `RawdashError` on a client-visible failure. HTTP adapters\n * (`@rawdash/hono`, etc.) wrap these in their framework's request/response\n * cycle and translate `RawdashError` into a structured error response.\n */\n\nexport function getHealth(): HealthResponse {\n return { status: 'ok' };\n}\n\nexport async function getSyncStateHandler(\n ctx: EngineContext,\n): Promise<SyncState> {\n const storage = await ctx.getStorage();\n return storage.getSyncState();\n}\n\nexport async function triggerSync(\n ctx: EngineContext,\n): Promise<TriggerSyncResponse> {\n const storage = await ctx.getStorage();\n const state = await storage.getSyncState();\n if (isSyncActive(state.status)) {\n return { queued: false };\n }\n // Load the config *before* marking queued — if config loading rejects\n // after we've persisted the queued transition, the sync state would be\n // stuck in `queued` with no background run to drain it.\n const config = await ctx.getConfig();\n // Persist the queued transition synchronously so clients polling\n // /sync/state right after the trigger see `queued` (with queuedAt),\n // not a stale terminal state.\n const queued = await storage.markSyncQueued();\n if (!queued) {\n return { queued: false };\n }\n void runSync(config, storage).catch((err) => {\n console.error('Rawdash sync failed', err);\n });\n return { queued: true };\n}\n\nexport async function listWidgets(\n ctx: EngineContext,\n dashboardId: string,\n): Promise<WidgetsListResponse> {\n const config = await ctx.getConfig();\n const dashboard = config.dashboards[dashboardId];\n if (!dashboard) {\n throw new RawdashError(404, 'DASHBOARD_NOT_FOUND', 'Dashboard not found');\n }\n const storage = await ctx.getStorage();\n const entries = Object.entries(dashboard.widgets);\n const resolved = await Promise.all(\n entries.map(([key, widget]) =>\n resolveWidget(key, widget, config.connectors, storage),\n ),\n );\n const widgets = resolved.filter((w): w is CachedWidget => w !== undefined);\n return { widgets };\n}\n\nexport async function getWidget(\n ctx: EngineContext,\n dashboardId: string,\n widgetId: string,\n): Promise<CachedWidget> {\n const config = await ctx.getConfig();\n const dashboard = config.dashboards[dashboardId];\n if (!dashboard) {\n throw new RawdashError(404, 'DASHBOARD_NOT_FOUND', 'Dashboard not found');\n }\n const widget = dashboard.widgets[widgetId];\n if (!widget) {\n throw new RawdashError(404, 'WIDGET_NOT_FOUND', 'Widget not found');\n }\n const storage = await ctx.getStorage();\n const result = await resolveWidget(\n widgetId,\n widget,\n config.connectors,\n storage,\n );\n if (!result) {\n throw new RawdashError(404, 'WIDGET_NOT_FOUND', 'Widget not found');\n }\n return result;\n}\n\nexport async function runRetentionOnce(ctx: EngineContext): Promise<void> {\n const config = await ctx.getConfig();\n const storage = await ctx.getStorage();\n await runRetention(config, storage);\n}\n","import type {\n DashboardConfig,\n RetentionConfig,\n ServerStorage,\n} from '@rawdash/core';\nimport { selectForDeletion } from '@rawdash/core';\n\nexport const DEFAULT_RETENTION_INTERVAL_MS = 60 * 60 * 1000; // 1 hour\n\nexport function hasPruningPolicy(config: RetentionConfig): boolean {\n return config.maxAge !== undefined || config.maxSize !== undefined;\n}\n\n/**\n * Apply the retention policy in `config` to every connector's stored data.\n * No-op if the config has no pruning policy. Throws an aggregated error if\n * any connector fails.\n */\nexport async function runRetention(\n config: DashboardConfig,\n storage: ServerStorage,\n): Promise<void> {\n const retentionConfig = config.retention;\n if (!retentionConfig || !hasPruningPolicy(retentionConfig)) {\n return;\n }\n\n const nowMs = Date.now();\n\n const results = await Promise.allSettled(\n config.connectors.map(async ({ connector }) => {\n const handle = storage.getStorageHandle(connector.id);\n\n const [events, metrics, distributions] = await Promise.all([\n handle.queryEvents({}),\n handle.queryMetrics({}),\n handle.queryDistributions({}),\n ]);\n\n await applyRetentionToShape(\n events,\n (e) => e.start_ts,\n retentionConfig,\n nowMs,\n (survivors, names) => handle.events(survivors, { names }),\n );\n\n await applyRetentionToShape(\n metrics,\n (m) => m.ts,\n retentionConfig,\n nowMs,\n (survivors, names) => handle.metrics(survivors, { names }),\n );\n\n await applyRetentionToShape(\n distributions,\n (d) => d.ts,\n retentionConfig,\n nowMs,\n (survivors, names) => handle.distributions(survivors, { names }),\n );\n }),\n );\n\n const failures = results.filter(\n (r): r is PromiseRejectedResult => r.status === 'rejected',\n );\n if (failures.length > 0) {\n throw new Error(\n `Retention failed for ${failures.length} connector(s): ${failures.map((f) => String(f.reason)).join('; ')}`,\n );\n }\n}\n\nasync function applyRetentionToShape<T extends { name: string }>(\n rows: T[],\n getTs: (row: T) => number,\n config: RetentionConfig,\n nowMs: number,\n writeSurvivors: (survivors: T[], names: string[]) => Promise<void>,\n): Promise<void> {\n if (rows.length === 0) {\n return;\n }\n\n const sorted = [...rows].sort((a, b) => getTs(b) - getTs(a));\n const toDeleteSet = new Set(selectForDeletion(sorted, getTs, config, nowMs));\n\n if (toDeleteSet.size === 0) {\n return;\n }\n\n const survivors = sorted.filter((r) => !toDeleteSet.has(r));\n const allNames = [...new Set(rows.map((r) => r.name))];\n\n await writeSurvivors(survivors, allNames);\n}\n","import type { DashboardConfig, ServerStorage } from '@rawdash/core';\n\nexport const FULL_SYNC_TIMEOUT_MS = 300_000;\n\n/**\n * Run a full sync across all connectors in the config in parallel via\n * `Promise.allSettled`, so one failure doesn't abort the others. Each\n * connector run is wrapped in a hard timeout (`FULL_SYNC_TIMEOUT_MS`)\n * raced against the connector's own `Promise`, so a connector that\n * ignores `AbortSignal` (or a storage call that hangs) can still not\n * pin sync state in `running` indefinitely.\n *\n * The per-run storage handle is bound to the same `AbortController`, so\n * once the timeout fires every subsequent write call on that handle\n * becomes a no-op. That makes tail writes from a timed-out connector\n * invisible to the next sync even if the connector itself keeps running\n * — see `withAbortSignal` in `@rawdash/core` and the safety-net note in\n * `docs/authoring-a-connector.md`.\n *\n * Transitions storage through `queued` → `running` → `succeeded`/`failed`.\n * The `queued` step is a no-op if the caller (typically `triggerSync`)\n * already marked the run as queued.\n *\n * Returns silently if another sync acquired the `running` lock first.\n */\nexport async function runSync(\n config: DashboardConfig,\n storage: ServerStorage,\n): Promise<void> {\n // Idempotent: if the caller already queued, this returns false and we\n // proceed to markSyncRunning anyway. If nothing queued us, we queue\n // ourselves now so the state machine still goes through `queued`.\n await storage.markSyncQueued();\n const acquired = await storage.markSyncRunning();\n if (!acquired) {\n return;\n }\n const errors: string[] = [];\n await Promise.allSettled(\n config.connectors.map(async ({ connector }) => {\n const controller = new AbortController();\n const handle = storage.getStorageHandle(connector.id, {\n signal: controller.signal,\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n const syncPromise = connector.sync(\n { mode: 'full' },\n handle,\n controller.signal,\n );\n const timeoutPromise = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n controller.abort();\n const err = new Error(\n `${connector.id} timed out after ${FULL_SYNC_TIMEOUT_MS}ms`,\n );\n err.name = 'AbortError';\n reject(err);\n }, FULL_SYNC_TIMEOUT_MS);\n });\n const result = await Promise.race([syncPromise, timeoutPromise]);\n if (!result.done) {\n errors.push(\n `${connector.id} did not complete in one chunk (chunked syncs are only supported in cloud)`,\n );\n }\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n errors.push(\n `${connector.id} timed out after ${FULL_SYNC_TIMEOUT_MS}ms`,\n );\n } else {\n errors.push(err instanceof Error ? err.message : String(err));\n }\n } finally {\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n }\n }),\n );\n if (errors.length > 0) {\n await storage.markSyncFailed(errors.join('; '));\n } else {\n await storage.markSyncSucceeded();\n }\n}\n","import type {\n CachedWidget,\n DashboardConfig,\n HealthResponse,\n ServerStorage,\n SyncState,\n TriggerSyncResponse,\n} from '@rawdash/core';\nimport { InMemoryStorage, isSyncActive, resolveWidget } from '@rawdash/core';\n\nimport { runSync } from './sync';\n\nexport interface EngineOptions {\n storage?: ServerStorage;\n}\n\nexport interface Engine {\n getWidget(\n dashboardId: string,\n widgetId: string,\n ): Promise<CachedWidget | undefined>;\n getWidgets(dashboardId: string): Promise<CachedWidget[]>;\n getHealth(): Promise<HealthResponse>;\n getSyncState(): Promise<SyncState>;\n triggerSync(): Promise<TriggerSyncResponse>;\n}\n\nexport function createEngine(\n config: DashboardConfig,\n options: EngineOptions = {},\n): Engine {\n const storage: ServerStorage = options.storage ?? new InMemoryStorage();\n\n return {\n async getWidget(dashboardId, widgetId) {\n const dashboard = config.dashboards[dashboardId];\n if (!dashboard) {\n return undefined;\n }\n const widget = dashboard.widgets[widgetId];\n if (!widget) {\n return undefined;\n }\n return resolveWidget(widgetId, widget, config.connectors, storage);\n },\n\n async getWidgets(dashboardId) {\n const dashboard = config.dashboards[dashboardId];\n if (!dashboard) {\n return [];\n }\n const entries = Object.entries(dashboard.widgets);\n const resolved = await Promise.all(\n entries.map(([key, widget]) =>\n resolveWidget(key, widget, config.connectors, storage),\n ),\n );\n return resolved.filter((w): w is CachedWidget => w !== undefined);\n },\n\n async getHealth() {\n return { status: 'ok' };\n },\n\n async getSyncState() {\n return storage.getSyncState();\n },\n\n async triggerSync() {\n const state = await storage.getSyncState();\n if (isSyncActive(state.status)) {\n return { queued: false };\n }\n const queued = await storage.markSyncQueued();\n if (!queued) {\n return { queued: false };\n }\n void runSync(config, storage).catch((error) => {\n console.error('Rawdash sync failed', error);\n });\n return { queued: true };\n },\n };\n}\n","export { computeMetric } from '@rawdash/core';\n","export { InMemoryStorage } from '@rawdash/core';\n","export type { EngineContext } from './context';\nexport { RawdashError, isRawdashError } from './errors';\nexport { ROUTES } from './routes';\nexport {\n getHealth,\n getSyncStateHandler,\n getWidget,\n listWidgets,\n runRetentionOnce,\n triggerSync,\n} from './handlers';\nexport { runSync, FULL_SYNC_TIMEOUT_MS } from './sync';\nexport {\n runRetention,\n hasPruningPolicy,\n DEFAULT_RETENTION_INTERVAL_MS,\n} from './retention';\nexport { createEngine } from './engine';\nexport type { Engine, EngineOptions } from './engine';\nexport { computeMetric } from './compute';\nexport { InMemoryStorage } from './storage';\nexport type {\n CachedWidget,\n ConfiguredConnector,\n DashboardConfig,\n HealthResponse,\n ServerStorage,\n SyncState,\n SyncStatus,\n TriggerSyncResponse,\n WidgetsListResponse,\n} from './types';\nexport { isSyncActive, ACTIVE_SYNC_STATUSES } from '@rawdash/core';\n"],"mappings":";AAMO,IAAM,eAAN,cAA2B,MAAM;AAAA,EACtC,YACW,QACA,MACT,SACA;AACA,UAAM,OAAO;AAJJ;AACA;AAIT,SAAK,OAAO;AAAA,EACd;AAAA,EANW;AAAA,EACA;AAMb;AAEO,SAAS,eAAe,KAAmC;AAChE,SAAO,eAAe;AACxB;;;ACZO,IAAM,SAAS;AAAA,EACpB,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,MAAM;AAAA,EACN,WAAW;AAAA,EACX,SAAS;AAAA,IACP,MAAM,CAAC,gBACL,eAAe,mBAAmB,WAAW,CAAC;AAAA,IAChD,QAAQ,CAAC,aAAqB,aAC5B,eAAe,mBAAmB,WAAW,CAAC,YAAY,mBAAmB,QAAQ,CAAC;AAAA,EAC1F;AACF;;;ACXA,SAAS,cAAc,qBAAqB;;;ACF5C,SAAS,yBAAyB;AAE3B,IAAM,gCAAgC,KAAK,KAAK;AAEhD,SAAS,iBAAiB,QAAkC;AACjE,SAAO,OAAO,WAAW,UAAa,OAAO,YAAY;AAC3D;AAOA,eAAsB,aACpB,QACA,SACe;AACf,QAAM,kBAAkB,OAAO;AAC/B,MAAI,CAAC,mBAAmB,CAAC,iBAAiB,eAAe,GAAG;AAC1D;AAAA,EACF;AAEA,QAAM,QAAQ,KAAK,IAAI;AAEvB,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,OAAO,WAAW,IAAI,OAAO,EAAE,UAAU,MAAM;AAC7C,YAAM,SAAS,QAAQ,iBAAiB,UAAU,EAAE;AAEpD,YAAM,CAAC,QAAQ,SAAS,aAAa,IAAI,MAAM,QAAQ,IAAI;AAAA,QACzD,OAAO,YAAY,CAAC,CAAC;AAAA,QACrB,OAAO,aAAa,CAAC,CAAC;AAAA,QACtB,OAAO,mBAAmB,CAAC,CAAC;AAAA,MAC9B,CAAC;AAED,YAAM;AAAA,QACJ;AAAA,QACA,CAAC,MAAM,EAAE;AAAA,QACT;AAAA,QACA;AAAA,QACA,CAAC,WAAW,UAAU,OAAO,OAAO,WAAW,EAAE,MAAM,CAAC;AAAA,MAC1D;AAEA,YAAM;AAAA,QACJ;AAAA,QACA,CAAC,MAAM,EAAE;AAAA,QACT;AAAA,QACA;AAAA,QACA,CAAC,WAAW,UAAU,OAAO,QAAQ,WAAW,EAAE,MAAM,CAAC;AAAA,MAC3D;AAEA,YAAM;AAAA,QACJ;AAAA,QACA,CAAC,MAAM,EAAE;AAAA,QACT;AAAA,QACA;AAAA,QACA,CAAC,WAAW,UAAU,OAAO,cAAc,WAAW,EAAE,MAAM,CAAC;AAAA,MACjE;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,WAAW,QAAQ;AAAA,IACvB,CAAC,MAAkC,EAAE,WAAW;AAAA,EAClD;AACA,MAAI,SAAS,SAAS,GAAG;AACvB,UAAM,IAAI;AAAA,MACR,wBAAwB,SAAS,MAAM,kBAAkB,SAAS,IAAI,CAAC,MAAM,OAAO,EAAE,MAAM,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA,IAC3G;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;;;AC/FO,IAAM,uBAAuB;AAuBpC,eAAsB,QACpB,QACA,SACe;AAIf,QAAM,QAAQ,eAAe;AAC7B,QAAM,WAAW,MAAM,QAAQ,gBAAgB;AAC/C,MAAI,CAAC,UAAU;AACb;AAAA,EACF;AACA,QAAM,SAAmB,CAAC;AAC1B,QAAM,QAAQ;AAAA,IACZ,OAAO,WAAW,IAAI,OAAO,EAAE,UAAU,MAAM;AAC7C,YAAM,aAAa,IAAI,gBAAgB;AACvC,YAAM,SAAS,QAAQ,iBAAiB,UAAU,IAAI;AAAA,QACpD,QAAQ,WAAW;AAAA,MACrB,CAAC;AACD,UAAI;AACJ,UAAI;AACF,cAAM,cAAc,UAAU;AAAA,UAC5B,EAAE,MAAM,OAAO;AAAA,UACf;AAAA,UACA,WAAW;AAAA,QACb;AACA,cAAM,iBAAiB,IAAI,QAAe,CAAC,UAAU,WAAW;AAC9D,kBAAQ,WAAW,MAAM;AACvB,uBAAW,MAAM;AACjB,kBAAM,MAAM,IAAI;AAAA,cACd,GAAG,UAAU,EAAE,oBAAoB,oBAAoB;AAAA,YACzD;AACA,gBAAI,OAAO;AACX,mBAAO,GAAG;AAAA,UACZ,GAAG,oBAAoB;AAAA,QACzB,CAAC;AACD,cAAM,SAAS,MAAM,QAAQ,KAAK,CAAC,aAAa,cAAc,CAAC;AAC/D,YAAI,CAAC,OAAO,MAAM;AAChB,iBAAO;AAAA,YACL,GAAG,UAAU,EAAE;AAAA,UACjB;AAAA,QACF;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,eAAe,SAAS,IAAI,SAAS,cAAc;AACrD,iBAAO;AAAA,YACL,GAAG,UAAU,EAAE,oBAAoB,oBAAoB;AAAA,UACzD;AAAA,QACF,OAAO;AACL,iBAAO,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,QAC9D;AAAA,MACF,UAAE;AACA,YAAI,UAAU,QAAW;AACvB,uBAAa,KAAK;AAAA,QACpB;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACA,MAAI,OAAO,SAAS,GAAG;AACrB,UAAM,QAAQ,eAAe,OAAO,KAAK,IAAI,CAAC;AAAA,EAChD,OAAO;AACL,UAAM,QAAQ,kBAAkB;AAAA,EAClC;AACF;;;AF/DO,SAAS,YAA4B;AAC1C,SAAO,EAAE,QAAQ,KAAK;AACxB;AAEA,eAAsB,oBACpB,KACoB;AACpB,QAAM,UAAU,MAAM,IAAI,WAAW;AACrC,SAAO,QAAQ,aAAa;AAC9B;AAEA,eAAsB,YACpB,KAC8B;AAC9B,QAAM,UAAU,MAAM,IAAI,WAAW;AACrC,QAAM,QAAQ,MAAM,QAAQ,aAAa;AACzC,MAAI,aAAa,MAAM,MAAM,GAAG;AAC9B,WAAO,EAAE,QAAQ,MAAM;AAAA,EACzB;AAIA,QAAM,SAAS,MAAM,IAAI,UAAU;AAInC,QAAM,SAAS,MAAM,QAAQ,eAAe;AAC5C,MAAI,CAAC,QAAQ;AACX,WAAO,EAAE,QAAQ,MAAM;AAAA,EACzB;AACA,OAAK,QAAQ,QAAQ,OAAO,EAAE,MAAM,CAAC,QAAQ;AAC3C,YAAQ,MAAM,uBAAuB,GAAG;AAAA,EAC1C,CAAC;AACD,SAAO,EAAE,QAAQ,KAAK;AACxB;AAEA,eAAsB,YACpB,KACA,aAC8B;AAC9B,QAAM,SAAS,MAAM,IAAI,UAAU;AACnC,QAAM,YAAY,OAAO,WAAW,WAAW;AAC/C,MAAI,CAAC,WAAW;AACd,UAAM,IAAI,aAAa,KAAK,uBAAuB,qBAAqB;AAAA,EAC1E;AACA,QAAM,UAAU,MAAM,IAAI,WAAW;AACrC,QAAM,UAAU,OAAO,QAAQ,UAAU,OAAO;AAChD,QAAM,WAAW,MAAM,QAAQ;AAAA,IAC7B,QAAQ;AAAA,MAAI,CAAC,CAAC,KAAK,MAAM,MACvB,cAAc,KAAK,QAAQ,OAAO,YAAY,OAAO;AAAA,IACvD;AAAA,EACF;AACA,QAAM,UAAU,SAAS,OAAO,CAAC,MAAyB,MAAM,MAAS;AACzE,SAAO,EAAE,QAAQ;AACnB;AAEA,eAAsB,UACpB,KACA,aACA,UACuB;AACvB,QAAM,SAAS,MAAM,IAAI,UAAU;AACnC,QAAM,YAAY,OAAO,WAAW,WAAW;AAC/C,MAAI,CAAC,WAAW;AACd,UAAM,IAAI,aAAa,KAAK,uBAAuB,qBAAqB;AAAA,EAC1E;AACA,QAAM,SAAS,UAAU,QAAQ,QAAQ;AACzC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,aAAa,KAAK,oBAAoB,kBAAkB;AAAA,EACpE;AACA,QAAM,UAAU,MAAM,IAAI,WAAW;AACrC,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP;AAAA,EACF;AACA,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,aAAa,KAAK,oBAAoB,kBAAkB;AAAA,EACpE;AACA,SAAO;AACT;AAEA,eAAsB,iBAAiB,KAAmC;AACxE,QAAM,SAAS,MAAM,IAAI,UAAU;AACnC,QAAM,UAAU,MAAM,IAAI,WAAW;AACrC,QAAM,aAAa,QAAQ,OAAO;AACpC;;;AGvGA,SAAS,iBAAiB,gBAAAA,eAAc,iBAAAC,sBAAqB;AAmBtD,SAAS,aACd,QACA,UAAyB,CAAC,GAClB;AACR,QAAM,UAAyB,QAAQ,WAAW,IAAI,gBAAgB;AAEtE,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,EAAE,QAAQ,KAAK;AAAA,IACxB;AAAA,IAEA,MAAM,eAAe;AACnB,aAAO,QAAQ,aAAa;AAAA,IAC9B;AAAA,IAEA,MAAM,cAAc;AAClB,YAAM,QAAQ,MAAM,QAAQ,aAAa;AACzC,UAAIC,cAAa,MAAM,MAAM,GAAG;AAC9B,eAAO,EAAE,QAAQ,MAAM;AAAA,MACzB;AACA,YAAM,SAAS,MAAM,QAAQ,eAAe;AAC5C,UAAI,CAAC,QAAQ;AACX,eAAO,EAAE,QAAQ,MAAM;AAAA,MACzB;AACA,WAAK,QAAQ,QAAQ,OAAO,EAAE,MAAM,CAAC,UAAU;AAC7C,gBAAQ,MAAM,uBAAuB,KAAK;AAAA,MAC5C,CAAC;AACD,aAAO,EAAE,QAAQ,KAAK;AAAA,IACxB;AAAA,EACF;AACF;;;ACnFA,SAAS,qBAAqB;;;ACA9B,SAAS,mBAAAC,wBAAuB;;;ACgChC,SAAS,gBAAAC,eAAc,4BAA4B;","names":["isSyncActive","resolveWidget","resolveWidget","isSyncActive","InMemoryStorage","isSyncActive"]}
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rawdash/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
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",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@rawdash/core": "0.
|
|
25
|
+
"@rawdash/core": "0.15.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"tsup": "^8.0.0",
|