@dudousxd/nestjs-durable-dashboard 0.29.7 → 0.30.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/dist/client/durable-client.d.ts +33 -79
- package/dist/client/durable-client.d.ts.map +1 -1
- package/dist/client/durable-client.js.map +1 -1
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +4 -10
- package/dist/server/index.d.ts +4 -10
- package/dist/server/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,95 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
export type
|
|
1
|
+
import type { RunListItem as CoreRunListItem, RunStatus as CoreRunStatus, RunWaiting as CoreRunWaiting, StepCheckpoint as CoreStepCheckpoint, StepEvent as CoreStepEvent, StepKind as CoreStepKind, StepError, WireDates } from '@dudousxd/nestjs-durable-core';
|
|
2
|
+
export type RunStatus = CoreRunStatus;
|
|
3
|
+
export type StepKind = CoreStepKind;
|
|
3
4
|
/** This deployment's durable role — mirrors the server's `DurableTopology` (kept local so the SPA
|
|
4
5
|
* stays standalone). `tenant` is the isolation partition name, set only when `role` is 'tenant'. */
|
|
5
6
|
export interface DurableTopology {
|
|
6
7
|
role: 'control-plane' | 'tenant';
|
|
7
8
|
tenant?: string;
|
|
8
9
|
}
|
|
9
|
-
/** What an event-parked suspended run is waiting on
|
|
10
|
-
*
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export interface WorkflowRun {
|
|
16
|
-
id: string;
|
|
17
|
-
workflow: string;
|
|
18
|
-
workflowVersion: string;
|
|
19
|
-
status: RunStatus;
|
|
20
|
-
input?: unknown;
|
|
21
|
-
output?: unknown;
|
|
22
|
-
error?: {
|
|
23
|
-
message: string;
|
|
24
|
-
code?: string;
|
|
25
|
-
};
|
|
26
|
-
wakeAt?: number;
|
|
27
|
-
recoveryAttempts?: number;
|
|
28
|
-
tags?: string[];
|
|
29
|
-
searchAttributes?: Record<string, string | number | boolean>;
|
|
30
|
-
/** The worker-pool partition (tenant) this run belongs to. `'default'` (or absent) on a single-pool
|
|
31
|
-
* / control-plane run; a named tenant on a multi-tenant deployment — shown in the UI when set. */
|
|
32
|
-
namespace?: string;
|
|
33
|
-
/** Set on runs from `listRuns` (control-plane-enriched): what an event-parked suspended run waits on. */
|
|
34
|
-
waiting?: RunWaiting;
|
|
35
|
-
createdAt: string;
|
|
36
|
-
updatedAt: string;
|
|
37
|
-
}
|
|
38
|
-
export interface StepEvent {
|
|
39
|
-
at: number;
|
|
40
|
-
level: 'debug' | 'info' | 'warn' | 'error';
|
|
41
|
-
message: string;
|
|
42
|
-
/** Stable run identity for a sub-process; distinct invocations of the same `name` get distinct ids. */
|
|
43
|
-
subId?: string;
|
|
44
|
-
/** For a sub-step/sub-process within the step: its name. */
|
|
45
|
-
name?: string;
|
|
46
|
-
/** Open, consumer-defined grouping label for a sub-process (e.g. a handler/lane). */
|
|
47
|
-
group?: string;
|
|
48
|
-
/** For a sub-step: its terminal outcome. */
|
|
49
|
-
status?: 'ok' | 'failed' | 'skipped';
|
|
50
|
-
/** Open, consumer-defined intermediate phase label for a sub-process transition (no `status`). */
|
|
51
|
-
phase?: string;
|
|
52
|
-
/** @deprecated owning sub-process **name** for a log line — superseded by `subId`. */
|
|
53
|
-
process?: string;
|
|
54
|
-
data?: unknown;
|
|
55
|
-
}
|
|
56
|
-
export interface StepCheckpoint {
|
|
57
|
-
runId: string;
|
|
58
|
-
seq: number;
|
|
59
|
-
name: string;
|
|
60
|
-
kind: StepKind;
|
|
10
|
+
/** What an event-parked suspended run is waiting on. Derived from core: adding a new `on` variant
|
|
11
|
+
* there (e.g. `breakpoint`) shows up here with no client-side edit needed. */
|
|
12
|
+
export type RunWaiting = CoreRunWaiting;
|
|
13
|
+
/** No `Date` fields and already an exact structural match to core's `StepEvent` — derived directly. */
|
|
14
|
+
export type StepEvent = CoreStepEvent;
|
|
15
|
+
export type StepCheckpoint = Omit<WireDates<CoreStepCheckpoint>, 'stepId' | 'enqueuedAt' | 'error'> & {
|
|
61
16
|
/**
|
|
62
|
-
* `
|
|
63
|
-
*
|
|
17
|
+
* Kept OPTIONAL here (core's `enqueuedAt` is a required `Date`, always populated) — the two SPA
|
|
18
|
+
* reads of this field (`SpansTimeline.tsx`, `StepDetailPanel.tsx`) both treat an absent value as
|
|
19
|
+
* "nothing to show" for a local step's queue-wait, which is the display semantic that matters here,
|
|
20
|
+
* not core's storage guarantee. Deliberate, load-bearing local divergence.
|
|
64
21
|
*/
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
error?:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
/** Structured events the step (or its worker) emitted — sub-process outcomes + debug/error logs. */
|
|
73
|
-
events?: StepEvent[];
|
|
22
|
+
enqueuedAt?: string;
|
|
23
|
+
/** Widened from the old ad-hoc `{ message; code? }` to the real `StepError` shape (adds `retryable`/
|
|
24
|
+
* `stack`) — every existing read (`step.error.message`) still works; this just stops re-declaring a
|
|
25
|
+
* narrower shadow of the server's actual error type. */
|
|
26
|
+
error?: StepError;
|
|
27
|
+
};
|
|
28
|
+
export type WorkflowRun = Omit<WireDates<CoreRunListItem>, 'input' | 'error'> & {
|
|
74
29
|
/**
|
|
75
|
-
*
|
|
76
|
-
* `
|
|
77
|
-
*
|
|
30
|
+
* Kept OPTIONAL here (core's `input` is a required `unknown`) — several SPA fixtures build a
|
|
31
|
+
* `WorkflowRun` without ever setting it, and `unknown` can't be enforced-present in any way that
|
|
32
|
+
* matters to a reader; the SPA never round-trips this value back to the server. Deliberate, load-
|
|
33
|
+
* bearing local divergence (dropping it would force `input: undefined` onto every construction site).
|
|
78
34
|
*/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
/** When the step was dispatched (remote) or began (local). Queue-wait = startedAt − enqueuedAt. */
|
|
84
|
-
enqueuedAt?: string;
|
|
85
|
-
/** When processing actually began: worker pickup for a remote step, execution start for a local one. */
|
|
86
|
-
startedAt: string;
|
|
87
|
-
finishedAt: string;
|
|
88
|
-
}
|
|
35
|
+
input?: unknown;
|
|
36
|
+
/** See {@link StepCheckpoint.error} — same widen-not-narrow convergence. */
|
|
37
|
+
error?: StepError;
|
|
38
|
+
};
|
|
89
39
|
export interface RunDetail {
|
|
90
40
|
run: WorkflowRun;
|
|
91
41
|
timeline: StepCheckpoint[];
|
|
92
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Kept OPTIONAL here (core's `children` is a required, always-populated array) — the SPA's own
|
|
44
|
+
* preview/test fixtures (`preview.tsx`, `merge-live-events.spec.ts`) construct a `RunDetail` without
|
|
45
|
+
* it, and every read already guards with `?.`. Deliberate, load-bearing local divergence.
|
|
46
|
+
*/
|
|
93
47
|
children?: string[];
|
|
94
48
|
}
|
|
95
49
|
/** How long a remote step may sit `pending` (dispatched, no result) before it's presumed a LOST
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"durable-client.d.ts","sourceRoot":"","sources":["../../src/client/durable-client.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"durable-client.d.ts","sourceRoot":"","sources":["../../src/client/durable-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"durable-client.js","sourceRoot":"","sources":["../../src/client/durable-client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"durable-client.js","sourceRoot":"","sources":["../../src/client/durable-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/index.ts","../../src/server/dashboard.service.ts","../../src/server/tokens.ts","../../src/server/durable-api.controller.ts","../../src/server/attr-filter.ts","../../src/server/durable-dashboard.module.ts","../../src/server/durable-ui.controller.ts"],"sourcesContent":["export * from './dashboard.service.js';\nexport * from './durable-api.controller.js';\nexport * from './durable-dashboard.module.js';\nexport * from './durable-ui.controller.js';\n","import {\n type DurableTopology,\n type EngineEvent,\n type GroupHealth,\n type MetricsCollector,\n type RunGateway,\n type RunListItem,\n type RunQuery,\n type RunResult,\n STATE_STORE_CANONICAL,\n type StateStore,\n type StepCheckpoint,\n type UpdateResult,\n WorkflowEngine,\n type WorkflowRun,\n collectMetrics,\n} from '@dudousxd/nestjs-durable-core';\nimport { Inject, Injectable, Optional } from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { RUN_GATEWAY } from './tokens.js';\n\nexport interface RunDetail {\n run: WorkflowRun;\n /** Steps in execution order — the end-to-end timeline (local + remote). */\n timeline: StepCheckpoint[];\n /** Ids of runs this run spawned (ctx.child / ctx.startChild) — the parent→children tree. */\n children: string[];\n}\n\n/**\n * Read-model and actions backing the control-plane UI. Run-facing ops (list/detail/retry/cancel/\n * continue/retryWithInput/stream/bulk/workerHealth) route through the injected {@link RUN_GATEWAY}\n * port, which the lib binds on both topologies (a store-backed impl on the control plane, a proxy\n * over the transport on a store-less tenant) — so those ops work regardless of whether `store`/\n * `engine` are present (`workerHealth` is scoped to the tenant's own groups over the proxy).\n * Operator-only ops (metrics/deliverWebhook/getEvent/update) still need direct `store`/`engine`\n * access; they stay `@Optional()` and throw a clear error when absent.\n */\n@Injectable()\nexport class DashboardService {\n /** Prometheus counters accumulated from engine events since boot (per process); undefined on a\n * store-less tenant (no engine to accumulate from). */\n private readonly metricsCollector: MetricsCollector | undefined;\n\n constructor(\n @Inject(RUN_GATEWAY) private readonly gateway: RunGateway,\n @Optional() @Inject(STATE_STORE_CANONICAL) private readonly store?: StateStore,\n @Optional() private readonly engine?: WorkflowEngine,\n ) {\n // Only the control plane (which has the canonical store) has a FULL engine to accumulate from;\n // on a tenant `this.engine` is a store-less start client with no `subscribe`, so guard on store.\n this.metricsCollector = this.store && this.engine ? collectMetrics(this.engine) : undefined;\n }\n\n /**\n * The operator-only ops need the FULL engine + canonical store, which only the control plane has.\n * On a tenant the `WorkflowEngine` token is a store-less start client that implements none of these\n * methods, so presence of the canonical store is the reliable discriminator — gate on it (never\n * on engine presence, which is truthy on a tenant too).\n */\n private controlPlane(): { store: StateStore; engine: WorkflowEngine } {\n if (!this.store || !this.engine) {\n throw new Error(\n 'This durable dashboard operation requires the control plane (not available on a tenant deployment).',\n );\n }\n return { store: this.store, engine: this.engine };\n }\n\n /**\n * Prometheus-text metrics for a `/metrics` scrape: the event-counters (runs/steps by outcome,\n * per-workflow counts) plus live **backlog gauges** queried at scrape time — `durable_pending_runs`\n * (the dispatch backlog: the key health signal of the dispatch model) and `durable_dead_runs` (DLQ\n * size). Capped per status so the scrape can't load an unbounded result set. Control-plane-only.\n */\n async metrics(): Promise<string> {\n const { store, engine } = this.controlPlane();\n const cap = 10_000;\n const [pending, running, dead] = await Promise.all([\n store.listRuns({ status: 'pending', limit: cap }),\n store.listRuns({ status: 'running', limit: cap }),\n store.listRuns({ status: 'dead', limit: cap }),\n ]);\n const gauges = [\n '# TYPE durable_pending_runs gauge',\n `durable_pending_runs ${pending.length}`,\n '# TYPE durable_running_runs gauge',\n `durable_running_runs ${running.length}`,\n '# TYPE durable_dead_runs gauge',\n `durable_dead_runs ${dead.length}`,\n ];\n // Per-group worker health: backlog vs. live workers. `depth>0 && live==0` is the alert (work\n // piling up with no consumer) — expressible as a Prometheus rule on these two series.\n const health = await engine.workerHealth();\n if (health.length > 0) {\n gauges.push('# TYPE durable_group_queue_depth gauge');\n for (const h of health) {\n gauges.push(`durable_group_queue_depth{group=\"${h.group}\"} ${h.depth}`);\n }\n gauges.push('# TYPE durable_group_live_workers gauge');\n for (const h of health) {\n gauges.push(`durable_group_live_workers{group=\"${h.group}\"} ${h.liveWorkers.length}`);\n }\n }\n return `${this.metricsCollector?.prometheus() ?? ''}${gauges.join('\\n')}\\n`;\n }\n\n listRuns(query: RunQuery): Promise<RunListItem[]> {\n return this.gateway.listRuns(query);\n }\n\n /** This deployment's durable role (control plane vs tenant) + tenant name — for the dashboard header\n * badge. Routes through the gateway, which knows its own topology (store-backed = operator, proxy =\n * tenant). Works on both topologies (no store/engine needed). */\n topology(): DurableTopology {\n return this.gateway.topology();\n }\n\n /** Per-group worker health (queue backlog + live worker heartbeats) for the Workers panel. The\n * alert state a row turns red on is `depth > 0 && liveWorkers.length === 0`. Routes through the\n * gateway so it works on BOTH topologies: the control plane returns every group; a tenant proxies\n * to the operator, which scopes the result to the tenant's own `@<tenant>` groups. */\n async workerHealth(): Promise<GroupHealth[]> {\n return this.gateway.workerHealth();\n }\n\n getRunDetail(runId: string): Promise<RunDetail | null> {\n return this.gateway.getRunDetail(runId);\n }\n\n /** Fix-and-replay a dead/failed run with a corrected input — a fresh linked run. Returns its id. */\n retryWithInput(runId: string, input: unknown): Promise<{ runId: string } | null> {\n return this.gateway.retryWithInput(runId, input);\n }\n\n /** Re-run a failed/incomplete run; completed steps replay from their checkpoints. */\n retry(runId: string): Promise<RunResult | null> {\n return this.gateway.retry(runId);\n }\n\n cancel(runId: string, opts?: { compensate?: boolean }): Promise<RunResult | null> {\n return this.gateway.cancel(runId, opts);\n }\n\n /**\n * Apply `retry` or `cancel` to every run matching a filter (status / tag / workflow) — e.g. \"retry\n * every `dead` run tagged `type:mel`\". Capped at 500 runs per call; runs that can't be acted on\n * (already terminal) are skipped. Returns how many matched and how many the action applied to.\n */\n async bulk(\n action: 'retry' | 'cancel',\n filter: Pick<RunQuery, 'status' | 'tag' | 'workflow' | 'attributes'>,\n opts?: { compensate?: boolean },\n ): Promise<{ matched: number; applied: number }> {\n const runs = await this.gateway.listRuns({ ...filter, limit: 500 });\n let applied = 0;\n for (const r of runs) {\n try {\n if (action === 'retry') await this.gateway.retry(r.id);\n else await this.gateway.cancel(r.id, opts);\n applied += 1;\n } catch {\n // Skip a run that can't take the action (e.g. already terminal) — keep going.\n }\n }\n return { matched: runs.length, applied };\n }\n\n /** Resume a run paused at a `ctx.breakpoint` (the \"continue\" button). */\n continue(runId: string): Promise<RunResult | null> {\n return this.gateway.continue(runId);\n }\n\n /** Re-dispatch a run's stuck `pending` remote steps — recovery for a lost step dispatch. */\n redispatch(runId: string) {\n return this.gateway.redispatchPending(runId);\n }\n\n /**\n * Deliver a `ctx.webhook()` callback: turn an inbound POST (token + body) into the signal the\n * waiting run is parked on. Returns the run result, or `null` if no run waits on that token (a\n * stale/duplicate callback) — a safe no-op the controller maps to 404. Control-plane-only.\n */\n async deliverWebhook(token: string, body: unknown): Promise<RunResult | null> {\n return this.controlPlane().engine.signal(token, body);\n }\n\n /** Side-effect-free read of a value a run published via `ctx.setEvent` (a live query). Control-plane-only. */\n async getEvent(runId: string, key: string): Promise<unknown> {\n return this.controlPlane().engine.getEvent(runId, key);\n }\n\n /** Deliver a validated `ctx.onUpdate` to a run; the validator may reject it (see UpdateResult). Control-plane-only. */\n async update(runId: string, name: string, arg: unknown): Promise<UpdateResult> {\n return this.controlPlane().engine.update(runId, name, arg);\n }\n\n /**\n * Live stream of a run's lifecycle events for SSE. Backed by `gateway.subscribe`, which — on the\n * control plane — receives events from EVERY instance (so a dashboard-only pod tails a run\n * executing on a worker pod), and on a tenant round-trips over the transport. Either way the\n * gateway already filters to `runId`; the dashboard just wires the events through.\n */\n streamRun(runId: string): Observable<{ data: EngineEvent }> {\n return new Observable<{ data: EngineEvent }>((subscriber) => {\n const off = this.gateway.subscribe(runId, (event) => subscriber.next({ data: event }));\n return () => off();\n });\n }\n}\n","/**\n * Injection token for the bound `RunGateway`, owned by `@dudousxd/nestjs-durable` (the nestjs\n * package). The dashboard package does NOT depend on `@dudousxd/nestjs-durable` — its only\n * relevant peer dep is `@dudousxd/nestjs-durable-core` — so we do NOT import the token from\n * there. Instead we share its well-known token by value, the same pattern\n * `@dudousxd/nestjs-durable`'s own `tokens.ts` uses for `CONTEXT_ACCESSOR` (a cross-lib token\n * owned by `@dudousxd/nestjs-context`).\n *\n * `Symbol.for(key)` uses the global symbol registry, so this resolves to the SAME symbol\n * instance as `packages/nestjs/src/tokens.ts`'s `RUN_GATEWAY` without any import. The key MUST\n * stay byte-identical with that export.\n */\nexport const RUN_GATEWAY = Symbol.for('nestjs-durable:run-gateway');\n","import type { RunStatus } from '@dudousxd/nestjs-durable-core';\nimport {\n Body,\n Controller,\n Get,\n Header,\n NotFoundException,\n Param,\n Post,\n Query,\n Sse,\n} from '@nestjs/common';\nimport { parseAttrFilters } from './attr-filter.js';\nimport { DashboardService } from './dashboard.service.js';\n\n/** JSON API consumed by the control-plane SPA. Mounted at `apiBasePath` (set by RouterModule). */\n@Controller()\nexport class DurableApiController {\n constructor(private readonly dashboard: DashboardService) {}\n\n @Get('runs')\n runs(\n @Query('status') status?: RunStatus,\n @Query('workflow') workflow?: string,\n @Query('tag') tag?: string,\n @Query('attr') attr?: string | string[],\n ) {\n return this.dashboard.listRuns({ status, workflow, tag, attributes: parseAttrFilters(attr) });\n }\n\n /** Prometheus-text metrics (runs/steps by outcome, per-workflow counts) for a scrape. */\n @Get('metrics')\n @Header('Content-Type', 'text/plain; version=0.0.4')\n metrics() {\n return this.dashboard.metrics();\n }\n\n /** Per-group worker health (queue backlog + live worker heartbeats) for the Workers panel. */\n @Get('workers')\n workers() {\n return this.dashboard.workerHealth();\n }\n\n /** This deployment's durable role (control plane vs tenant) + tenant name — for the header badge. */\n @Get('topology')\n topology() {\n return this.dashboard.topology();\n }\n\n @Get('runs/:id')\n async run(@Param('id') id: string) {\n const detail = await this.dashboard.getRunDetail(id);\n if (!detail) throw new NotFoundException(`run ${id} not found`);\n return detail;\n }\n\n /** Server-Sent Events stream of a run's live lifecycle events — the dashboard tails it instead\n * of polling. Cross-pod when the transport has a control plane (see DashboardService.streamRun). */\n @Sse('runs/:id/stream')\n stream(@Param('id') id: string) {\n return this.dashboard.streamRun(id);\n }\n\n @Post('runs/:id/retry')\n retry(@Param('id') id: string) {\n return this.dashboard.retry(id);\n }\n\n /** Fix-and-replay: re-run a dead/failed run with a corrected input (a fresh linked run). */\n @Post('runs/:id/retry-with-input')\n retryWithInput(@Param('id') id: string, @Body() body: { input: unknown }) {\n return this.dashboard.retryWithInput(id, body?.input);\n }\n\n /** Bulk retry/cancel every run matching a filter (status / tag / workflow). */\n @Post('bulk/:action')\n bulk(\n @Param('action') action: 'retry' | 'cancel',\n @Query('status') status?: RunStatus,\n @Query('tag') tag?: string,\n @Query('workflow') workflow?: string,\n @Query('attr') attr?: string | string[],\n @Query('compensate') compensate?: string,\n ) {\n return this.dashboard.bulk(\n action === 'cancel' ? 'cancel' : 'retry',\n { status, tag, workflow, attributes: parseAttrFilters(attr) },\n { compensate: compensate === 'true' },\n );\n }\n\n @Post('runs/:id/cancel')\n async cancel(@Param('id') id: string, @Query('compensate') compensate?: string) {\n const result = await this.dashboard.cancel(id, { compensate: compensate === 'true' });\n if (!result) throw new NotFoundException(`run ${id} not found`);\n return result;\n }\n\n @Post('runs/:id/continue')\n async continue(@Param('id') id: string) {\n const result = await this.dashboard.continue(id);\n if (!result) throw new NotFoundException(`run ${id} is not paused at a breakpoint`);\n return result;\n }\n\n /** Re-dispatch a run's stuck `pending` remote steps — recovery for a lost step dispatch. */\n @Post('runs/:id/redispatch')\n async redispatch(@Param('id') id: string) {\n const result = await this.dashboard.redispatch(id);\n if (!result) throw new NotFoundException(`run ${id} not found`);\n return result;\n }\n\n /**\n * Public callback endpoint for `ctx.webhook()`: a third party POSTs here (the url handed to it),\n * and the body resumes the waiting run. NOTE: this is reachable by external systems — protect it\n * by treating the token as a secret (it embeds runId:seq) and/or fronting it with signature\n * verification in your own middleware.\n */\n @Post('webhooks/:token')\n async webhook(@Param('token') token: string, @Body() body: unknown) {\n const result = await this.dashboard.deliverWebhook(token, body);\n if (!result) throw new NotFoundException(`no run is waiting on webhook ${token}`);\n return result;\n }\n\n /** Live query: the latest value a run published for `key` via `ctx.setEvent` (no side effect). */\n @Get('runs/:id/events/:key')\n event(@Param('id') id: string, @Param('key') key: string) {\n return this.dashboard.getEvent(id, key);\n }\n\n /** Deliver a validated `ctx.onUpdate` to a run; the body is the update argument. */\n @Post('runs/:id/updates/:name')\n update(@Param('id') id: string, @Param('name') name: string, @Body() body: unknown) {\n return this.dashboard.update(id, name, body);\n }\n}\n","import type { AttributeFilter, AttributeOp } from '@dudousxd/nestjs-durable-core';\n\nconst ATTR_OPS = new Set<AttributeOp>(['eq', 'ne', 'gt', 'gte', 'lt', 'lte']);\n\n/** Coerce a query-string value: `true`/`false` → boolean, numeric → number, else the raw string. */\nfunction coerce(v: string): string | number | boolean {\n if (v === 'true') return true;\n if (v === 'false') return false;\n if (v !== '' && !Number.isNaN(Number(v))) return Number(v);\n return v;\n}\n\n/**\n * Parse `attr=key:op:value` query params (repeatable, ANDed) into {@link AttributeFilter}s — e.g.\n * `?attr=amount:gte:200&attr=tier:eq:pro`. `op` must be a known operator; a colon in the value is\n * preserved (only the first two colons delimit). Malformed entries are skipped.\n */\nexport function parseAttrFilters(attr?: string | string[]): AttributeFilter[] | undefined {\n if (!attr) return undefined;\n const raw = Array.isArray(attr) ? attr : [attr];\n const filters: AttributeFilter[] = [];\n for (const entry of raw) {\n const [key, op, ...rest] = entry.split(':');\n if (!key || !ATTR_OPS.has(op as AttributeOp) || rest.length === 0) continue;\n filters.push({ key, op: op as AttributeOp, value: coerce(rest.join(':')) });\n }\n return filters.length ? filters : undefined;\n}\n","import { type DynamicModule, Module } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { DashboardService } from './dashboard.service.js';\nimport { DurableApiController } from './durable-api.controller.js';\nimport {\n DASHBOARD_API_PATH,\n DASHBOARD_BASE_PATH,\n DurableUiController,\n} from './durable-ui.controller.js';\n\nexport interface DurableDashboardOptions {\n /**\n * Where the SPA (UI) is served. Default `/durable`. This is a page route — keep it out of an\n * `/api` prefix so it reads as a UI, not an endpoint.\n */\n basePath?: string;\n /**\n * Where the JSON API is mounted (what the SPA fetches). Default `<basePath>/api`. Set it under\n * your app's `/api` prefix — e.g. `/api/durable` — so the API inherits the app's auth/proxy rules\n * while the UI stays at `basePath`.\n */\n apiBasePath?: string;\n}\n\n/** Leading slash, no trailing slash. */\nfunction normalize(path: string): string {\n return `/${path.replace(/^\\/+|\\/+$/g, '')}`;\n}\n\n/** Holds the JSON API controller + its read service, mounted on its own path by `forRoot`. */\n@Module({\n controllers: [DurableApiController],\n providers: [DashboardService],\n exports: [DashboardService],\n})\nexport class DurableApiModule {}\n\n/**\n * Mounts the control plane: the bundled React SPA at `basePath` and its JSON API at `apiBasePath`\n * (default `<basePath>/api`). Import via `DurableDashboardModule.forRoot(...)` alongside\n * `DurableModule` (global), so it resolves the engine and store. Front the routes with a guard.\n */\n@Module({})\nexport class DurableDashboardModule {\n static forRoot(options: DurableDashboardOptions = {}): DynamicModule {\n const basePath = normalize(options.basePath ?? '/durable');\n const apiBasePath = normalize(options.apiBasePath ?? `${basePath}/api`);\n return {\n module: DurableDashboardModule,\n imports: [\n DurableApiModule,\n RouterModule.register([\n { path: basePath, module: DurableDashboardModule }, // the UI controller below\n { path: apiBasePath, module: DurableApiModule },\n ]),\n ],\n controllers: [DurableUiController],\n providers: [\n { provide: DASHBOARD_BASE_PATH, useValue: basePath },\n { provide: DASHBOARD_API_PATH, useValue: apiBasePath },\n ],\n // Re-export the API module so its DashboardService reaches importers (e.g. flip's own controllers).\n exports: [DurableApiModule],\n };\n }\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { basename, extname, join, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n Controller,\n Get,\n Header,\n Inject,\n NotFoundException,\n Param,\n StreamableFile,\n} from '@nestjs/common';\n\n/** DI token carrying the UI mount base (e.g. `/durable`). */\nexport const DASHBOARD_BASE_PATH = Symbol('DASHBOARD_BASE_PATH');\n/** DI token carrying the JSON API base the SPA fetches from (e.g. `/api/durable`). */\nexport const DASHBOARD_API_PATH = Symbol('DASHBOARD_API_PATH');\n\n/** The base the SPA bundle was built with (Vite `base`); rewritten to the configured base at serve time. */\nconst BUILD_BASE = '/durable';\n\n/** dist/server/durable-ui.controller.js -> ../spa (the Vite build output). */\nfunction spaDir(): string {\n return fileURLToPath(new URL('../spa', import.meta.url));\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.map': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.json': 'application/json; charset=utf-8',\n '.woff2': 'font/woff2',\n '.ico': 'image/x-icon',\n};\n\n/**\n * Serves the bundled control-plane SPA at the configured base (+ hashed assets at `<base>/assets`).\n * The path comes from `RouterModule` (set by `DurableDashboardModule.forRoot({ basePath })`), so the\n * controller routes are relative.\n */\n@Controller()\nexport class DurableUiController {\n private readonly dir = spaDir();\n\n constructor(\n @Inject(DASHBOARD_BASE_PATH) private readonly basePath: string,\n @Inject(DASHBOARD_API_PATH) private readonly apiBasePath: string,\n ) {}\n\n // index.html references hash-named bundles, so it MUST NOT be cached (stale bundle = the\n // classic \"stuck loading after a deploy\"). The hashed assets below are immutable.\n @Get()\n @Header('Content-Type', 'text/html; charset=utf-8')\n @Header('Cache-Control', 'no-store, must-revalidate')\n index(): string {\n const indexPath = join(this.dir, 'index.html');\n if (!existsSync(indexPath)) {\n throw new NotFoundException('Dashboard is not built. Run the package build.');\n }\n // The bundle was built with Vite base `/durable/`; rewrite asset URLs to the configured base so\n // the SPA loads from `<base>/assets` wherever it's mounted, and tell the client its API base.\n const html = readFileSync(indexPath, 'utf8').replaceAll(\n `=\"${BUILD_BASE}/`,\n `=\"${this.basePath}/`,\n );\n // __DURABLE_BASE__ = where assets load; __DURABLE_API__ = where the SPA fetches the JSON API.\n const inject = `<script>window.__DURABLE_BASE__='${this.basePath}';window.__DURABLE_API__='${this.apiBasePath}';</script>`;\n return html.includes('</head>') ? html.replace('</head>', `${inject}</head>`) : inject + html;\n }\n\n @Get('assets/:file')\n @Header('Cache-Control', 'public, max-age=31536000, immutable')\n asset(@Param('file') file: string): StreamableFile {\n const safe = basename(file);\n if (safe !== file) throw new NotFoundException();\n const root = resolve(this.dir, 'assets');\n const assetPath = resolve(root, safe);\n if (!assetPath.startsWith(root + sep) || !existsSync(assetPath)) {\n throw new NotFoundException();\n }\n const type = CONTENT_TYPES[extname(safe)] ?? 'application/octet-stream';\n return new StreamableFile(readFileSync(assetPath), { type });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;;;ACAA,iCAgBO;AACP,oBAA6C;AAC7C,kBAA2B;;;ACNpB,IAAMA,cAAcC,OAAOC,IAAI,4BAAA;;;;;;;;;;;;;;;;;;;;AD2B/B,IAAMC,mBAAN,MAAMA;SAAAA;;;;;;;;EAGMC;EAEjB,YACwCC,SACsBC,OAC/BC,QAC7B;SAHsCF,UAAAA;SACsBC,QAAAA;SAC/BC,SAAAA;AAI7B,SAAKH,mBAAmB,KAAKE,SAAS,KAAKC,aAASC,2CAAe,KAAKD,MAAM,IAAIE;EACpF;;;;;;;EAQQC,eAA8D;AACpE,QAAI,CAAC,KAAKJ,SAAS,CAAC,KAAKC,QAAQ;AAC/B,YAAM,IAAII,MACR,qGAAA;IAEJ;AACA,WAAO;MAAEL,OAAO,KAAKA;MAAOC,QAAQ,KAAKA;IAAO;EAClD;;;;;;;EAQA,MAAMK,UAA2B;AAC/B,UAAM,EAAEN,OAAOC,OAAM,IAAK,KAAKG,aAAY;AAC3C,UAAMG,MAAM;AACZ,UAAM,CAACC,SAASC,SAASC,IAAAA,IAAQ,MAAMC,QAAQC,IAAI;MACjDZ,MAAMa,SAAS;QAAEC,QAAQ;QAAWC,OAAOR;MAAI,CAAA;MAC/CP,MAAMa,SAAS;QAAEC,QAAQ;QAAWC,OAAOR;MAAI,CAAA;MAC/CP,MAAMa,SAAS;QAAEC,QAAQ;QAAQC,OAAOR;MAAI,CAAA;KAC7C;AACD,UAAMS,SAAS;MACb;MACA,wBAAwBR,QAAQS,MAAM;MACtC;MACA,wBAAwBR,QAAQQ,MAAM;MACtC;MACA,qBAAqBP,KAAKO,MAAM;;AAIlC,UAAMC,SAAS,MAAMjB,OAAOkB,aAAY;AACxC,QAAID,OAAOD,SAAS,GAAG;AACrBD,aAAOI,KAAK,wCAAA;AACZ,iBAAWC,KAAKH,QAAQ;AACtBF,eAAOI,KAAK,oCAAoCC,EAAEC,KAAK,MAAMD,EAAEE,KAAK,EAAE;MACxE;AACAP,aAAOI,KAAK,yCAAA;AACZ,iBAAWC,KAAKH,QAAQ;AACtBF,eAAOI,KAAK,qCAAqCC,EAAEC,KAAK,MAAMD,EAAEG,YAAYP,MAAM,EAAE;MACtF;IACF;AACA,WAAO,GAAG,KAAKnB,kBAAkB2B,WAAAA,KAAgB,EAAA,GAAKT,OAAOU,KAAK,IAAA,CAAA;;EACpE;EAEAb,SAASc,OAAyC;AAChD,WAAO,KAAK5B,QAAQc,SAASc,KAAAA;EAC/B;;;;EAKAC,WAA4B;AAC1B,WAAO,KAAK7B,QAAQ6B,SAAQ;EAC9B;;;;;EAMA,MAAMT,eAAuC;AAC3C,WAAO,KAAKpB,QAAQoB,aAAY;EAClC;EAEAU,aAAaC,OAA0C;AACrD,WAAO,KAAK/B,QAAQ8B,aAAaC,KAAAA;EACnC;;EAGAC,eAAeD,OAAeE,OAAmD;AAC/E,WAAO,KAAKjC,QAAQgC,eAAeD,OAAOE,KAAAA;EAC5C;;EAGAC,MAAMH,OAA0C;AAC9C,WAAO,KAAK/B,QAAQkC,MAAMH,KAAAA;EAC5B;EAEAI,OAAOJ,OAAeK,MAA4D;AAChF,WAAO,KAAKpC,QAAQmC,OAAOJ,OAAOK,IAAAA;EACpC;;;;;;EAOA,MAAMC,KACJC,QACAC,QACAH,MAC+C;AAC/C,UAAMI,OAAO,MAAM,KAAKxC,QAAQc,SAAS;MAAE,GAAGyB;MAAQvB,OAAO;IAAI,CAAA;AACjE,QAAIyB,UAAU;AACd,eAAWC,KAAKF,MAAM;AACpB,UAAI;AACF,YAAIF,WAAW,QAAS,OAAM,KAAKtC,QAAQkC,MAAMQ,EAAEC,EAAE;YAChD,OAAM,KAAK3C,QAAQmC,OAAOO,EAAEC,IAAIP,IAAAA;AACrCK,mBAAW;MACb,QAAQ;MAER;IACF;AACA,WAAO;MAAEG,SAASJ,KAAKtB;MAAQuB;IAAQ;EACzC;;EAGAI,SAASd,OAA0C;AACjD,WAAO,KAAK/B,QAAQ6C,SAASd,KAAAA;EAC/B;;EAGAe,WAAWf,OAAe;AACxB,WAAO,KAAK/B,QAAQ+C,kBAAkBhB,KAAAA;EACxC;;;;;;EAOA,MAAMiB,eAAeC,OAAeC,MAA0C;AAC5E,WAAO,KAAK7C,aAAY,EAAGH,OAAOiD,OAAOF,OAAOC,IAAAA;EAClD;;EAGA,MAAME,SAASrB,OAAesB,KAA+B;AAC3D,WAAO,KAAKhD,aAAY,EAAGH,OAAOkD,SAASrB,OAAOsB,GAAAA;EACpD;;EAGA,MAAMC,OAAOvB,OAAewB,MAAcC,KAAqC;AAC7E,WAAO,KAAKnD,aAAY,EAAGH,OAAOoD,OAAOvB,OAAOwB,MAAMC,GAAAA;EACxD;;;;;;;EAQAC,UAAU1B,OAAkD;AAC1D,WAAO,IAAI2B,uBAAkC,CAACC,eAAAA;AAC5C,YAAMC,MAAM,KAAK5D,QAAQ6D,UAAU9B,OAAO,CAAC+B,UAAUH,WAAWI,KAAK;QAAEC,MAAMF;MAAM,CAAA,CAAA;AACnF,aAAO,MAAMF,IAAAA;IACf,CAAA;EACF;AACF;;;;;;;;;;;;;;;;AEhNA,IAAAK,iBAUO;;;ACTP,IAAMC,WAAW,oBAAIC,IAAiB;EAAC;EAAM;EAAM;EAAM;EAAO;EAAM;CAAM;AAG5E,SAASC,OAAOC,GAAS;AACvB,MAAIA,MAAM,OAAQ,QAAO;AACzB,MAAIA,MAAM,QAAS,QAAO;AAC1B,MAAIA,MAAM,MAAM,CAACC,OAAOC,MAAMD,OAAOD,CAAAA,CAAAA,EAAK,QAAOC,OAAOD,CAAAA;AACxD,SAAOA;AACT;AALSD;AAYF,SAASI,iBAAiBC,MAAwB;AACvD,MAAI,CAACA,KAAM,QAAOC;AAClB,QAAMC,MAAMC,MAAMC,QAAQJ,IAAAA,IAAQA,OAAO;IAACA;;AAC1C,QAAMK,UAA6B,CAAA;AACnC,aAAWC,SAASJ,KAAK;AACvB,UAAM,CAACK,KAAKC,IAAI,GAAGC,IAAAA,IAAQH,MAAMI,MAAM,GAAA;AACvC,QAAI,CAACH,OAAO,CAACd,SAASkB,IAAIH,EAAAA,KAAsBC,KAAKG,WAAW,EAAG;AACnEP,YAAQQ,KAAK;MAAEN;MAAKC;MAAuBM,OAAOnB,OAAOc,KAAKM,KAAK,GAAA,CAAA;IAAM,CAAA;EAC3E;AACA,SAAOV,QAAQO,SAASP,UAAUJ;AACpC;AAVgBF;;;;;;;;;;;;;;;;;;;;ADAT,IAAMiB,uBAAN,MAAMA;SAAAA;;;;EACX,YAA6BC,WAA6B;SAA7BA,YAAAA;EAA8B;EAG3DC,KACmBC,QACEC,UACLC,KACCC,MACf;AACA,WAAO,KAAKL,UAAUM,SAAS;MAAEJ;MAAQC;MAAUC;MAAKG,YAAYC,iBAAiBH,IAAAA;IAAM,CAAA;EAC7F;;EAKAI,UAAU;AACR,WAAO,KAAKT,UAAUS,QAAO;EAC/B;;EAIAC,UAAU;AACR,WAAO,KAAKV,UAAUW,aAAY;EACpC;;EAIAC,WAAW;AACT,WAAO,KAAKZ,UAAUY,SAAQ;EAChC;EAEA,MACMC,IAAiBC,IAAY;AACjC,UAAMC,SAAS,MAAM,KAAKf,UAAUgB,aAAaF,EAAAA;AACjD,QAAI,CAACC,OAAQ,OAAM,IAAIE,iCAAkB,OAAOH,EAAAA,YAAc;AAC9D,WAAOC;EACT;;;EAKAG,OAAoBJ,IAAY;AAC9B,WAAO,KAAKd,UAAUmB,UAAUL,EAAAA;EAClC;EAGAM,MAAmBN,IAAY;AAC7B,WAAO,KAAKd,UAAUoB,MAAMN,EAAAA;EAC9B;;EAIAO,eAA4BP,IAAoBQ,MAA0B;AACxE,WAAO,KAAKtB,UAAUqB,eAAeP,IAAIQ,MAAMC,KAAAA;EACjD;;EAIAC,KACmBC,QACAvB,QACHE,KACKD,UACJE,MACMqB,YACrB;AACA,WAAO,KAAK1B,UAAUwB,KACpBC,WAAW,WAAW,WAAW,SACjC;MAAEvB;MAAQE;MAAKD;MAAUI,YAAYC,iBAAiBH,IAAAA;IAAM,GAC5D;MAAEqB,YAAYA,eAAe;IAAO,CAAA;EAExC;EAEA,MACMC,OAAoBb,IAAiCY,YAAqB;AAC9E,UAAME,SAAS,MAAM,KAAK5B,UAAU2B,OAAOb,IAAI;MAAEY,YAAYA,eAAe;IAAO,CAAA;AACnF,QAAI,CAACE,OAAQ,OAAM,IAAIX,iCAAkB,OAAOH,EAAAA,YAAc;AAC9D,WAAOc;EACT;EAEA,MACMC,SAAsBf,IAAY;AACtC,UAAMc,SAAS,MAAM,KAAK5B,UAAU6B,SAASf,EAAAA;AAC7C,QAAI,CAACc,OAAQ,OAAM,IAAIX,iCAAkB,OAAOH,EAAAA,gCAAkC;AAClF,WAAOc;EACT;;EAGA,MACME,WAAwBhB,IAAY;AACxC,UAAMc,SAAS,MAAM,KAAK5B,UAAU8B,WAAWhB,EAAAA;AAC/C,QAAI,CAACc,OAAQ,OAAM,IAAIX,iCAAkB,OAAOH,EAAAA,YAAc;AAC9D,WAAOc;EACT;;;;;;;EAQA,MACMG,QAAwBC,OAAuBV,MAAe;AAClE,UAAMM,SAAS,MAAM,KAAK5B,UAAUiC,eAAeD,OAAOV,IAAAA;AAC1D,QAAI,CAACM,OAAQ,OAAM,IAAIX,iCAAkB,gCAAgCe,KAAAA,EAAO;AAChF,WAAOJ;EACT;;EAIAM,MAAmBpB,IAA0BqB,KAAa;AACxD,WAAO,KAAKnC,UAAUoC,SAAStB,IAAIqB,GAAAA;EACrC;;EAIAE,OAAoBvB,IAA2BwB,MAAsBhB,MAAe;AAClF,WAAO,KAAKtB,UAAUqC,OAAOvB,IAAIwB,MAAMhB,IAAAA;EACzC;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AEzIA,IAAAiB,iBAA2C;AAC3C,kBAA6B;;;ACD7B,qBAAyC;AACzC,uBAAsD;AACtD,sBAA8B;AAC9B,IAAAC,iBAQO;;;;;;;;;;;;;;;;;;AAGA,IAAMC,sBAAsBC,OAAO,qBAAA;AAEnC,IAAMC,qBAAqBD,OAAO,oBAAA;AAGzC,IAAME,aAAa;AAGnB,SAASC,SAAAA;AACP,aAAOC,+BAAc,IAAIC,IAAI,UAAU,eAAe,CAAA;AACxD;AAFSF;AAIT,IAAMG,gBAAwC;EAC5C,OAAO;EACP,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,UAAU;EACV,QAAQ;AACV;AAQO,IAAMC,sBAAN,MAAMA;SAAAA;;;;;EACMC,MAAML,OAAAA;EAEvB,YACgDM,UACDC,aAC7C;SAF8CD,WAAAA;SACDC,cAAAA;EAC5C;;;EAOHC,QAAgB;AACd,UAAMC,gBAAYC,uBAAK,KAAKL,KAAK,YAAA;AACjC,QAAI,KAACM,2BAAWF,SAAAA,GAAY;AAC1B,YAAM,IAAIG,iCAAkB,gDAAA;IAC9B;AAGA,UAAMC,WAAOC,6BAAaL,WAAW,MAAA,EAAQM,WAC3C,KAAKhB,UAAAA,KACL,KAAK,KAAKO,QAAQ,GAAG;AAGvB,UAAMU,SAAS,oCAAoC,KAAKV,QAAQ,6BAA6B,KAAKC,WAAW;AAC7G,WAAOM,KAAKI,SAAS,SAAA,IAAaJ,KAAKK,QAAQ,WAAW,GAAGF,MAAAA,SAAe,IAAIA,SAASH;EAC3F;EAIAM,MAAqBC,MAA8B;AACjD,UAAMC,WAAOC,2BAASF,IAAAA;AACtB,QAAIC,SAASD,KAAM,OAAM,IAAIR,iCAAAA;AAC7B,UAAMW,WAAOC,0BAAQ,KAAKnB,KAAK,QAAA;AAC/B,UAAMoB,gBAAYD,0BAAQD,MAAMF,IAAAA;AAChC,QAAI,CAACI,UAAUC,WAAWH,OAAOI,oBAAAA,KAAQ,KAAChB,2BAAWc,SAAAA,GAAY;AAC/D,YAAM,IAAIb,iCAAAA;IACZ;AACA,UAAMgB,OAAOzB,kBAAc0B,0BAAQR,IAAAA,CAAAA,KAAU;AAC7C,WAAO,IAAIS,kCAAehB,6BAAaW,SAAAA,GAAY;MAAEG;IAAK,CAAA;EAC5D;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AD3DA,SAASG,UAAUC,MAAY;AAC7B,SAAO,IAAIA,KAAKC,QAAQ,cAAc,EAAA,CAAA;AACxC;AAFSF;AAUF,IAAMG,mBAAN,MAAMA;SAAAA;;;AAAkB;;;IAJ7BC,aAAa;MAACC;;IACdC,WAAW;MAACC;;IACZC,SAAS;MAACD;;;;AAUL,IAAME,yBAAN,MAAMA,wBAAAA;SAAAA;;;EACX,OAAOC,QAAQC,UAAmC,CAAC,GAAkB;AACnE,UAAMC,WAAWZ,UAAUW,QAAQC,YAAY,UAAA;AAC/C,UAAMC,cAAcb,UAAUW,QAAQE,eAAe,GAAGD,QAAAA,MAAc;AACtE,WAAO;MACLE,QAAQL;MACRM,SAAS;QACPZ;QACAa,yBAAaC,SAAS;UACpB;YAAEhB,MAAMW;YAAUE,QAAQL;UAAuB;UACjD;YAAER,MAAMY;YAAaC,QAAQX;UAAiB;SAC/C;;MAEHC,aAAa;QAACc;;MACdZ,WAAW;QACT;UAAEa,SAASC;UAAqBC,UAAUT;QAAS;QACnD;UAAEO,SAASG;UAAoBD,UAAUR;QAAY;;;MAGvDL,SAAS;QAACL;;IACZ;EACF;AACF;;;;","names":["RUN_GATEWAY","Symbol","for","DashboardService","metricsCollector","gateway","store","engine","collectMetrics","undefined","controlPlane","Error","metrics","cap","pending","running","dead","Promise","all","listRuns","status","limit","gauges","length","health","workerHealth","push","h","group","depth","liveWorkers","prometheus","join","query","topology","getRunDetail","runId","retryWithInput","input","retry","cancel","opts","bulk","action","filter","runs","applied","r","id","matched","continue","redispatch","redispatchPending","deliverWebhook","token","body","signal","getEvent","key","update","name","arg","streamRun","Observable","subscriber","off","subscribe","event","next","data","import_common","ATTR_OPS","Set","coerce","v","Number","isNaN","parseAttrFilters","attr","undefined","raw","Array","isArray","filters","entry","key","op","rest","split","has","length","push","value","join","DurableApiController","dashboard","runs","status","workflow","tag","attr","listRuns","attributes","parseAttrFilters","metrics","workers","workerHealth","topology","run","id","detail","getRunDetail","NotFoundException","stream","streamRun","retry","retryWithInput","body","input","bulk","action","compensate","cancel","result","continue","redispatch","webhook","token","deliverWebhook","event","key","getEvent","update","name","import_common","import_common","DASHBOARD_BASE_PATH","Symbol","DASHBOARD_API_PATH","BUILD_BASE","spaDir","fileURLToPath","URL","CONTENT_TYPES","DurableUiController","dir","basePath","apiBasePath","index","indexPath","join","existsSync","NotFoundException","html","readFileSync","replaceAll","inject","includes","replace","asset","file","safe","basename","root","resolve","assetPath","startsWith","sep","type","extname","StreamableFile","normalize","path","replace","DurableApiModule","controllers","DurableApiController","providers","DashboardService","exports","DurableDashboardModule","forRoot","options","basePath","apiBasePath","module","imports","RouterModule","register","DurableUiController","provide","DASHBOARD_BASE_PATH","useValue","DASHBOARD_API_PATH"]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/index.ts","../../src/server/dashboard.service.ts","../../src/server/tokens.ts","../../src/server/durable-api.controller.ts","../../src/server/attr-filter.ts","../../src/server/durable-dashboard.module.ts","../../src/server/durable-ui.controller.ts"],"sourcesContent":["export * from './dashboard.service.js';\nexport * from './durable-api.controller.js';\nexport * from './durable-dashboard.module.js';\nexport * from './durable-ui.controller.js';\n","import {\n type DurableTopology,\n type EngineEvent,\n type GroupHealth,\n type MetricsCollector,\n type RunDetail,\n type RunGateway,\n type RunListItem,\n type RunQuery,\n type RunResult,\n STATE_STORE_CANONICAL,\n type StateStore,\n type UpdateResult,\n WorkflowEngine,\n collectMetrics,\n} from '@dudousxd/nestjs-durable-core';\nimport { Inject, Injectable, Optional } from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { RUN_GATEWAY } from './tokens.js';\n\n// Single source of truth: core owns `RunDetail` (`run-gateway.ts`) — re-exported here (not\n// re-declared) so this package's existing public API (`import type { RunDetail } from\n// '@dudousxd/nestjs-durable-dashboard'`) keeps working unchanged.\nexport type { RunDetail };\n\n/**\n * Read-model and actions backing the control-plane UI. Run-facing ops (list/detail/retry/cancel/\n * continue/retryWithInput/stream/bulk/workerHealth) route through the injected {@link RUN_GATEWAY}\n * port, which the lib binds on both topologies (a store-backed impl on the control plane, a proxy\n * over the transport on a store-less tenant) — so those ops work regardless of whether `store`/\n * `engine` are present (`workerHealth` is scoped to the tenant's own groups over the proxy).\n * Operator-only ops (metrics/deliverWebhook/getEvent/update) still need direct `store`/`engine`\n * access; they stay `@Optional()` and throw a clear error when absent.\n */\n@Injectable()\nexport class DashboardService {\n /** Prometheus counters accumulated from engine events since boot (per process); undefined on a\n * store-less tenant (no engine to accumulate from). */\n private readonly metricsCollector: MetricsCollector | undefined;\n\n constructor(\n @Inject(RUN_GATEWAY) private readonly gateway: RunGateway,\n @Optional() @Inject(STATE_STORE_CANONICAL) private readonly store?: StateStore,\n @Optional() private readonly engine?: WorkflowEngine,\n ) {\n // Only the control plane (which has the canonical store) has a FULL engine to accumulate from;\n // on a tenant `this.engine` is a store-less start client with no `subscribe`, so guard on store.\n this.metricsCollector = this.store && this.engine ? collectMetrics(this.engine) : undefined;\n }\n\n /**\n * The operator-only ops need the FULL engine + canonical store, which only the control plane has.\n * On a tenant the `WorkflowEngine` token is a store-less start client that implements none of these\n * methods, so presence of the canonical store is the reliable discriminator — gate on it (never\n * on engine presence, which is truthy on a tenant too).\n */\n private controlPlane(): { store: StateStore; engine: WorkflowEngine } {\n if (!this.store || !this.engine) {\n throw new Error(\n 'This durable dashboard operation requires the control plane (not available on a tenant deployment).',\n );\n }\n return { store: this.store, engine: this.engine };\n }\n\n /**\n * Prometheus-text metrics for a `/metrics` scrape: the event-counters (runs/steps by outcome,\n * per-workflow counts) plus live **backlog gauges** queried at scrape time — `durable_pending_runs`\n * (the dispatch backlog: the key health signal of the dispatch model) and `durable_dead_runs` (DLQ\n * size). Capped per status so the scrape can't load an unbounded result set. Control-plane-only.\n */\n async metrics(): Promise<string> {\n const { store, engine } = this.controlPlane();\n const cap = 10_000;\n const [pending, running, dead] = await Promise.all([\n store.listRuns({ status: 'pending', limit: cap }),\n store.listRuns({ status: 'running', limit: cap }),\n store.listRuns({ status: 'dead', limit: cap }),\n ]);\n const gauges = [\n '# TYPE durable_pending_runs gauge',\n `durable_pending_runs ${pending.length}`,\n '# TYPE durable_running_runs gauge',\n `durable_running_runs ${running.length}`,\n '# TYPE durable_dead_runs gauge',\n `durable_dead_runs ${dead.length}`,\n ];\n // Per-group worker health: backlog vs. live workers. `depth>0 && live==0` is the alert (work\n // piling up with no consumer) — expressible as a Prometheus rule on these two series.\n const health = await engine.workerHealth();\n if (health.length > 0) {\n gauges.push('# TYPE durable_group_queue_depth gauge');\n for (const h of health) {\n gauges.push(`durable_group_queue_depth{group=\"${h.group}\"} ${h.depth}`);\n }\n gauges.push('# TYPE durable_group_live_workers gauge');\n for (const h of health) {\n gauges.push(`durable_group_live_workers{group=\"${h.group}\"} ${h.liveWorkers.length}`);\n }\n }\n return `${this.metricsCollector?.prometheus() ?? ''}${gauges.join('\\n')}\\n`;\n }\n\n listRuns(query: RunQuery): Promise<RunListItem[]> {\n return this.gateway.listRuns(query);\n }\n\n /** This deployment's durable role (control plane vs tenant) + tenant name — for the dashboard header\n * badge. Routes through the gateway, which knows its own topology (store-backed = operator, proxy =\n * tenant). Works on both topologies (no store/engine needed). */\n topology(): DurableTopology {\n return this.gateway.topology();\n }\n\n /** Per-group worker health (queue backlog + live worker heartbeats) for the Workers panel. The\n * alert state a row turns red on is `depth > 0 && liveWorkers.length === 0`. Routes through the\n * gateway so it works on BOTH topologies: the control plane returns every group; a tenant proxies\n * to the operator, which scopes the result to the tenant's own `@<tenant>` groups. */\n async workerHealth(): Promise<GroupHealth[]> {\n return this.gateway.workerHealth();\n }\n\n getRunDetail(runId: string): Promise<RunDetail | null> {\n return this.gateway.getRunDetail(runId);\n }\n\n /** Fix-and-replay a dead/failed run with a corrected input — a fresh linked run. Returns its id. */\n retryWithInput(runId: string, input: unknown): Promise<{ runId: string } | null> {\n return this.gateway.retryWithInput(runId, input);\n }\n\n /** Re-run a failed/incomplete run; completed steps replay from their checkpoints. */\n retry(runId: string): Promise<RunResult | null> {\n return this.gateway.retry(runId);\n }\n\n cancel(runId: string, opts?: { compensate?: boolean }): Promise<RunResult | null> {\n return this.gateway.cancel(runId, opts);\n }\n\n /**\n * Apply `retry` or `cancel` to every run matching a filter (status / tag / workflow) — e.g. \"retry\n * every `dead` run tagged `type:mel`\". Capped at 500 runs per call; runs that can't be acted on\n * (already terminal) are skipped. Returns how many matched and how many the action applied to.\n */\n async bulk(\n action: 'retry' | 'cancel',\n filter: Pick<RunQuery, 'status' | 'tag' | 'workflow' | 'attributes'>,\n opts?: { compensate?: boolean },\n ): Promise<{ matched: number; applied: number }> {\n const runs = await this.gateway.listRuns({ ...filter, limit: 500 });\n let applied = 0;\n for (const r of runs) {\n try {\n if (action === 'retry') await this.gateway.retry(r.id);\n else await this.gateway.cancel(r.id, opts);\n applied += 1;\n } catch {\n // Skip a run that can't take the action (e.g. already terminal) — keep going.\n }\n }\n return { matched: runs.length, applied };\n }\n\n /** Resume a run paused at a `ctx.breakpoint` (the \"continue\" button). */\n continue(runId: string): Promise<RunResult | null> {\n return this.gateway.continue(runId);\n }\n\n /** Re-dispatch a run's stuck `pending` remote steps — recovery for a lost step dispatch. */\n redispatch(runId: string) {\n return this.gateway.redispatchPending(runId);\n }\n\n /**\n * Deliver a `ctx.webhook()` callback: turn an inbound POST (token + body) into the signal the\n * waiting run is parked on. Returns the run result, or `null` if no run waits on that token (a\n * stale/duplicate callback) — a safe no-op the controller maps to 404. Control-plane-only.\n */\n async deliverWebhook(token: string, body: unknown): Promise<RunResult | null> {\n return this.controlPlane().engine.signal(token, body);\n }\n\n /** Side-effect-free read of a value a run published via `ctx.setEvent` (a live query). Control-plane-only. */\n async getEvent(runId: string, key: string): Promise<unknown> {\n return this.controlPlane().engine.getEvent(runId, key);\n }\n\n /** Deliver a validated `ctx.onUpdate` to a run; the validator may reject it (see UpdateResult). Control-plane-only. */\n async update(runId: string, name: string, arg: unknown): Promise<UpdateResult> {\n return this.controlPlane().engine.update(runId, name, arg);\n }\n\n /**\n * Live stream of a run's lifecycle events for SSE. Backed by `gateway.subscribe`, which — on the\n * control plane — receives events from EVERY instance (so a dashboard-only pod tails a run\n * executing on a worker pod), and on a tenant round-trips over the transport. Either way the\n * gateway already filters to `runId`; the dashboard just wires the events through.\n */\n streamRun(runId: string): Observable<{ data: EngineEvent }> {\n return new Observable<{ data: EngineEvent }>((subscriber) => {\n const off = this.gateway.subscribe(runId, (event) => subscriber.next({ data: event }));\n return () => off();\n });\n }\n}\n","/**\n * Injection token for the bound `RunGateway`, owned by `@dudousxd/nestjs-durable` (the nestjs\n * package). The dashboard package does NOT depend on `@dudousxd/nestjs-durable` — its only\n * relevant peer dep is `@dudousxd/nestjs-durable-core` — so we do NOT import the token from\n * there. Instead we share its well-known token by value, the same pattern\n * `@dudousxd/nestjs-durable`'s own `tokens.ts` uses for `CONTEXT_ACCESSOR` (a cross-lib token\n * owned by `@dudousxd/nestjs-context`).\n *\n * `Symbol.for(key)` uses the global symbol registry, so this resolves to the SAME symbol\n * instance as `packages/nestjs/src/tokens.ts`'s `RUN_GATEWAY` without any import. The key MUST\n * stay byte-identical with that export.\n */\nexport const RUN_GATEWAY = Symbol.for('nestjs-durable:run-gateway');\n","import type { RunStatus } from '@dudousxd/nestjs-durable-core';\nimport {\n Body,\n Controller,\n Get,\n Header,\n NotFoundException,\n Param,\n Post,\n Query,\n Sse,\n} from '@nestjs/common';\nimport { parseAttrFilters } from './attr-filter.js';\nimport { DashboardService } from './dashboard.service.js';\n\n/** JSON API consumed by the control-plane SPA. Mounted at `apiBasePath` (set by RouterModule). */\n@Controller()\nexport class DurableApiController {\n constructor(private readonly dashboard: DashboardService) {}\n\n @Get('runs')\n runs(\n @Query('status') status?: RunStatus,\n @Query('workflow') workflow?: string,\n @Query('tag') tag?: string,\n @Query('attr') attr?: string | string[],\n ) {\n return this.dashboard.listRuns({ status, workflow, tag, attributes: parseAttrFilters(attr) });\n }\n\n /** Prometheus-text metrics (runs/steps by outcome, per-workflow counts) for a scrape. */\n @Get('metrics')\n @Header('Content-Type', 'text/plain; version=0.0.4')\n metrics() {\n return this.dashboard.metrics();\n }\n\n /** Per-group worker health (queue backlog + live worker heartbeats) for the Workers panel. */\n @Get('workers')\n workers() {\n return this.dashboard.workerHealth();\n }\n\n /** This deployment's durable role (control plane vs tenant) + tenant name — for the header badge. */\n @Get('topology')\n topology() {\n return this.dashboard.topology();\n }\n\n @Get('runs/:id')\n async run(@Param('id') id: string) {\n const detail = await this.dashboard.getRunDetail(id);\n if (!detail) throw new NotFoundException(`run ${id} not found`);\n return detail;\n }\n\n /** Server-Sent Events stream of a run's live lifecycle events — the dashboard tails it instead\n * of polling. Cross-pod when the transport has a control plane (see DashboardService.streamRun). */\n @Sse('runs/:id/stream')\n stream(@Param('id') id: string) {\n return this.dashboard.streamRun(id);\n }\n\n @Post('runs/:id/retry')\n retry(@Param('id') id: string) {\n return this.dashboard.retry(id);\n }\n\n /** Fix-and-replay: re-run a dead/failed run with a corrected input (a fresh linked run). */\n @Post('runs/:id/retry-with-input')\n retryWithInput(@Param('id') id: string, @Body() body: { input: unknown }) {\n return this.dashboard.retryWithInput(id, body?.input);\n }\n\n /** Bulk retry/cancel every run matching a filter (status / tag / workflow). */\n @Post('bulk/:action')\n bulk(\n @Param('action') action: 'retry' | 'cancel',\n @Query('status') status?: RunStatus,\n @Query('tag') tag?: string,\n @Query('workflow') workflow?: string,\n @Query('attr') attr?: string | string[],\n @Query('compensate') compensate?: string,\n ) {\n return this.dashboard.bulk(\n action === 'cancel' ? 'cancel' : 'retry',\n { status, tag, workflow, attributes: parseAttrFilters(attr) },\n { compensate: compensate === 'true' },\n );\n }\n\n @Post('runs/:id/cancel')\n async cancel(@Param('id') id: string, @Query('compensate') compensate?: string) {\n const result = await this.dashboard.cancel(id, { compensate: compensate === 'true' });\n if (!result) throw new NotFoundException(`run ${id} not found`);\n return result;\n }\n\n @Post('runs/:id/continue')\n async continue(@Param('id') id: string) {\n const result = await this.dashboard.continue(id);\n if (!result) throw new NotFoundException(`run ${id} is not paused at a breakpoint`);\n return result;\n }\n\n /** Re-dispatch a run's stuck `pending` remote steps — recovery for a lost step dispatch. */\n @Post('runs/:id/redispatch')\n async redispatch(@Param('id') id: string) {\n const result = await this.dashboard.redispatch(id);\n if (!result) throw new NotFoundException(`run ${id} not found`);\n return result;\n }\n\n /**\n * Public callback endpoint for `ctx.webhook()`: a third party POSTs here (the url handed to it),\n * and the body resumes the waiting run. NOTE: this is reachable by external systems — protect it\n * by treating the token as a secret (it embeds runId:seq) and/or fronting it with signature\n * verification in your own middleware.\n */\n @Post('webhooks/:token')\n async webhook(@Param('token') token: string, @Body() body: unknown) {\n const result = await this.dashboard.deliverWebhook(token, body);\n if (!result) throw new NotFoundException(`no run is waiting on webhook ${token}`);\n return result;\n }\n\n /** Live query: the latest value a run published for `key` via `ctx.setEvent` (no side effect). */\n @Get('runs/:id/events/:key')\n event(@Param('id') id: string, @Param('key') key: string) {\n return this.dashboard.getEvent(id, key);\n }\n\n /** Deliver a validated `ctx.onUpdate` to a run; the body is the update argument. */\n @Post('runs/:id/updates/:name')\n update(@Param('id') id: string, @Param('name') name: string, @Body() body: unknown) {\n return this.dashboard.update(id, name, body);\n }\n}\n","import type { AttributeFilter, AttributeOp } from '@dudousxd/nestjs-durable-core';\n\nconst ATTR_OPS = new Set<AttributeOp>(['eq', 'ne', 'gt', 'gte', 'lt', 'lte']);\n\n/** Coerce a query-string value: `true`/`false` → boolean, numeric → number, else the raw string. */\nfunction coerce(v: string): string | number | boolean {\n if (v === 'true') return true;\n if (v === 'false') return false;\n if (v !== '' && !Number.isNaN(Number(v))) return Number(v);\n return v;\n}\n\n/**\n * Parse `attr=key:op:value` query params (repeatable, ANDed) into {@link AttributeFilter}s — e.g.\n * `?attr=amount:gte:200&attr=tier:eq:pro`. `op` must be a known operator; a colon in the value is\n * preserved (only the first two colons delimit). Malformed entries are skipped.\n */\nexport function parseAttrFilters(attr?: string | string[]): AttributeFilter[] | undefined {\n if (!attr) return undefined;\n const raw = Array.isArray(attr) ? attr : [attr];\n const filters: AttributeFilter[] = [];\n for (const entry of raw) {\n const [key, op, ...rest] = entry.split(':');\n if (!key || !ATTR_OPS.has(op as AttributeOp) || rest.length === 0) continue;\n filters.push({ key, op: op as AttributeOp, value: coerce(rest.join(':')) });\n }\n return filters.length ? filters : undefined;\n}\n","import { type DynamicModule, Module } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { DashboardService } from './dashboard.service.js';\nimport { DurableApiController } from './durable-api.controller.js';\nimport {\n DASHBOARD_API_PATH,\n DASHBOARD_BASE_PATH,\n DurableUiController,\n} from './durable-ui.controller.js';\n\nexport interface DurableDashboardOptions {\n /**\n * Where the SPA (UI) is served. Default `/durable`. This is a page route — keep it out of an\n * `/api` prefix so it reads as a UI, not an endpoint.\n */\n basePath?: string;\n /**\n * Where the JSON API is mounted (what the SPA fetches). Default `<basePath>/api`. Set it under\n * your app's `/api` prefix — e.g. `/api/durable` — so the API inherits the app's auth/proxy rules\n * while the UI stays at `basePath`.\n */\n apiBasePath?: string;\n}\n\n/** Leading slash, no trailing slash. */\nfunction normalize(path: string): string {\n return `/${path.replace(/^\\/+|\\/+$/g, '')}`;\n}\n\n/** Holds the JSON API controller + its read service, mounted on its own path by `forRoot`. */\n@Module({\n controllers: [DurableApiController],\n providers: [DashboardService],\n exports: [DashboardService],\n})\nexport class DurableApiModule {}\n\n/**\n * Mounts the control plane: the bundled React SPA at `basePath` and its JSON API at `apiBasePath`\n * (default `<basePath>/api`). Import via `DurableDashboardModule.forRoot(...)` alongside\n * `DurableModule` (global), so it resolves the engine and store. Front the routes with a guard.\n */\n@Module({})\nexport class DurableDashboardModule {\n static forRoot(options: DurableDashboardOptions = {}): DynamicModule {\n const basePath = normalize(options.basePath ?? '/durable');\n const apiBasePath = normalize(options.apiBasePath ?? `${basePath}/api`);\n return {\n module: DurableDashboardModule,\n imports: [\n DurableApiModule,\n RouterModule.register([\n { path: basePath, module: DurableDashboardModule }, // the UI controller below\n { path: apiBasePath, module: DurableApiModule },\n ]),\n ],\n controllers: [DurableUiController],\n providers: [\n { provide: DASHBOARD_BASE_PATH, useValue: basePath },\n { provide: DASHBOARD_API_PATH, useValue: apiBasePath },\n ],\n // Re-export the API module so its DashboardService reaches importers (e.g. flip's own controllers).\n exports: [DurableApiModule],\n };\n }\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { basename, extname, join, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n Controller,\n Get,\n Header,\n Inject,\n NotFoundException,\n Param,\n StreamableFile,\n} from '@nestjs/common';\n\n/** DI token carrying the UI mount base (e.g. `/durable`). */\nexport const DASHBOARD_BASE_PATH = Symbol('DASHBOARD_BASE_PATH');\n/** DI token carrying the JSON API base the SPA fetches from (e.g. `/api/durable`). */\nexport const DASHBOARD_API_PATH = Symbol('DASHBOARD_API_PATH');\n\n/** The base the SPA bundle was built with (Vite `base`); rewritten to the configured base at serve time. */\nconst BUILD_BASE = '/durable';\n\n/** dist/server/durable-ui.controller.js -> ../spa (the Vite build output). */\nfunction spaDir(): string {\n return fileURLToPath(new URL('../spa', import.meta.url));\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.map': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.json': 'application/json; charset=utf-8',\n '.woff2': 'font/woff2',\n '.ico': 'image/x-icon',\n};\n\n/**\n * Serves the bundled control-plane SPA at the configured base (+ hashed assets at `<base>/assets`).\n * The path comes from `RouterModule` (set by `DurableDashboardModule.forRoot({ basePath })`), so the\n * controller routes are relative.\n */\n@Controller()\nexport class DurableUiController {\n private readonly dir = spaDir();\n\n constructor(\n @Inject(DASHBOARD_BASE_PATH) private readonly basePath: string,\n @Inject(DASHBOARD_API_PATH) private readonly apiBasePath: string,\n ) {}\n\n // index.html references hash-named bundles, so it MUST NOT be cached (stale bundle = the\n // classic \"stuck loading after a deploy\"). The hashed assets below are immutable.\n @Get()\n @Header('Content-Type', 'text/html; charset=utf-8')\n @Header('Cache-Control', 'no-store, must-revalidate')\n index(): string {\n const indexPath = join(this.dir, 'index.html');\n if (!existsSync(indexPath)) {\n throw new NotFoundException('Dashboard is not built. Run the package build.');\n }\n // The bundle was built with Vite base `/durable/`; rewrite asset URLs to the configured base so\n // the SPA loads from `<base>/assets` wherever it's mounted, and tell the client its API base.\n const html = readFileSync(indexPath, 'utf8').replaceAll(\n `=\"${BUILD_BASE}/`,\n `=\"${this.basePath}/`,\n );\n // __DURABLE_BASE__ = where assets load; __DURABLE_API__ = where the SPA fetches the JSON API.\n const inject = `<script>window.__DURABLE_BASE__='${this.basePath}';window.__DURABLE_API__='${this.apiBasePath}';</script>`;\n return html.includes('</head>') ? html.replace('</head>', `${inject}</head>`) : inject + html;\n }\n\n @Get('assets/:file')\n @Header('Cache-Control', 'public, max-age=31536000, immutable')\n asset(@Param('file') file: string): StreamableFile {\n const safe = basename(file);\n if (safe !== file) throw new NotFoundException();\n const root = resolve(this.dir, 'assets');\n const assetPath = resolve(root, safe);\n if (!assetPath.startsWith(root + sep) || !existsSync(assetPath)) {\n throw new NotFoundException();\n }\n const type = CONTENT_TYPES[extname(safe)] ?? 'application/octet-stream';\n return new StreamableFile(readFileSync(assetPath), { type });\n }\n}\n"],"mappings":"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","names":["RUN_GATEWAY","Symbol","for","DashboardService","metricsCollector","gateway","store","engine","collectMetrics","undefined","controlPlane","Error","metrics","cap","pending","running","dead","Promise","all","listRuns","status","limit","gauges","length","health","workerHealth","push","h","group","depth","liveWorkers","prometheus","join","query","topology","getRunDetail","runId","retryWithInput","input","retry","cancel","opts","bulk","action","filter","runs","applied","r","id","matched","continue","redispatch","redispatchPending","deliverWebhook","token","body","signal","getEvent","key","update","name","arg","streamRun","Observable","subscriber","off","subscribe","event","next","data","import_common","ATTR_OPS","Set","coerce","v","Number","isNaN","parseAttrFilters","attr","undefined","raw","Array","isArray","filters","entry","key","op","rest","split","has","length","push","value","join","DurableApiController","dashboard","runs","status","workflow","tag","attr","listRuns","attributes","parseAttrFilters","metrics","workers","workerHealth","topology","run","id","detail","getRunDetail","NotFoundException","stream","streamRun","retry","retryWithInput","body","input","bulk","action","compensate","cancel","result","continue","redispatch","webhook","token","deliverWebhook","event","key","getEvent","update","name","import_common","import_common","DASHBOARD_BASE_PATH","Symbol","DASHBOARD_API_PATH","BUILD_BASE","spaDir","fileURLToPath","URL","CONTENT_TYPES","DurableUiController","dir","basePath","apiBasePath","index","indexPath","join","existsSync","NotFoundException","html","readFileSync","replaceAll","inject","includes","replace","asset","file","safe","basename","root","resolve","assetPath","startsWith","sep","type","extname","StreamableFile","normalize","path","replace","DurableApiModule","controllers","DurableApiController","providers","DashboardService","exports","DurableDashboardModule","forRoot","options","basePath","apiBasePath","module","imports","RouterModule","register","DurableUiController","provide","DASHBOARD_BASE_PATH","useValue","DASHBOARD_API_PATH"]}
|
package/dist/server/index.d.cts
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import * as _dudousxd_nestjs_durable_core from '@dudousxd/nestjs-durable-core';
|
|
2
|
-
import { RunGateway, StateStore, WorkflowEngine, RunQuery, RunListItem, DurableTopology, GroupHealth,
|
|
2
|
+
import { RunGateway, StateStore, WorkflowEngine, RunQuery, RunListItem, DurableTopology, GroupHealth, RunDetail, RunResult, UpdateResult, EngineEvent, RunStatus } from '@dudousxd/nestjs-durable-core';
|
|
3
|
+
export { RunDetail } from '@dudousxd/nestjs-durable-core';
|
|
3
4
|
import * as rxjs from 'rxjs';
|
|
4
5
|
import { Observable } from 'rxjs';
|
|
5
6
|
import { DynamicModule, StreamableFile } from '@nestjs/common';
|
|
6
7
|
|
|
7
|
-
interface RunDetail {
|
|
8
|
-
run: WorkflowRun;
|
|
9
|
-
/** Steps in execution order — the end-to-end timeline (local + remote). */
|
|
10
|
-
timeline: StepCheckpoint[];
|
|
11
|
-
/** Ids of runs this run spawned (ctx.child / ctx.startChild) — the parent→children tree. */
|
|
12
|
-
children: string[];
|
|
13
|
-
}
|
|
14
8
|
/**
|
|
15
9
|
* Read-model and actions backing the control-plane UI. Run-facing ops (list/detail/retry/cancel/
|
|
16
10
|
* continue/retryWithInput/stream/bulk/workerHealth) route through the injected {@link RUN_GATEWAY}
|
|
@@ -111,7 +105,7 @@ declare class DurableApiController {
|
|
|
111
105
|
workers(): Promise<_dudousxd_nestjs_durable_core.GroupHealth[]>;
|
|
112
106
|
/** This deployment's durable role (control plane vs tenant) + tenant name — for the header badge. */
|
|
113
107
|
topology(): _dudousxd_nestjs_durable_core.DurableTopology;
|
|
114
|
-
run(id: string): Promise<RunDetail>;
|
|
108
|
+
run(id: string): Promise<_dudousxd_nestjs_durable_core.RunDetail>;
|
|
115
109
|
/** Server-Sent Events stream of a run's live lifecycle events — the dashboard tails it instead
|
|
116
110
|
* of polling. Cross-pod when the transport has a control plane (see DashboardService.streamRun). */
|
|
117
111
|
stream(id: string): rxjs.Observable<{
|
|
@@ -191,4 +185,4 @@ declare class DurableUiController {
|
|
|
191
185
|
asset(file: string): StreamableFile;
|
|
192
186
|
}
|
|
193
187
|
|
|
194
|
-
export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, DashboardService, DurableApiController, DurableApiModule, DurableDashboardModule, type DurableDashboardOptions, DurableUiController
|
|
188
|
+
export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, DashboardService, DurableApiController, DurableApiModule, DurableDashboardModule, type DurableDashboardOptions, DurableUiController };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import * as _dudousxd_nestjs_durable_core from '@dudousxd/nestjs-durable-core';
|
|
2
|
-
import { RunGateway, StateStore, WorkflowEngine, RunQuery, RunListItem, DurableTopology, GroupHealth,
|
|
2
|
+
import { RunGateway, StateStore, WorkflowEngine, RunQuery, RunListItem, DurableTopology, GroupHealth, RunDetail, RunResult, UpdateResult, EngineEvent, RunStatus } from '@dudousxd/nestjs-durable-core';
|
|
3
|
+
export { RunDetail } from '@dudousxd/nestjs-durable-core';
|
|
3
4
|
import * as rxjs from 'rxjs';
|
|
4
5
|
import { Observable } from 'rxjs';
|
|
5
6
|
import { DynamicModule, StreamableFile } from '@nestjs/common';
|
|
6
7
|
|
|
7
|
-
interface RunDetail {
|
|
8
|
-
run: WorkflowRun;
|
|
9
|
-
/** Steps in execution order — the end-to-end timeline (local + remote). */
|
|
10
|
-
timeline: StepCheckpoint[];
|
|
11
|
-
/** Ids of runs this run spawned (ctx.child / ctx.startChild) — the parent→children tree. */
|
|
12
|
-
children: string[];
|
|
13
|
-
}
|
|
14
8
|
/**
|
|
15
9
|
* Read-model and actions backing the control-plane UI. Run-facing ops (list/detail/retry/cancel/
|
|
16
10
|
* continue/retryWithInput/stream/bulk/workerHealth) route through the injected {@link RUN_GATEWAY}
|
|
@@ -111,7 +105,7 @@ declare class DurableApiController {
|
|
|
111
105
|
workers(): Promise<_dudousxd_nestjs_durable_core.GroupHealth[]>;
|
|
112
106
|
/** This deployment's durable role (control plane vs tenant) + tenant name — for the header badge. */
|
|
113
107
|
topology(): _dudousxd_nestjs_durable_core.DurableTopology;
|
|
114
|
-
run(id: string): Promise<RunDetail>;
|
|
108
|
+
run(id: string): Promise<_dudousxd_nestjs_durable_core.RunDetail>;
|
|
115
109
|
/** Server-Sent Events stream of a run's live lifecycle events — the dashboard tails it instead
|
|
116
110
|
* of polling. Cross-pod when the transport has a control plane (see DashboardService.streamRun). */
|
|
117
111
|
stream(id: string): rxjs.Observable<{
|
|
@@ -191,4 +185,4 @@ declare class DurableUiController {
|
|
|
191
185
|
asset(file: string): StreamableFile;
|
|
192
186
|
}
|
|
193
187
|
|
|
194
|
-
export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, DashboardService, DurableApiController, DurableApiModule, DurableDashboardModule, type DurableDashboardOptions, DurableUiController
|
|
188
|
+
export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, DashboardService, DurableApiController, DurableApiModule, DurableDashboardModule, type DurableDashboardOptions, DurableUiController };
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/dashboard.service.ts","../../src/server/tokens.ts","../../src/server/durable-api.controller.ts","../../src/server/attr-filter.ts","../../src/server/durable-dashboard.module.ts","../../src/server/durable-ui.controller.ts"],"sourcesContent":["import {\n type DurableTopology,\n type EngineEvent,\n type GroupHealth,\n type MetricsCollector,\n type RunGateway,\n type RunListItem,\n type RunQuery,\n type RunResult,\n STATE_STORE_CANONICAL,\n type StateStore,\n type StepCheckpoint,\n type UpdateResult,\n WorkflowEngine,\n type WorkflowRun,\n collectMetrics,\n} from '@dudousxd/nestjs-durable-core';\nimport { Inject, Injectable, Optional } from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { RUN_GATEWAY } from './tokens.js';\n\nexport interface RunDetail {\n run: WorkflowRun;\n /** Steps in execution order — the end-to-end timeline (local + remote). */\n timeline: StepCheckpoint[];\n /** Ids of runs this run spawned (ctx.child / ctx.startChild) — the parent→children tree. */\n children: string[];\n}\n\n/**\n * Read-model and actions backing the control-plane UI. Run-facing ops (list/detail/retry/cancel/\n * continue/retryWithInput/stream/bulk/workerHealth) route through the injected {@link RUN_GATEWAY}\n * port, which the lib binds on both topologies (a store-backed impl on the control plane, a proxy\n * over the transport on a store-less tenant) — so those ops work regardless of whether `store`/\n * `engine` are present (`workerHealth` is scoped to the tenant's own groups over the proxy).\n * Operator-only ops (metrics/deliverWebhook/getEvent/update) still need direct `store`/`engine`\n * access; they stay `@Optional()` and throw a clear error when absent.\n */\n@Injectable()\nexport class DashboardService {\n /** Prometheus counters accumulated from engine events since boot (per process); undefined on a\n * store-less tenant (no engine to accumulate from). */\n private readonly metricsCollector: MetricsCollector | undefined;\n\n constructor(\n @Inject(RUN_GATEWAY) private readonly gateway: RunGateway,\n @Optional() @Inject(STATE_STORE_CANONICAL) private readonly store?: StateStore,\n @Optional() private readonly engine?: WorkflowEngine,\n ) {\n // Only the control plane (which has the canonical store) has a FULL engine to accumulate from;\n // on a tenant `this.engine` is a store-less start client with no `subscribe`, so guard on store.\n this.metricsCollector = this.store && this.engine ? collectMetrics(this.engine) : undefined;\n }\n\n /**\n * The operator-only ops need the FULL engine + canonical store, which only the control plane has.\n * On a tenant the `WorkflowEngine` token is a store-less start client that implements none of these\n * methods, so presence of the canonical store is the reliable discriminator — gate on it (never\n * on engine presence, which is truthy on a tenant too).\n */\n private controlPlane(): { store: StateStore; engine: WorkflowEngine } {\n if (!this.store || !this.engine) {\n throw new Error(\n 'This durable dashboard operation requires the control plane (not available on a tenant deployment).',\n );\n }\n return { store: this.store, engine: this.engine };\n }\n\n /**\n * Prometheus-text metrics for a `/metrics` scrape: the event-counters (runs/steps by outcome,\n * per-workflow counts) plus live **backlog gauges** queried at scrape time — `durable_pending_runs`\n * (the dispatch backlog: the key health signal of the dispatch model) and `durable_dead_runs` (DLQ\n * size). Capped per status so the scrape can't load an unbounded result set. Control-plane-only.\n */\n async metrics(): Promise<string> {\n const { store, engine } = this.controlPlane();\n const cap = 10_000;\n const [pending, running, dead] = await Promise.all([\n store.listRuns({ status: 'pending', limit: cap }),\n store.listRuns({ status: 'running', limit: cap }),\n store.listRuns({ status: 'dead', limit: cap }),\n ]);\n const gauges = [\n '# TYPE durable_pending_runs gauge',\n `durable_pending_runs ${pending.length}`,\n '# TYPE durable_running_runs gauge',\n `durable_running_runs ${running.length}`,\n '# TYPE durable_dead_runs gauge',\n `durable_dead_runs ${dead.length}`,\n ];\n // Per-group worker health: backlog vs. live workers. `depth>0 && live==0` is the alert (work\n // piling up with no consumer) — expressible as a Prometheus rule on these two series.\n const health = await engine.workerHealth();\n if (health.length > 0) {\n gauges.push('# TYPE durable_group_queue_depth gauge');\n for (const h of health) {\n gauges.push(`durable_group_queue_depth{group=\"${h.group}\"} ${h.depth}`);\n }\n gauges.push('# TYPE durable_group_live_workers gauge');\n for (const h of health) {\n gauges.push(`durable_group_live_workers{group=\"${h.group}\"} ${h.liveWorkers.length}`);\n }\n }\n return `${this.metricsCollector?.prometheus() ?? ''}${gauges.join('\\n')}\\n`;\n }\n\n listRuns(query: RunQuery): Promise<RunListItem[]> {\n return this.gateway.listRuns(query);\n }\n\n /** This deployment's durable role (control plane vs tenant) + tenant name — for the dashboard header\n * badge. Routes through the gateway, which knows its own topology (store-backed = operator, proxy =\n * tenant). Works on both topologies (no store/engine needed). */\n topology(): DurableTopology {\n return this.gateway.topology();\n }\n\n /** Per-group worker health (queue backlog + live worker heartbeats) for the Workers panel. The\n * alert state a row turns red on is `depth > 0 && liveWorkers.length === 0`. Routes through the\n * gateway so it works on BOTH topologies: the control plane returns every group; a tenant proxies\n * to the operator, which scopes the result to the tenant's own `@<tenant>` groups. */\n async workerHealth(): Promise<GroupHealth[]> {\n return this.gateway.workerHealth();\n }\n\n getRunDetail(runId: string): Promise<RunDetail | null> {\n return this.gateway.getRunDetail(runId);\n }\n\n /** Fix-and-replay a dead/failed run with a corrected input — a fresh linked run. Returns its id. */\n retryWithInput(runId: string, input: unknown): Promise<{ runId: string } | null> {\n return this.gateway.retryWithInput(runId, input);\n }\n\n /** Re-run a failed/incomplete run; completed steps replay from their checkpoints. */\n retry(runId: string): Promise<RunResult | null> {\n return this.gateway.retry(runId);\n }\n\n cancel(runId: string, opts?: { compensate?: boolean }): Promise<RunResult | null> {\n return this.gateway.cancel(runId, opts);\n }\n\n /**\n * Apply `retry` or `cancel` to every run matching a filter (status / tag / workflow) — e.g. \"retry\n * every `dead` run tagged `type:mel`\". Capped at 500 runs per call; runs that can't be acted on\n * (already terminal) are skipped. Returns how many matched and how many the action applied to.\n */\n async bulk(\n action: 'retry' | 'cancel',\n filter: Pick<RunQuery, 'status' | 'tag' | 'workflow' | 'attributes'>,\n opts?: { compensate?: boolean },\n ): Promise<{ matched: number; applied: number }> {\n const runs = await this.gateway.listRuns({ ...filter, limit: 500 });\n let applied = 0;\n for (const r of runs) {\n try {\n if (action === 'retry') await this.gateway.retry(r.id);\n else await this.gateway.cancel(r.id, opts);\n applied += 1;\n } catch {\n // Skip a run that can't take the action (e.g. already terminal) — keep going.\n }\n }\n return { matched: runs.length, applied };\n }\n\n /** Resume a run paused at a `ctx.breakpoint` (the \"continue\" button). */\n continue(runId: string): Promise<RunResult | null> {\n return this.gateway.continue(runId);\n }\n\n /** Re-dispatch a run's stuck `pending` remote steps — recovery for a lost step dispatch. */\n redispatch(runId: string) {\n return this.gateway.redispatchPending(runId);\n }\n\n /**\n * Deliver a `ctx.webhook()` callback: turn an inbound POST (token + body) into the signal the\n * waiting run is parked on. Returns the run result, or `null` if no run waits on that token (a\n * stale/duplicate callback) — a safe no-op the controller maps to 404. Control-plane-only.\n */\n async deliverWebhook(token: string, body: unknown): Promise<RunResult | null> {\n return this.controlPlane().engine.signal(token, body);\n }\n\n /** Side-effect-free read of a value a run published via `ctx.setEvent` (a live query). Control-plane-only. */\n async getEvent(runId: string, key: string): Promise<unknown> {\n return this.controlPlane().engine.getEvent(runId, key);\n }\n\n /** Deliver a validated `ctx.onUpdate` to a run; the validator may reject it (see UpdateResult). Control-plane-only. */\n async update(runId: string, name: string, arg: unknown): Promise<UpdateResult> {\n return this.controlPlane().engine.update(runId, name, arg);\n }\n\n /**\n * Live stream of a run's lifecycle events for SSE. Backed by `gateway.subscribe`, which — on the\n * control plane — receives events from EVERY instance (so a dashboard-only pod tails a run\n * executing on a worker pod), and on a tenant round-trips over the transport. Either way the\n * gateway already filters to `runId`; the dashboard just wires the events through.\n */\n streamRun(runId: string): Observable<{ data: EngineEvent }> {\n return new Observable<{ data: EngineEvent }>((subscriber) => {\n const off = this.gateway.subscribe(runId, (event) => subscriber.next({ data: event }));\n return () => off();\n });\n }\n}\n","/**\n * Injection token for the bound `RunGateway`, owned by `@dudousxd/nestjs-durable` (the nestjs\n * package). The dashboard package does NOT depend on `@dudousxd/nestjs-durable` — its only\n * relevant peer dep is `@dudousxd/nestjs-durable-core` — so we do NOT import the token from\n * there. Instead we share its well-known token by value, the same pattern\n * `@dudousxd/nestjs-durable`'s own `tokens.ts` uses for `CONTEXT_ACCESSOR` (a cross-lib token\n * owned by `@dudousxd/nestjs-context`).\n *\n * `Symbol.for(key)` uses the global symbol registry, so this resolves to the SAME symbol\n * instance as `packages/nestjs/src/tokens.ts`'s `RUN_GATEWAY` without any import. The key MUST\n * stay byte-identical with that export.\n */\nexport const RUN_GATEWAY = Symbol.for('nestjs-durable:run-gateway');\n","import type { RunStatus } from '@dudousxd/nestjs-durable-core';\nimport {\n Body,\n Controller,\n Get,\n Header,\n NotFoundException,\n Param,\n Post,\n Query,\n Sse,\n} from '@nestjs/common';\nimport { parseAttrFilters } from './attr-filter.js';\nimport { DashboardService } from './dashboard.service.js';\n\n/** JSON API consumed by the control-plane SPA. Mounted at `apiBasePath` (set by RouterModule). */\n@Controller()\nexport class DurableApiController {\n constructor(private readonly dashboard: DashboardService) {}\n\n @Get('runs')\n runs(\n @Query('status') status?: RunStatus,\n @Query('workflow') workflow?: string,\n @Query('tag') tag?: string,\n @Query('attr') attr?: string | string[],\n ) {\n return this.dashboard.listRuns({ status, workflow, tag, attributes: parseAttrFilters(attr) });\n }\n\n /** Prometheus-text metrics (runs/steps by outcome, per-workflow counts) for a scrape. */\n @Get('metrics')\n @Header('Content-Type', 'text/plain; version=0.0.4')\n metrics() {\n return this.dashboard.metrics();\n }\n\n /** Per-group worker health (queue backlog + live worker heartbeats) for the Workers panel. */\n @Get('workers')\n workers() {\n return this.dashboard.workerHealth();\n }\n\n /** This deployment's durable role (control plane vs tenant) + tenant name — for the header badge. */\n @Get('topology')\n topology() {\n return this.dashboard.topology();\n }\n\n @Get('runs/:id')\n async run(@Param('id') id: string) {\n const detail = await this.dashboard.getRunDetail(id);\n if (!detail) throw new NotFoundException(`run ${id} not found`);\n return detail;\n }\n\n /** Server-Sent Events stream of a run's live lifecycle events — the dashboard tails it instead\n * of polling. Cross-pod when the transport has a control plane (see DashboardService.streamRun). */\n @Sse('runs/:id/stream')\n stream(@Param('id') id: string) {\n return this.dashboard.streamRun(id);\n }\n\n @Post('runs/:id/retry')\n retry(@Param('id') id: string) {\n return this.dashboard.retry(id);\n }\n\n /** Fix-and-replay: re-run a dead/failed run with a corrected input (a fresh linked run). */\n @Post('runs/:id/retry-with-input')\n retryWithInput(@Param('id') id: string, @Body() body: { input: unknown }) {\n return this.dashboard.retryWithInput(id, body?.input);\n }\n\n /** Bulk retry/cancel every run matching a filter (status / tag / workflow). */\n @Post('bulk/:action')\n bulk(\n @Param('action') action: 'retry' | 'cancel',\n @Query('status') status?: RunStatus,\n @Query('tag') tag?: string,\n @Query('workflow') workflow?: string,\n @Query('attr') attr?: string | string[],\n @Query('compensate') compensate?: string,\n ) {\n return this.dashboard.bulk(\n action === 'cancel' ? 'cancel' : 'retry',\n { status, tag, workflow, attributes: parseAttrFilters(attr) },\n { compensate: compensate === 'true' },\n );\n }\n\n @Post('runs/:id/cancel')\n async cancel(@Param('id') id: string, @Query('compensate') compensate?: string) {\n const result = await this.dashboard.cancel(id, { compensate: compensate === 'true' });\n if (!result) throw new NotFoundException(`run ${id} not found`);\n return result;\n }\n\n @Post('runs/:id/continue')\n async continue(@Param('id') id: string) {\n const result = await this.dashboard.continue(id);\n if (!result) throw new NotFoundException(`run ${id} is not paused at a breakpoint`);\n return result;\n }\n\n /** Re-dispatch a run's stuck `pending` remote steps — recovery for a lost step dispatch. */\n @Post('runs/:id/redispatch')\n async redispatch(@Param('id') id: string) {\n const result = await this.dashboard.redispatch(id);\n if (!result) throw new NotFoundException(`run ${id} not found`);\n return result;\n }\n\n /**\n * Public callback endpoint for `ctx.webhook()`: a third party POSTs here (the url handed to it),\n * and the body resumes the waiting run. NOTE: this is reachable by external systems — protect it\n * by treating the token as a secret (it embeds runId:seq) and/or fronting it with signature\n * verification in your own middleware.\n */\n @Post('webhooks/:token')\n async webhook(@Param('token') token: string, @Body() body: unknown) {\n const result = await this.dashboard.deliverWebhook(token, body);\n if (!result) throw new NotFoundException(`no run is waiting on webhook ${token}`);\n return result;\n }\n\n /** Live query: the latest value a run published for `key` via `ctx.setEvent` (no side effect). */\n @Get('runs/:id/events/:key')\n event(@Param('id') id: string, @Param('key') key: string) {\n return this.dashboard.getEvent(id, key);\n }\n\n /** Deliver a validated `ctx.onUpdate` to a run; the body is the update argument. */\n @Post('runs/:id/updates/:name')\n update(@Param('id') id: string, @Param('name') name: string, @Body() body: unknown) {\n return this.dashboard.update(id, name, body);\n }\n}\n","import type { AttributeFilter, AttributeOp } from '@dudousxd/nestjs-durable-core';\n\nconst ATTR_OPS = new Set<AttributeOp>(['eq', 'ne', 'gt', 'gte', 'lt', 'lte']);\n\n/** Coerce a query-string value: `true`/`false` → boolean, numeric → number, else the raw string. */\nfunction coerce(v: string): string | number | boolean {\n if (v === 'true') return true;\n if (v === 'false') return false;\n if (v !== '' && !Number.isNaN(Number(v))) return Number(v);\n return v;\n}\n\n/**\n * Parse `attr=key:op:value` query params (repeatable, ANDed) into {@link AttributeFilter}s — e.g.\n * `?attr=amount:gte:200&attr=tier:eq:pro`. `op` must be a known operator; a colon in the value is\n * preserved (only the first two colons delimit). Malformed entries are skipped.\n */\nexport function parseAttrFilters(attr?: string | string[]): AttributeFilter[] | undefined {\n if (!attr) return undefined;\n const raw = Array.isArray(attr) ? attr : [attr];\n const filters: AttributeFilter[] = [];\n for (const entry of raw) {\n const [key, op, ...rest] = entry.split(':');\n if (!key || !ATTR_OPS.has(op as AttributeOp) || rest.length === 0) continue;\n filters.push({ key, op: op as AttributeOp, value: coerce(rest.join(':')) });\n }\n return filters.length ? filters : undefined;\n}\n","import { type DynamicModule, Module } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { DashboardService } from './dashboard.service.js';\nimport { DurableApiController } from './durable-api.controller.js';\nimport {\n DASHBOARD_API_PATH,\n DASHBOARD_BASE_PATH,\n DurableUiController,\n} from './durable-ui.controller.js';\n\nexport interface DurableDashboardOptions {\n /**\n * Where the SPA (UI) is served. Default `/durable`. This is a page route — keep it out of an\n * `/api` prefix so it reads as a UI, not an endpoint.\n */\n basePath?: string;\n /**\n * Where the JSON API is mounted (what the SPA fetches). Default `<basePath>/api`. Set it under\n * your app's `/api` prefix — e.g. `/api/durable` — so the API inherits the app's auth/proxy rules\n * while the UI stays at `basePath`.\n */\n apiBasePath?: string;\n}\n\n/** Leading slash, no trailing slash. */\nfunction normalize(path: string): string {\n return `/${path.replace(/^\\/+|\\/+$/g, '')}`;\n}\n\n/** Holds the JSON API controller + its read service, mounted on its own path by `forRoot`. */\n@Module({\n controllers: [DurableApiController],\n providers: [DashboardService],\n exports: [DashboardService],\n})\nexport class DurableApiModule {}\n\n/**\n * Mounts the control plane: the bundled React SPA at `basePath` and its JSON API at `apiBasePath`\n * (default `<basePath>/api`). Import via `DurableDashboardModule.forRoot(...)` alongside\n * `DurableModule` (global), so it resolves the engine and store. Front the routes with a guard.\n */\n@Module({})\nexport class DurableDashboardModule {\n static forRoot(options: DurableDashboardOptions = {}): DynamicModule {\n const basePath = normalize(options.basePath ?? '/durable');\n const apiBasePath = normalize(options.apiBasePath ?? `${basePath}/api`);\n return {\n module: DurableDashboardModule,\n imports: [\n DurableApiModule,\n RouterModule.register([\n { path: basePath, module: DurableDashboardModule }, // the UI controller below\n { path: apiBasePath, module: DurableApiModule },\n ]),\n ],\n controllers: [DurableUiController],\n providers: [\n { provide: DASHBOARD_BASE_PATH, useValue: basePath },\n { provide: DASHBOARD_API_PATH, useValue: apiBasePath },\n ],\n // Re-export the API module so its DashboardService reaches importers (e.g. flip's own controllers).\n exports: [DurableApiModule],\n };\n }\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { basename, extname, join, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n Controller,\n Get,\n Header,\n Inject,\n NotFoundException,\n Param,\n StreamableFile,\n} from '@nestjs/common';\n\n/** DI token carrying the UI mount base (e.g. `/durable`). */\nexport const DASHBOARD_BASE_PATH = Symbol('DASHBOARD_BASE_PATH');\n/** DI token carrying the JSON API base the SPA fetches from (e.g. `/api/durable`). */\nexport const DASHBOARD_API_PATH = Symbol('DASHBOARD_API_PATH');\n\n/** The base the SPA bundle was built with (Vite `base`); rewritten to the configured base at serve time. */\nconst BUILD_BASE = '/durable';\n\n/** dist/server/durable-ui.controller.js -> ../spa (the Vite build output). */\nfunction spaDir(): string {\n return fileURLToPath(new URL('../spa', import.meta.url));\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.map': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.json': 'application/json; charset=utf-8',\n '.woff2': 'font/woff2',\n '.ico': 'image/x-icon',\n};\n\n/**\n * Serves the bundled control-plane SPA at the configured base (+ hashed assets at `<base>/assets`).\n * The path comes from `RouterModule` (set by `DurableDashboardModule.forRoot({ basePath })`), so the\n * controller routes are relative.\n */\n@Controller()\nexport class DurableUiController {\n private readonly dir = spaDir();\n\n constructor(\n @Inject(DASHBOARD_BASE_PATH) private readonly basePath: string,\n @Inject(DASHBOARD_API_PATH) private readonly apiBasePath: string,\n ) {}\n\n // index.html references hash-named bundles, so it MUST NOT be cached (stale bundle = the\n // classic \"stuck loading after a deploy\"). The hashed assets below are immutable.\n @Get()\n @Header('Content-Type', 'text/html; charset=utf-8')\n @Header('Cache-Control', 'no-store, must-revalidate')\n index(): string {\n const indexPath = join(this.dir, 'index.html');\n if (!existsSync(indexPath)) {\n throw new NotFoundException('Dashboard is not built. Run the package build.');\n }\n // The bundle was built with Vite base `/durable/`; rewrite asset URLs to the configured base so\n // the SPA loads from `<base>/assets` wherever it's mounted, and tell the client its API base.\n const html = readFileSync(indexPath, 'utf8').replaceAll(\n `=\"${BUILD_BASE}/`,\n `=\"${this.basePath}/`,\n );\n // __DURABLE_BASE__ = where assets load; __DURABLE_API__ = where the SPA fetches the JSON API.\n const inject = `<script>window.__DURABLE_BASE__='${this.basePath}';window.__DURABLE_API__='${this.apiBasePath}';</script>`;\n return html.includes('</head>') ? html.replace('</head>', `${inject}</head>`) : inject + html;\n }\n\n @Get('assets/:file')\n @Header('Cache-Control', 'public, max-age=31536000, immutable')\n asset(@Param('file') file: string): StreamableFile {\n const safe = basename(file);\n if (safe !== file) throw new NotFoundException();\n const root = resolve(this.dir, 'assets');\n const assetPath = resolve(root, safe);\n if (!assetPath.startsWith(root + sep) || !existsSync(assetPath)) {\n throw new NotFoundException();\n }\n const type = CONTENT_TYPES[extname(safe)] ?? 'application/octet-stream';\n return new StreamableFile(readFileSync(assetPath), { type });\n }\n}\n"],"mappings":"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","names":["STATE_STORE_CANONICAL","WorkflowEngine","collectMetrics","Inject","Injectable","Optional","Observable","RUN_GATEWAY","Symbol","for","DashboardService","metricsCollector","gateway","store","engine","collectMetrics","undefined","controlPlane","Error","metrics","cap","pending","running","dead","Promise","all","listRuns","status","limit","gauges","length","health","workerHealth","push","h","group","depth","liveWorkers","prometheus","join","query","topology","getRunDetail","runId","retryWithInput","input","retry","cancel","opts","bulk","action","filter","runs","applied","r","id","matched","continue","redispatch","redispatchPending","deliverWebhook","token","body","signal","getEvent","key","update","name","arg","streamRun","Observable","subscriber","off","subscribe","event","next","data","Body","Controller","Get","Header","NotFoundException","Param","Post","Query","Sse","ATTR_OPS","Set","coerce","v","Number","isNaN","parseAttrFilters","attr","undefined","raw","Array","isArray","filters","entry","key","op","rest","split","has","length","push","value","join","DurableApiController","dashboard","runs","status","workflow","tag","attr","listRuns","attributes","parseAttrFilters","metrics","workers","workerHealth","topology","run","id","detail","getRunDetail","NotFoundException","stream","streamRun","retry","retryWithInput","body","input","bulk","action","compensate","cancel","result","continue","redispatch","webhook","token","deliverWebhook","event","key","getEvent","update","name","Module","RouterModule","existsSync","readFileSync","basename","extname","join","resolve","sep","fileURLToPath","Controller","Get","Header","Inject","NotFoundException","Param","StreamableFile","DASHBOARD_BASE_PATH","Symbol","DASHBOARD_API_PATH","BUILD_BASE","spaDir","fileURLToPath","URL","url","CONTENT_TYPES","DurableUiController","dir","basePath","apiBasePath","index","indexPath","join","existsSync","NotFoundException","html","readFileSync","replaceAll","inject","includes","replace","asset","file","safe","basename","root","resolve","assetPath","startsWith","sep","type","extname","StreamableFile","normalize","path","replace","DurableApiModule","controllers","DurableApiController","providers","DashboardService","exports","DurableDashboardModule","forRoot","options","basePath","apiBasePath","module","imports","RouterModule","register","DurableUiController","provide","DASHBOARD_BASE_PATH","useValue","DASHBOARD_API_PATH"]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/dashboard.service.ts","../../src/server/tokens.ts","../../src/server/durable-api.controller.ts","../../src/server/attr-filter.ts","../../src/server/durable-dashboard.module.ts","../../src/server/durable-ui.controller.ts"],"sourcesContent":["import {\n type DurableTopology,\n type EngineEvent,\n type GroupHealth,\n type MetricsCollector,\n type RunDetail,\n type RunGateway,\n type RunListItem,\n type RunQuery,\n type RunResult,\n STATE_STORE_CANONICAL,\n type StateStore,\n type UpdateResult,\n WorkflowEngine,\n collectMetrics,\n} from '@dudousxd/nestjs-durable-core';\nimport { Inject, Injectable, Optional } from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { RUN_GATEWAY } from './tokens.js';\n\n// Single source of truth: core owns `RunDetail` (`run-gateway.ts`) — re-exported here (not\n// re-declared) so this package's existing public API (`import type { RunDetail } from\n// '@dudousxd/nestjs-durable-dashboard'`) keeps working unchanged.\nexport type { RunDetail };\n\n/**\n * Read-model and actions backing the control-plane UI. Run-facing ops (list/detail/retry/cancel/\n * continue/retryWithInput/stream/bulk/workerHealth) route through the injected {@link RUN_GATEWAY}\n * port, which the lib binds on both topologies (a store-backed impl on the control plane, a proxy\n * over the transport on a store-less tenant) — so those ops work regardless of whether `store`/\n * `engine` are present (`workerHealth` is scoped to the tenant's own groups over the proxy).\n * Operator-only ops (metrics/deliverWebhook/getEvent/update) still need direct `store`/`engine`\n * access; they stay `@Optional()` and throw a clear error when absent.\n */\n@Injectable()\nexport class DashboardService {\n /** Prometheus counters accumulated from engine events since boot (per process); undefined on a\n * store-less tenant (no engine to accumulate from). */\n private readonly metricsCollector: MetricsCollector | undefined;\n\n constructor(\n @Inject(RUN_GATEWAY) private readonly gateway: RunGateway,\n @Optional() @Inject(STATE_STORE_CANONICAL) private readonly store?: StateStore,\n @Optional() private readonly engine?: WorkflowEngine,\n ) {\n // Only the control plane (which has the canonical store) has a FULL engine to accumulate from;\n // on a tenant `this.engine` is a store-less start client with no `subscribe`, so guard on store.\n this.metricsCollector = this.store && this.engine ? collectMetrics(this.engine) : undefined;\n }\n\n /**\n * The operator-only ops need the FULL engine + canonical store, which only the control plane has.\n * On a tenant the `WorkflowEngine` token is a store-less start client that implements none of these\n * methods, so presence of the canonical store is the reliable discriminator — gate on it (never\n * on engine presence, which is truthy on a tenant too).\n */\n private controlPlane(): { store: StateStore; engine: WorkflowEngine } {\n if (!this.store || !this.engine) {\n throw new Error(\n 'This durable dashboard operation requires the control plane (not available on a tenant deployment).',\n );\n }\n return { store: this.store, engine: this.engine };\n }\n\n /**\n * Prometheus-text metrics for a `/metrics` scrape: the event-counters (runs/steps by outcome,\n * per-workflow counts) plus live **backlog gauges** queried at scrape time — `durable_pending_runs`\n * (the dispatch backlog: the key health signal of the dispatch model) and `durable_dead_runs` (DLQ\n * size). Capped per status so the scrape can't load an unbounded result set. Control-plane-only.\n */\n async metrics(): Promise<string> {\n const { store, engine } = this.controlPlane();\n const cap = 10_000;\n const [pending, running, dead] = await Promise.all([\n store.listRuns({ status: 'pending', limit: cap }),\n store.listRuns({ status: 'running', limit: cap }),\n store.listRuns({ status: 'dead', limit: cap }),\n ]);\n const gauges = [\n '# TYPE durable_pending_runs gauge',\n `durable_pending_runs ${pending.length}`,\n '# TYPE durable_running_runs gauge',\n `durable_running_runs ${running.length}`,\n '# TYPE durable_dead_runs gauge',\n `durable_dead_runs ${dead.length}`,\n ];\n // Per-group worker health: backlog vs. live workers. `depth>0 && live==0` is the alert (work\n // piling up with no consumer) — expressible as a Prometheus rule on these two series.\n const health = await engine.workerHealth();\n if (health.length > 0) {\n gauges.push('# TYPE durable_group_queue_depth gauge');\n for (const h of health) {\n gauges.push(`durable_group_queue_depth{group=\"${h.group}\"} ${h.depth}`);\n }\n gauges.push('# TYPE durable_group_live_workers gauge');\n for (const h of health) {\n gauges.push(`durable_group_live_workers{group=\"${h.group}\"} ${h.liveWorkers.length}`);\n }\n }\n return `${this.metricsCollector?.prometheus() ?? ''}${gauges.join('\\n')}\\n`;\n }\n\n listRuns(query: RunQuery): Promise<RunListItem[]> {\n return this.gateway.listRuns(query);\n }\n\n /** This deployment's durable role (control plane vs tenant) + tenant name — for the dashboard header\n * badge. Routes through the gateway, which knows its own topology (store-backed = operator, proxy =\n * tenant). Works on both topologies (no store/engine needed). */\n topology(): DurableTopology {\n return this.gateway.topology();\n }\n\n /** Per-group worker health (queue backlog + live worker heartbeats) for the Workers panel. The\n * alert state a row turns red on is `depth > 0 && liveWorkers.length === 0`. Routes through the\n * gateway so it works on BOTH topologies: the control plane returns every group; a tenant proxies\n * to the operator, which scopes the result to the tenant's own `@<tenant>` groups. */\n async workerHealth(): Promise<GroupHealth[]> {\n return this.gateway.workerHealth();\n }\n\n getRunDetail(runId: string): Promise<RunDetail | null> {\n return this.gateway.getRunDetail(runId);\n }\n\n /** Fix-and-replay a dead/failed run with a corrected input — a fresh linked run. Returns its id. */\n retryWithInput(runId: string, input: unknown): Promise<{ runId: string } | null> {\n return this.gateway.retryWithInput(runId, input);\n }\n\n /** Re-run a failed/incomplete run; completed steps replay from their checkpoints. */\n retry(runId: string): Promise<RunResult | null> {\n return this.gateway.retry(runId);\n }\n\n cancel(runId: string, opts?: { compensate?: boolean }): Promise<RunResult | null> {\n return this.gateway.cancel(runId, opts);\n }\n\n /**\n * Apply `retry` or `cancel` to every run matching a filter (status / tag / workflow) — e.g. \"retry\n * every `dead` run tagged `type:mel`\". Capped at 500 runs per call; runs that can't be acted on\n * (already terminal) are skipped. Returns how many matched and how many the action applied to.\n */\n async bulk(\n action: 'retry' | 'cancel',\n filter: Pick<RunQuery, 'status' | 'tag' | 'workflow' | 'attributes'>,\n opts?: { compensate?: boolean },\n ): Promise<{ matched: number; applied: number }> {\n const runs = await this.gateway.listRuns({ ...filter, limit: 500 });\n let applied = 0;\n for (const r of runs) {\n try {\n if (action === 'retry') await this.gateway.retry(r.id);\n else await this.gateway.cancel(r.id, opts);\n applied += 1;\n } catch {\n // Skip a run that can't take the action (e.g. already terminal) — keep going.\n }\n }\n return { matched: runs.length, applied };\n }\n\n /** Resume a run paused at a `ctx.breakpoint` (the \"continue\" button). */\n continue(runId: string): Promise<RunResult | null> {\n return this.gateway.continue(runId);\n }\n\n /** Re-dispatch a run's stuck `pending` remote steps — recovery for a lost step dispatch. */\n redispatch(runId: string) {\n return this.gateway.redispatchPending(runId);\n }\n\n /**\n * Deliver a `ctx.webhook()` callback: turn an inbound POST (token + body) into the signal the\n * waiting run is parked on. Returns the run result, or `null` if no run waits on that token (a\n * stale/duplicate callback) — a safe no-op the controller maps to 404. Control-plane-only.\n */\n async deliverWebhook(token: string, body: unknown): Promise<RunResult | null> {\n return this.controlPlane().engine.signal(token, body);\n }\n\n /** Side-effect-free read of a value a run published via `ctx.setEvent` (a live query). Control-plane-only. */\n async getEvent(runId: string, key: string): Promise<unknown> {\n return this.controlPlane().engine.getEvent(runId, key);\n }\n\n /** Deliver a validated `ctx.onUpdate` to a run; the validator may reject it (see UpdateResult). Control-plane-only. */\n async update(runId: string, name: string, arg: unknown): Promise<UpdateResult> {\n return this.controlPlane().engine.update(runId, name, arg);\n }\n\n /**\n * Live stream of a run's lifecycle events for SSE. Backed by `gateway.subscribe`, which — on the\n * control plane — receives events from EVERY instance (so a dashboard-only pod tails a run\n * executing on a worker pod), and on a tenant round-trips over the transport. Either way the\n * gateway already filters to `runId`; the dashboard just wires the events through.\n */\n streamRun(runId: string): Observable<{ data: EngineEvent }> {\n return new Observable<{ data: EngineEvent }>((subscriber) => {\n const off = this.gateway.subscribe(runId, (event) => subscriber.next({ data: event }));\n return () => off();\n });\n }\n}\n","/**\n * Injection token for the bound `RunGateway`, owned by `@dudousxd/nestjs-durable` (the nestjs\n * package). The dashboard package does NOT depend on `@dudousxd/nestjs-durable` — its only\n * relevant peer dep is `@dudousxd/nestjs-durable-core` — so we do NOT import the token from\n * there. Instead we share its well-known token by value, the same pattern\n * `@dudousxd/nestjs-durable`'s own `tokens.ts` uses for `CONTEXT_ACCESSOR` (a cross-lib token\n * owned by `@dudousxd/nestjs-context`).\n *\n * `Symbol.for(key)` uses the global symbol registry, so this resolves to the SAME symbol\n * instance as `packages/nestjs/src/tokens.ts`'s `RUN_GATEWAY` without any import. The key MUST\n * stay byte-identical with that export.\n */\nexport const RUN_GATEWAY = Symbol.for('nestjs-durable:run-gateway');\n","import type { RunStatus } from '@dudousxd/nestjs-durable-core';\nimport {\n Body,\n Controller,\n Get,\n Header,\n NotFoundException,\n Param,\n Post,\n Query,\n Sse,\n} from '@nestjs/common';\nimport { parseAttrFilters } from './attr-filter.js';\nimport { DashboardService } from './dashboard.service.js';\n\n/** JSON API consumed by the control-plane SPA. Mounted at `apiBasePath` (set by RouterModule). */\n@Controller()\nexport class DurableApiController {\n constructor(private readonly dashboard: DashboardService) {}\n\n @Get('runs')\n runs(\n @Query('status') status?: RunStatus,\n @Query('workflow') workflow?: string,\n @Query('tag') tag?: string,\n @Query('attr') attr?: string | string[],\n ) {\n return this.dashboard.listRuns({ status, workflow, tag, attributes: parseAttrFilters(attr) });\n }\n\n /** Prometheus-text metrics (runs/steps by outcome, per-workflow counts) for a scrape. */\n @Get('metrics')\n @Header('Content-Type', 'text/plain; version=0.0.4')\n metrics() {\n return this.dashboard.metrics();\n }\n\n /** Per-group worker health (queue backlog + live worker heartbeats) for the Workers panel. */\n @Get('workers')\n workers() {\n return this.dashboard.workerHealth();\n }\n\n /** This deployment's durable role (control plane vs tenant) + tenant name — for the header badge. */\n @Get('topology')\n topology() {\n return this.dashboard.topology();\n }\n\n @Get('runs/:id')\n async run(@Param('id') id: string) {\n const detail = await this.dashboard.getRunDetail(id);\n if (!detail) throw new NotFoundException(`run ${id} not found`);\n return detail;\n }\n\n /** Server-Sent Events stream of a run's live lifecycle events — the dashboard tails it instead\n * of polling. Cross-pod when the transport has a control plane (see DashboardService.streamRun). */\n @Sse('runs/:id/stream')\n stream(@Param('id') id: string) {\n return this.dashboard.streamRun(id);\n }\n\n @Post('runs/:id/retry')\n retry(@Param('id') id: string) {\n return this.dashboard.retry(id);\n }\n\n /** Fix-and-replay: re-run a dead/failed run with a corrected input (a fresh linked run). */\n @Post('runs/:id/retry-with-input')\n retryWithInput(@Param('id') id: string, @Body() body: { input: unknown }) {\n return this.dashboard.retryWithInput(id, body?.input);\n }\n\n /** Bulk retry/cancel every run matching a filter (status / tag / workflow). */\n @Post('bulk/:action')\n bulk(\n @Param('action') action: 'retry' | 'cancel',\n @Query('status') status?: RunStatus,\n @Query('tag') tag?: string,\n @Query('workflow') workflow?: string,\n @Query('attr') attr?: string | string[],\n @Query('compensate') compensate?: string,\n ) {\n return this.dashboard.bulk(\n action === 'cancel' ? 'cancel' : 'retry',\n { status, tag, workflow, attributes: parseAttrFilters(attr) },\n { compensate: compensate === 'true' },\n );\n }\n\n @Post('runs/:id/cancel')\n async cancel(@Param('id') id: string, @Query('compensate') compensate?: string) {\n const result = await this.dashboard.cancel(id, { compensate: compensate === 'true' });\n if (!result) throw new NotFoundException(`run ${id} not found`);\n return result;\n }\n\n @Post('runs/:id/continue')\n async continue(@Param('id') id: string) {\n const result = await this.dashboard.continue(id);\n if (!result) throw new NotFoundException(`run ${id} is not paused at a breakpoint`);\n return result;\n }\n\n /** Re-dispatch a run's stuck `pending` remote steps — recovery for a lost step dispatch. */\n @Post('runs/:id/redispatch')\n async redispatch(@Param('id') id: string) {\n const result = await this.dashboard.redispatch(id);\n if (!result) throw new NotFoundException(`run ${id} not found`);\n return result;\n }\n\n /**\n * Public callback endpoint for `ctx.webhook()`: a third party POSTs here (the url handed to it),\n * and the body resumes the waiting run. NOTE: this is reachable by external systems — protect it\n * by treating the token as a secret (it embeds runId:seq) and/or fronting it with signature\n * verification in your own middleware.\n */\n @Post('webhooks/:token')\n async webhook(@Param('token') token: string, @Body() body: unknown) {\n const result = await this.dashboard.deliverWebhook(token, body);\n if (!result) throw new NotFoundException(`no run is waiting on webhook ${token}`);\n return result;\n }\n\n /** Live query: the latest value a run published for `key` via `ctx.setEvent` (no side effect). */\n @Get('runs/:id/events/:key')\n event(@Param('id') id: string, @Param('key') key: string) {\n return this.dashboard.getEvent(id, key);\n }\n\n /** Deliver a validated `ctx.onUpdate` to a run; the body is the update argument. */\n @Post('runs/:id/updates/:name')\n update(@Param('id') id: string, @Param('name') name: string, @Body() body: unknown) {\n return this.dashboard.update(id, name, body);\n }\n}\n","import type { AttributeFilter, AttributeOp } from '@dudousxd/nestjs-durable-core';\n\nconst ATTR_OPS = new Set<AttributeOp>(['eq', 'ne', 'gt', 'gte', 'lt', 'lte']);\n\n/** Coerce a query-string value: `true`/`false` → boolean, numeric → number, else the raw string. */\nfunction coerce(v: string): string | number | boolean {\n if (v === 'true') return true;\n if (v === 'false') return false;\n if (v !== '' && !Number.isNaN(Number(v))) return Number(v);\n return v;\n}\n\n/**\n * Parse `attr=key:op:value` query params (repeatable, ANDed) into {@link AttributeFilter}s — e.g.\n * `?attr=amount:gte:200&attr=tier:eq:pro`. `op` must be a known operator; a colon in the value is\n * preserved (only the first two colons delimit). Malformed entries are skipped.\n */\nexport function parseAttrFilters(attr?: string | string[]): AttributeFilter[] | undefined {\n if (!attr) return undefined;\n const raw = Array.isArray(attr) ? attr : [attr];\n const filters: AttributeFilter[] = [];\n for (const entry of raw) {\n const [key, op, ...rest] = entry.split(':');\n if (!key || !ATTR_OPS.has(op as AttributeOp) || rest.length === 0) continue;\n filters.push({ key, op: op as AttributeOp, value: coerce(rest.join(':')) });\n }\n return filters.length ? filters : undefined;\n}\n","import { type DynamicModule, Module } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { DashboardService } from './dashboard.service.js';\nimport { DurableApiController } from './durable-api.controller.js';\nimport {\n DASHBOARD_API_PATH,\n DASHBOARD_BASE_PATH,\n DurableUiController,\n} from './durable-ui.controller.js';\n\nexport interface DurableDashboardOptions {\n /**\n * Where the SPA (UI) is served. Default `/durable`. This is a page route — keep it out of an\n * `/api` prefix so it reads as a UI, not an endpoint.\n */\n basePath?: string;\n /**\n * Where the JSON API is mounted (what the SPA fetches). Default `<basePath>/api`. Set it under\n * your app's `/api` prefix — e.g. `/api/durable` — so the API inherits the app's auth/proxy rules\n * while the UI stays at `basePath`.\n */\n apiBasePath?: string;\n}\n\n/** Leading slash, no trailing slash. */\nfunction normalize(path: string): string {\n return `/${path.replace(/^\\/+|\\/+$/g, '')}`;\n}\n\n/** Holds the JSON API controller + its read service, mounted on its own path by `forRoot`. */\n@Module({\n controllers: [DurableApiController],\n providers: [DashboardService],\n exports: [DashboardService],\n})\nexport class DurableApiModule {}\n\n/**\n * Mounts the control plane: the bundled React SPA at `basePath` and its JSON API at `apiBasePath`\n * (default `<basePath>/api`). Import via `DurableDashboardModule.forRoot(...)` alongside\n * `DurableModule` (global), so it resolves the engine and store. Front the routes with a guard.\n */\n@Module({})\nexport class DurableDashboardModule {\n static forRoot(options: DurableDashboardOptions = {}): DynamicModule {\n const basePath = normalize(options.basePath ?? '/durable');\n const apiBasePath = normalize(options.apiBasePath ?? `${basePath}/api`);\n return {\n module: DurableDashboardModule,\n imports: [\n DurableApiModule,\n RouterModule.register([\n { path: basePath, module: DurableDashboardModule }, // the UI controller below\n { path: apiBasePath, module: DurableApiModule },\n ]),\n ],\n controllers: [DurableUiController],\n providers: [\n { provide: DASHBOARD_BASE_PATH, useValue: basePath },\n { provide: DASHBOARD_API_PATH, useValue: apiBasePath },\n ],\n // Re-export the API module so its DashboardService reaches importers (e.g. flip's own controllers).\n exports: [DurableApiModule],\n };\n }\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { basename, extname, join, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n Controller,\n Get,\n Header,\n Inject,\n NotFoundException,\n Param,\n StreamableFile,\n} from '@nestjs/common';\n\n/** DI token carrying the UI mount base (e.g. `/durable`). */\nexport const DASHBOARD_BASE_PATH = Symbol('DASHBOARD_BASE_PATH');\n/** DI token carrying the JSON API base the SPA fetches from (e.g. `/api/durable`). */\nexport const DASHBOARD_API_PATH = Symbol('DASHBOARD_API_PATH');\n\n/** The base the SPA bundle was built with (Vite `base`); rewritten to the configured base at serve time. */\nconst BUILD_BASE = '/durable';\n\n/** dist/server/durable-ui.controller.js -> ../spa (the Vite build output). */\nfunction spaDir(): string {\n return fileURLToPath(new URL('../spa', import.meta.url));\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.map': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.json': 'application/json; charset=utf-8',\n '.woff2': 'font/woff2',\n '.ico': 'image/x-icon',\n};\n\n/**\n * Serves the bundled control-plane SPA at the configured base (+ hashed assets at `<base>/assets`).\n * The path comes from `RouterModule` (set by `DurableDashboardModule.forRoot({ basePath })`), so the\n * controller routes are relative.\n */\n@Controller()\nexport class DurableUiController {\n private readonly dir = spaDir();\n\n constructor(\n @Inject(DASHBOARD_BASE_PATH) private readonly basePath: string,\n @Inject(DASHBOARD_API_PATH) private readonly apiBasePath: string,\n ) {}\n\n // index.html references hash-named bundles, so it MUST NOT be cached (stale bundle = the\n // classic \"stuck loading after a deploy\"). The hashed assets below are immutable.\n @Get()\n @Header('Content-Type', 'text/html; charset=utf-8')\n @Header('Cache-Control', 'no-store, must-revalidate')\n index(): string {\n const indexPath = join(this.dir, 'index.html');\n if (!existsSync(indexPath)) {\n throw new NotFoundException('Dashboard is not built. Run the package build.');\n }\n // The bundle was built with Vite base `/durable/`; rewrite asset URLs to the configured base so\n // the SPA loads from `<base>/assets` wherever it's mounted, and tell the client its API base.\n const html = readFileSync(indexPath, 'utf8').replaceAll(\n `=\"${BUILD_BASE}/`,\n `=\"${this.basePath}/`,\n );\n // __DURABLE_BASE__ = where assets load; __DURABLE_API__ = where the SPA fetches the JSON API.\n const inject = `<script>window.__DURABLE_BASE__='${this.basePath}';window.__DURABLE_API__='${this.apiBasePath}';</script>`;\n return html.includes('</head>') ? html.replace('</head>', `${inject}</head>`) : inject + html;\n }\n\n @Get('assets/:file')\n @Header('Cache-Control', 'public, max-age=31536000, immutable')\n asset(@Param('file') file: string): StreamableFile {\n const safe = basename(file);\n if (safe !== file) throw new NotFoundException();\n const root = resolve(this.dir, 'assets');\n const assetPath = resolve(root, safe);\n if (!assetPath.startsWith(root + sep) || !existsSync(assetPath)) {\n throw new NotFoundException();\n }\n const type = CONTENT_TYPES[extname(safe)] ?? 'application/octet-stream';\n return new StreamableFile(readFileSync(assetPath), { type });\n }\n}\n"],"mappings":"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","names":["STATE_STORE_CANONICAL","WorkflowEngine","collectMetrics","Inject","Injectable","Optional","Observable","RUN_GATEWAY","Symbol","for","DashboardService","metricsCollector","gateway","store","engine","collectMetrics","undefined","controlPlane","Error","metrics","cap","pending","running","dead","Promise","all","listRuns","status","limit","gauges","length","health","workerHealth","push","h","group","depth","liveWorkers","prometheus","join","query","topology","getRunDetail","runId","retryWithInput","input","retry","cancel","opts","bulk","action","filter","runs","applied","r","id","matched","continue","redispatch","redispatchPending","deliverWebhook","token","body","signal","getEvent","key","update","name","arg","streamRun","Observable","subscriber","off","subscribe","event","next","data","Body","Controller","Get","Header","NotFoundException","Param","Post","Query","Sse","ATTR_OPS","Set","coerce","v","Number","isNaN","parseAttrFilters","attr","undefined","raw","Array","isArray","filters","entry","key","op","rest","split","has","length","push","value","join","DurableApiController","dashboard","runs","status","workflow","tag","attr","listRuns","attributes","parseAttrFilters","metrics","workers","workerHealth","topology","run","id","detail","getRunDetail","NotFoundException","stream","streamRun","retry","retryWithInput","body","input","bulk","action","compensate","cancel","result","continue","redispatch","webhook","token","deliverWebhook","event","key","getEvent","update","name","Module","RouterModule","existsSync","readFileSync","basename","extname","join","resolve","sep","fileURLToPath","Controller","Get","Header","Inject","NotFoundException","Param","StreamableFile","DASHBOARD_BASE_PATH","Symbol","DASHBOARD_API_PATH","BUILD_BASE","spaDir","fileURLToPath","URL","url","CONTENT_TYPES","DurableUiController","dir","basePath","apiBasePath","index","indexPath","join","existsSync","NotFoundException","html","readFileSync","replaceAll","inject","includes","replace","asset","file","safe","basename","root","resolve","assetPath","startsWith","sep","type","extname","StreamableFile","normalize","path","replace","DurableApiModule","controllers","DurableApiController","providers","DashboardService","exports","DurableDashboardModule","forRoot","options","basePath","apiBasePath","module","imports","RouterModule","register","DurableUiController","provide","DASHBOARD_BASE_PATH","useValue","DASHBOARD_API_PATH"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/nestjs-durable-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Embedded control plane (runs, step timeline, retry/cancel) for nestjs-durable — bundled React SPA served by NestJS",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Davide Carvalho",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"tsup": "8.3.5",
|
|
57
57
|
"typescript": "5.9.3",
|
|
58
58
|
"vite": "5.4.21",
|
|
59
|
-
"@dudousxd/nestjs-durable-core": "^0.
|
|
59
|
+
"@dudousxd/nestjs-durable-core": "^0.50.0"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "vite build && tsup && tsc -p tsconfig.client.json",
|