@dudousxd/nestjs-durable-dashboard 0.29.7 → 0.31.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 +44 -14
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +47 -13
- package/dist/server/index.d.ts +47 -13
- package/dist/server/index.js +44 -14
- 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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"}
|
package/dist/server/index.cjs
CHANGED
|
@@ -555,6 +555,7 @@ DurableApiController = _ts_decorate2([
|
|
|
555
555
|
], DurableApiController);
|
|
556
556
|
|
|
557
557
|
// src/server/durable-dashboard.module.ts
|
|
558
|
+
var import_reflect_metadata = require("reflect-metadata");
|
|
558
559
|
var import_common4 = require("@nestjs/common");
|
|
559
560
|
var import_core = require("@nestjs/core");
|
|
560
561
|
|
|
@@ -669,27 +670,42 @@ function _ts_decorate4(decorators, target, key, desc) {
|
|
|
669
670
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
670
671
|
}
|
|
671
672
|
__name(_ts_decorate4, "_ts_decorate");
|
|
673
|
+
var GUARDS_METADATA = "__guards__";
|
|
672
674
|
function normalize(path) {
|
|
673
675
|
return `/${path.replace(/^\/+|\/+$/g, "")}`;
|
|
674
676
|
}
|
|
675
677
|
__name(normalize, "normalize");
|
|
676
|
-
|
|
678
|
+
function stampGuards(guards, ...controllers) {
|
|
679
|
+
for (const controller of controllers) {
|
|
680
|
+
Reflect.defineMetadata(GUARDS_METADATA, guards ?? [], controller);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
__name(stampGuards, "stampGuards");
|
|
684
|
+
var DurableApiModule = class _DurableApiModule {
|
|
677
685
|
static {
|
|
678
686
|
__name(this, "DurableApiModule");
|
|
679
687
|
}
|
|
688
|
+
static register(options) {
|
|
689
|
+
return {
|
|
690
|
+
module: _DurableApiModule,
|
|
691
|
+
imports: [
|
|
692
|
+
...options.imports ?? []
|
|
693
|
+
],
|
|
694
|
+
controllers: [
|
|
695
|
+
DurableApiController
|
|
696
|
+
],
|
|
697
|
+
providers: [
|
|
698
|
+
DashboardService,
|
|
699
|
+
...options.guards ?? []
|
|
700
|
+
],
|
|
701
|
+
exports: [
|
|
702
|
+
DashboardService
|
|
703
|
+
]
|
|
704
|
+
};
|
|
705
|
+
}
|
|
680
706
|
};
|
|
681
707
|
DurableApiModule = _ts_decorate4([
|
|
682
|
-
(0, import_common4.Module)({
|
|
683
|
-
controllers: [
|
|
684
|
-
DurableApiController
|
|
685
|
-
],
|
|
686
|
-
providers: [
|
|
687
|
-
DashboardService
|
|
688
|
-
],
|
|
689
|
-
exports: [
|
|
690
|
-
DashboardService
|
|
691
|
-
]
|
|
692
|
-
})
|
|
708
|
+
(0, import_common4.Module)({})
|
|
693
709
|
], DurableApiModule);
|
|
694
710
|
var DurableDashboardModule = class _DurableDashboardModule {
|
|
695
711
|
static {
|
|
@@ -698,10 +714,22 @@ var DurableDashboardModule = class _DurableDashboardModule {
|
|
|
698
714
|
static forRoot(options = {}) {
|
|
699
715
|
const basePath = normalize(options.basePath ?? "/durable");
|
|
700
716
|
const apiBasePath = normalize(options.apiBasePath ?? `${basePath}/api`);
|
|
717
|
+
stampGuards(options.guards, DurableApiController, DurableUiController);
|
|
701
718
|
return {
|
|
702
719
|
module: _DurableDashboardModule,
|
|
703
720
|
imports: [
|
|
704
|
-
|
|
721
|
+
...options.imports ?? [],
|
|
722
|
+
// Guards + host imports must reach the API controller's HOST module — enhancers resolve
|
|
723
|
+
// from their controller's own module, never from a parent (see DurableApiModule.register).
|
|
724
|
+
// Spread-only-when-set: exactOptionalPropertyTypes rejects an explicit `undefined`.
|
|
725
|
+
DurableApiModule.register({
|
|
726
|
+
...options.imports ? {
|
|
727
|
+
imports: options.imports
|
|
728
|
+
} : {},
|
|
729
|
+
...options.guards ? {
|
|
730
|
+
guards: options.guards
|
|
731
|
+
} : {}
|
|
732
|
+
}),
|
|
705
733
|
import_core.RouterModule.register([
|
|
706
734
|
{
|
|
707
735
|
path: basePath,
|
|
@@ -724,7 +752,9 @@ var DurableDashboardModule = class _DurableDashboardModule {
|
|
|
724
752
|
{
|
|
725
753
|
provide: DASHBOARD_API_PATH,
|
|
726
754
|
useValue: apiBasePath
|
|
727
|
-
}
|
|
755
|
+
},
|
|
756
|
+
// DurableUiController is hosted HERE, so its guards DI-instantiate from this module.
|
|
757
|
+
...options.guards ?? []
|
|
728
758
|
],
|
|
729
759
|
// Re-export the API module so its DashboardService reaches importers (e.g. flip's own controllers).
|
|
730
760
|
exports: [
|
|
@@ -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":"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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"]}
|
|
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 'reflect-metadata';\nimport { type CanActivate, type DynamicModule, Module, type Type } 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\n/**\n * `@nestjs/common`'s own `GUARDS_METADATA` key, INLINED rather than deep-imported from\n * '@nestjs/common/constants' — that subpath has no extension and a strict ESM resolver (which the\n * built dual ESM/CJS output of this package is loaded under) 404s on it. A drift spec imports the\n * real constant (via the resolvable `'@nestjs/common/constants.js'` subpath) and asserts this literal\n * stays byte-identical to it. Mirrors `@dudousxd/nestjs-agent-dashboard`'s `agent-dashboard.module.ts`.\n */\nconst GUARDS_METADATA = '__guards__';\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 * Guard classes fronting BOTH dashboard controllers (the SPA at `basePath` and its JSON API at\n * `apiBasePath`). Stamped onto each controller via `@nestjs/common`'s own `@UseGuards` metadata key\n * — REPLACE semantics, so a second `forRoot(...)` call overwrites (not appends to) whatever a prior\n * call stamped, same as re-applying `@UseGuards` by hand. Omit to leave the routes unguarded (the\n * host fronts them another way, e.g. a global guard or reverse-proxy auth).\n *\n * A guard's own DEPENDENCIES resolve from this module's `imports` (see {@link imports}) — the\n * dashboard module has no application context of its own to pull them from otherwise.\n *\n * Auth reality for the two mount points: `apiBasePath` is fetched by the SPA's own JS (an XHR/fetch\n * from the same origin), so a guard reading a bearer/`Authorization` header there works exactly like\n * any other API route. `basePath` (the UI shell, `DurableUiController.index()`) is a full-page\n * navigation — the BROWSER issues that GET directly (typing the URL, a bookmark, a link click), so\n * there is no custom header to read: only whatever ambient credential the browser sends by default\n * (a cookie, or nothing) reaches the guard there. A guard that only checks an `Authorization` header\n * will 403 the page shell while the API calls it makes right after would have passed — put session\n * auth behind a cookie (or front the UI mount with your reverse proxy / SSO instead) if you need the\n * shell itself gated.\n */\n guards?: Type<CanActivate>[];\n /**\n * Extra `imports` merged into the dashboard's dynamic module — the DI resolution path for a class\n * passed to {@link guards} (or any other provider the controllers need reachable). Typically the\n * host's own auth module, e.g. `imports: [AuthModule]` alongside `guards: [JwtAuthGuard]`.\n */\n imports?: DynamicModule['imports'];\n}\n\n/** Leading slash, no trailing slash. */\nfunction normalize(path: string): string {\n return `/${path.replace(/^\\/+|\\/+$/g, '')}`;\n}\n\n/** Stamp (or clear) `@UseGuards`-equivalent metadata on the dashboard controllers — REPLACE, not append. */\nfunction stampGuards(guards: Type<CanActivate>[] | undefined, ...controllers: Type[]): void {\n for (const controller of controllers) {\n Reflect.defineMetadata(GUARDS_METADATA, guards ?? [], controller);\n }\n}\n\n/**\n * Holds the JSON API controller + its read service, mounted on its own path by `forRoot`. Dynamic:\n * guards are DI-instantiated by the CONTROLLER's host module, so this module — not the outer wrapper\n * — must carry the guard classes as providers plus the host's `imports` that resolve their\n * dependencies. A static module here made `guards: [SomeGuardWithDeps]` fail at boot with \"Nest can't\n * resolve dependencies ... in the DurableApiModule context\" even when the host passed the right\n * `imports` to `forRoot` (mirrors `@dudousxd/nestjs-agent-dashboard`'s `AgentApiModule`).\n */\n@Module({})\nexport class DurableApiModule {\n static register(options: {\n imports?: DynamicModule['imports'];\n guards?: Type<CanActivate>[];\n }): DynamicModule {\n return {\n module: DurableApiModule,\n imports: [...(options.imports ?? [])],\n controllers: [DurableApiController],\n providers: [DashboardService, ...(options.guards ?? [])],\n exports: [DashboardService],\n };\n }\n}\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 the first-class\n * `guards` option (plus `imports` for the guards' own dependencies) — see\n * {@link DurableDashboardOptions.guards}.\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 stampGuards(options.guards, DurableApiController, DurableUiController);\n return {\n module: DurableDashboardModule,\n imports: [\n ...(options.imports ?? []),\n // Guards + host imports must reach the API controller's HOST module — enhancers resolve\n // from their controller's own module, never from a parent (see DurableApiModule.register).\n // Spread-only-when-set: exactOptionalPropertyTypes rejects an explicit `undefined`.\n DurableApiModule.register({\n ...(options.imports ? { imports: options.imports } : {}),\n ...(options.guards ? { guards: options.guards } : {}),\n }),\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 // DurableUiController is hosted HERE, so its guards DI-instantiate from this module.\n ...(options.guards ?? []),\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","GUARDS_METADATA","normalize","path","replace","stampGuards","guards","controllers","controller","Reflect","defineMetadata","DurableApiModule","register","options","module","imports","DurableApiController","providers","DashboardService","exports","DurableDashboardModule","forRoot","basePath","apiBasePath","DurableUiController","RouterModule","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
|
-
import { DynamicModule, StreamableFile } from '@nestjs/common';
|
|
6
|
+
import { DynamicModule, Type, CanActivate, 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<{
|
|
@@ -160,14 +154,54 @@ interface DurableDashboardOptions {
|
|
|
160
154
|
* while the UI stays at `basePath`.
|
|
161
155
|
*/
|
|
162
156
|
apiBasePath?: string;
|
|
157
|
+
/**
|
|
158
|
+
* Guard classes fronting BOTH dashboard controllers (the SPA at `basePath` and its JSON API at
|
|
159
|
+
* `apiBasePath`). Stamped onto each controller via `@nestjs/common`'s own `@UseGuards` metadata key
|
|
160
|
+
* — REPLACE semantics, so a second `forRoot(...)` call overwrites (not appends to) whatever a prior
|
|
161
|
+
* call stamped, same as re-applying `@UseGuards` by hand. Omit to leave the routes unguarded (the
|
|
162
|
+
* host fronts them another way, e.g. a global guard or reverse-proxy auth).
|
|
163
|
+
*
|
|
164
|
+
* A guard's own DEPENDENCIES resolve from this module's `imports` (see {@link imports}) — the
|
|
165
|
+
* dashboard module has no application context of its own to pull them from otherwise.
|
|
166
|
+
*
|
|
167
|
+
* Auth reality for the two mount points: `apiBasePath` is fetched by the SPA's own JS (an XHR/fetch
|
|
168
|
+
* from the same origin), so a guard reading a bearer/`Authorization` header there works exactly like
|
|
169
|
+
* any other API route. `basePath` (the UI shell, `DurableUiController.index()`) is a full-page
|
|
170
|
+
* navigation — the BROWSER issues that GET directly (typing the URL, a bookmark, a link click), so
|
|
171
|
+
* there is no custom header to read: only whatever ambient credential the browser sends by default
|
|
172
|
+
* (a cookie, or nothing) reaches the guard there. A guard that only checks an `Authorization` header
|
|
173
|
+
* will 403 the page shell while the API calls it makes right after would have passed — put session
|
|
174
|
+
* auth behind a cookie (or front the UI mount with your reverse proxy / SSO instead) if you need the
|
|
175
|
+
* shell itself gated.
|
|
176
|
+
*/
|
|
177
|
+
guards?: Type<CanActivate>[];
|
|
178
|
+
/**
|
|
179
|
+
* Extra `imports` merged into the dashboard's dynamic module — the DI resolution path for a class
|
|
180
|
+
* passed to {@link guards} (or any other provider the controllers need reachable). Typically the
|
|
181
|
+
* host's own auth module, e.g. `imports: [AuthModule]` alongside `guards: [JwtAuthGuard]`.
|
|
182
|
+
*/
|
|
183
|
+
imports?: DynamicModule['imports'];
|
|
163
184
|
}
|
|
164
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Holds the JSON API controller + its read service, mounted on its own path by `forRoot`. Dynamic:
|
|
187
|
+
* guards are DI-instantiated by the CONTROLLER's host module, so this module — not the outer wrapper
|
|
188
|
+
* — must carry the guard classes as providers plus the host's `imports` that resolve their
|
|
189
|
+
* dependencies. A static module here made `guards: [SomeGuardWithDeps]` fail at boot with "Nest can't
|
|
190
|
+
* resolve dependencies ... in the DurableApiModule context" even when the host passed the right
|
|
191
|
+
* `imports` to `forRoot` (mirrors `@dudousxd/nestjs-agent-dashboard`'s `AgentApiModule`).
|
|
192
|
+
*/
|
|
165
193
|
declare class DurableApiModule {
|
|
194
|
+
static register(options: {
|
|
195
|
+
imports?: DynamicModule['imports'];
|
|
196
|
+
guards?: Type<CanActivate>[];
|
|
197
|
+
}): DynamicModule;
|
|
166
198
|
}
|
|
167
199
|
/**
|
|
168
200
|
* Mounts the control plane: the bundled React SPA at `basePath` and its JSON API at `apiBasePath`
|
|
169
201
|
* (default `<basePath>/api`). Import via `DurableDashboardModule.forRoot(...)` alongside
|
|
170
|
-
* `DurableModule` (global), so it resolves the engine and store. Front the routes with
|
|
202
|
+
* `DurableModule` (global), so it resolves the engine and store. Front the routes with the first-class
|
|
203
|
+
* `guards` option (plus `imports` for the guards' own dependencies) — see
|
|
204
|
+
* {@link DurableDashboardOptions.guards}.
|
|
171
205
|
*/
|
|
172
206
|
declare class DurableDashboardModule {
|
|
173
207
|
static forRoot(options?: DurableDashboardOptions): DynamicModule;
|
|
@@ -191,4 +225,4 @@ declare class DurableUiController {
|
|
|
191
225
|
asset(file: string): StreamableFile;
|
|
192
226
|
}
|
|
193
227
|
|
|
194
|
-
export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, DashboardService, DurableApiController, DurableApiModule, DurableDashboardModule, type DurableDashboardOptions, DurableUiController
|
|
228
|
+
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
|
-
import { DynamicModule, StreamableFile } from '@nestjs/common';
|
|
6
|
+
import { DynamicModule, Type, CanActivate, 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<{
|
|
@@ -160,14 +154,54 @@ interface DurableDashboardOptions {
|
|
|
160
154
|
* while the UI stays at `basePath`.
|
|
161
155
|
*/
|
|
162
156
|
apiBasePath?: string;
|
|
157
|
+
/**
|
|
158
|
+
* Guard classes fronting BOTH dashboard controllers (the SPA at `basePath` and its JSON API at
|
|
159
|
+
* `apiBasePath`). Stamped onto each controller via `@nestjs/common`'s own `@UseGuards` metadata key
|
|
160
|
+
* — REPLACE semantics, so a second `forRoot(...)` call overwrites (not appends to) whatever a prior
|
|
161
|
+
* call stamped, same as re-applying `@UseGuards` by hand. Omit to leave the routes unguarded (the
|
|
162
|
+
* host fronts them another way, e.g. a global guard or reverse-proxy auth).
|
|
163
|
+
*
|
|
164
|
+
* A guard's own DEPENDENCIES resolve from this module's `imports` (see {@link imports}) — the
|
|
165
|
+
* dashboard module has no application context of its own to pull them from otherwise.
|
|
166
|
+
*
|
|
167
|
+
* Auth reality for the two mount points: `apiBasePath` is fetched by the SPA's own JS (an XHR/fetch
|
|
168
|
+
* from the same origin), so a guard reading a bearer/`Authorization` header there works exactly like
|
|
169
|
+
* any other API route. `basePath` (the UI shell, `DurableUiController.index()`) is a full-page
|
|
170
|
+
* navigation — the BROWSER issues that GET directly (typing the URL, a bookmark, a link click), so
|
|
171
|
+
* there is no custom header to read: only whatever ambient credential the browser sends by default
|
|
172
|
+
* (a cookie, or nothing) reaches the guard there. A guard that only checks an `Authorization` header
|
|
173
|
+
* will 403 the page shell while the API calls it makes right after would have passed — put session
|
|
174
|
+
* auth behind a cookie (or front the UI mount with your reverse proxy / SSO instead) if you need the
|
|
175
|
+
* shell itself gated.
|
|
176
|
+
*/
|
|
177
|
+
guards?: Type<CanActivate>[];
|
|
178
|
+
/**
|
|
179
|
+
* Extra `imports` merged into the dashboard's dynamic module — the DI resolution path for a class
|
|
180
|
+
* passed to {@link guards} (or any other provider the controllers need reachable). Typically the
|
|
181
|
+
* host's own auth module, e.g. `imports: [AuthModule]` alongside `guards: [JwtAuthGuard]`.
|
|
182
|
+
*/
|
|
183
|
+
imports?: DynamicModule['imports'];
|
|
163
184
|
}
|
|
164
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Holds the JSON API controller + its read service, mounted on its own path by `forRoot`. Dynamic:
|
|
187
|
+
* guards are DI-instantiated by the CONTROLLER's host module, so this module — not the outer wrapper
|
|
188
|
+
* — must carry the guard classes as providers plus the host's `imports` that resolve their
|
|
189
|
+
* dependencies. A static module here made `guards: [SomeGuardWithDeps]` fail at boot with "Nest can't
|
|
190
|
+
* resolve dependencies ... in the DurableApiModule context" even when the host passed the right
|
|
191
|
+
* `imports` to `forRoot` (mirrors `@dudousxd/nestjs-agent-dashboard`'s `AgentApiModule`).
|
|
192
|
+
*/
|
|
165
193
|
declare class DurableApiModule {
|
|
194
|
+
static register(options: {
|
|
195
|
+
imports?: DynamicModule['imports'];
|
|
196
|
+
guards?: Type<CanActivate>[];
|
|
197
|
+
}): DynamicModule;
|
|
166
198
|
}
|
|
167
199
|
/**
|
|
168
200
|
* Mounts the control plane: the bundled React SPA at `basePath` and its JSON API at `apiBasePath`
|
|
169
201
|
* (default `<basePath>/api`). Import via `DurableDashboardModule.forRoot(...)` alongside
|
|
170
|
-
* `DurableModule` (global), so it resolves the engine and store. Front the routes with
|
|
202
|
+
* `DurableModule` (global), so it resolves the engine and store. Front the routes with the first-class
|
|
203
|
+
* `guards` option (plus `imports` for the guards' own dependencies) — see
|
|
204
|
+
* {@link DurableDashboardOptions.guards}.
|
|
171
205
|
*/
|
|
172
206
|
declare class DurableDashboardModule {
|
|
173
207
|
static forRoot(options?: DurableDashboardOptions): DynamicModule;
|
|
@@ -191,4 +225,4 @@ declare class DurableUiController {
|
|
|
191
225
|
asset(file: string): StreamableFile;
|
|
192
226
|
}
|
|
193
227
|
|
|
194
|
-
export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, DashboardService, DurableApiController, DurableApiModule, DurableDashboardModule, type DurableDashboardOptions, DurableUiController
|
|
228
|
+
export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, DashboardService, DurableApiController, DurableApiModule, DurableDashboardModule, type DurableDashboardOptions, DurableUiController };
|
package/dist/server/index.js
CHANGED
|
@@ -524,6 +524,7 @@ DurableApiController = _ts_decorate2([
|
|
|
524
524
|
], DurableApiController);
|
|
525
525
|
|
|
526
526
|
// src/server/durable-dashboard.module.ts
|
|
527
|
+
import "reflect-metadata";
|
|
527
528
|
import { Module } from "@nestjs/common";
|
|
528
529
|
import { RouterModule } from "@nestjs/core";
|
|
529
530
|
|
|
@@ -638,27 +639,42 @@ function _ts_decorate4(decorators, target, key, desc) {
|
|
|
638
639
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
639
640
|
}
|
|
640
641
|
__name(_ts_decorate4, "_ts_decorate");
|
|
642
|
+
var GUARDS_METADATA = "__guards__";
|
|
641
643
|
function normalize(path) {
|
|
642
644
|
return `/${path.replace(/^\/+|\/+$/g, "")}`;
|
|
643
645
|
}
|
|
644
646
|
__name(normalize, "normalize");
|
|
645
|
-
|
|
647
|
+
function stampGuards(guards, ...controllers) {
|
|
648
|
+
for (const controller of controllers) {
|
|
649
|
+
Reflect.defineMetadata(GUARDS_METADATA, guards ?? [], controller);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
__name(stampGuards, "stampGuards");
|
|
653
|
+
var DurableApiModule = class _DurableApiModule {
|
|
646
654
|
static {
|
|
647
655
|
__name(this, "DurableApiModule");
|
|
648
656
|
}
|
|
657
|
+
static register(options) {
|
|
658
|
+
return {
|
|
659
|
+
module: _DurableApiModule,
|
|
660
|
+
imports: [
|
|
661
|
+
...options.imports ?? []
|
|
662
|
+
],
|
|
663
|
+
controllers: [
|
|
664
|
+
DurableApiController
|
|
665
|
+
],
|
|
666
|
+
providers: [
|
|
667
|
+
DashboardService,
|
|
668
|
+
...options.guards ?? []
|
|
669
|
+
],
|
|
670
|
+
exports: [
|
|
671
|
+
DashboardService
|
|
672
|
+
]
|
|
673
|
+
};
|
|
674
|
+
}
|
|
649
675
|
};
|
|
650
676
|
DurableApiModule = _ts_decorate4([
|
|
651
|
-
Module({
|
|
652
|
-
controllers: [
|
|
653
|
-
DurableApiController
|
|
654
|
-
],
|
|
655
|
-
providers: [
|
|
656
|
-
DashboardService
|
|
657
|
-
],
|
|
658
|
-
exports: [
|
|
659
|
-
DashboardService
|
|
660
|
-
]
|
|
661
|
-
})
|
|
677
|
+
Module({})
|
|
662
678
|
], DurableApiModule);
|
|
663
679
|
var DurableDashboardModule = class _DurableDashboardModule {
|
|
664
680
|
static {
|
|
@@ -667,10 +683,22 @@ var DurableDashboardModule = class _DurableDashboardModule {
|
|
|
667
683
|
static forRoot(options = {}) {
|
|
668
684
|
const basePath = normalize(options.basePath ?? "/durable");
|
|
669
685
|
const apiBasePath = normalize(options.apiBasePath ?? `${basePath}/api`);
|
|
686
|
+
stampGuards(options.guards, DurableApiController, DurableUiController);
|
|
670
687
|
return {
|
|
671
688
|
module: _DurableDashboardModule,
|
|
672
689
|
imports: [
|
|
673
|
-
|
|
690
|
+
...options.imports ?? [],
|
|
691
|
+
// Guards + host imports must reach the API controller's HOST module — enhancers resolve
|
|
692
|
+
// from their controller's own module, never from a parent (see DurableApiModule.register).
|
|
693
|
+
// Spread-only-when-set: exactOptionalPropertyTypes rejects an explicit `undefined`.
|
|
694
|
+
DurableApiModule.register({
|
|
695
|
+
...options.imports ? {
|
|
696
|
+
imports: options.imports
|
|
697
|
+
} : {},
|
|
698
|
+
...options.guards ? {
|
|
699
|
+
guards: options.guards
|
|
700
|
+
} : {}
|
|
701
|
+
}),
|
|
674
702
|
RouterModule.register([
|
|
675
703
|
{
|
|
676
704
|
path: basePath,
|
|
@@ -693,7 +721,9 @@ var DurableDashboardModule = class _DurableDashboardModule {
|
|
|
693
721
|
{
|
|
694
722
|
provide: DASHBOARD_API_PATH,
|
|
695
723
|
useValue: apiBasePath
|
|
696
|
-
}
|
|
724
|
+
},
|
|
725
|
+
// DurableUiController is hosted HERE, so its guards DI-instantiate from this module.
|
|
726
|
+
...options.guards ?? []
|
|
697
727
|
],
|
|
698
728
|
// Re-export the API module so its DashboardService reaches importers (e.g. flip's own controllers).
|
|
699
729
|
exports: [
|
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 'reflect-metadata';\nimport { type CanActivate, type DynamicModule, Module, type Type } 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\n/**\n * `@nestjs/common`'s own `GUARDS_METADATA` key, INLINED rather than deep-imported from\n * '@nestjs/common/constants' — that subpath has no extension and a strict ESM resolver (which the\n * built dual ESM/CJS output of this package is loaded under) 404s on it. A drift spec imports the\n * real constant (via the resolvable `'@nestjs/common/constants.js'` subpath) and asserts this literal\n * stays byte-identical to it. Mirrors `@dudousxd/nestjs-agent-dashboard`'s `agent-dashboard.module.ts`.\n */\nconst GUARDS_METADATA = '__guards__';\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 * Guard classes fronting BOTH dashboard controllers (the SPA at `basePath` and its JSON API at\n * `apiBasePath`). Stamped onto each controller via `@nestjs/common`'s own `@UseGuards` metadata key\n * — REPLACE semantics, so a second `forRoot(...)` call overwrites (not appends to) whatever a prior\n * call stamped, same as re-applying `@UseGuards` by hand. Omit to leave the routes unguarded (the\n * host fronts them another way, e.g. a global guard or reverse-proxy auth).\n *\n * A guard's own DEPENDENCIES resolve from this module's `imports` (see {@link imports}) — the\n * dashboard module has no application context of its own to pull them from otherwise.\n *\n * Auth reality for the two mount points: `apiBasePath` is fetched by the SPA's own JS (an XHR/fetch\n * from the same origin), so a guard reading a bearer/`Authorization` header there works exactly like\n * any other API route. `basePath` (the UI shell, `DurableUiController.index()`) is a full-page\n * navigation — the BROWSER issues that GET directly (typing the URL, a bookmark, a link click), so\n * there is no custom header to read: only whatever ambient credential the browser sends by default\n * (a cookie, or nothing) reaches the guard there. A guard that only checks an `Authorization` header\n * will 403 the page shell while the API calls it makes right after would have passed — put session\n * auth behind a cookie (or front the UI mount with your reverse proxy / SSO instead) if you need the\n * shell itself gated.\n */\n guards?: Type<CanActivate>[];\n /**\n * Extra `imports` merged into the dashboard's dynamic module — the DI resolution path for a class\n * passed to {@link guards} (or any other provider the controllers need reachable). Typically the\n * host's own auth module, e.g. `imports: [AuthModule]` alongside `guards: [JwtAuthGuard]`.\n */\n imports?: DynamicModule['imports'];\n}\n\n/** Leading slash, no trailing slash. */\nfunction normalize(path: string): string {\n return `/${path.replace(/^\\/+|\\/+$/g, '')}`;\n}\n\n/** Stamp (or clear) `@UseGuards`-equivalent metadata on the dashboard controllers — REPLACE, not append. */\nfunction stampGuards(guards: Type<CanActivate>[] | undefined, ...controllers: Type[]): void {\n for (const controller of controllers) {\n Reflect.defineMetadata(GUARDS_METADATA, guards ?? [], controller);\n }\n}\n\n/**\n * Holds the JSON API controller + its read service, mounted on its own path by `forRoot`. Dynamic:\n * guards are DI-instantiated by the CONTROLLER's host module, so this module — not the outer wrapper\n * — must carry the guard classes as providers plus the host's `imports` that resolve their\n * dependencies. A static module here made `guards: [SomeGuardWithDeps]` fail at boot with \"Nest can't\n * resolve dependencies ... in the DurableApiModule context\" even when the host passed the right\n * `imports` to `forRoot` (mirrors `@dudousxd/nestjs-agent-dashboard`'s `AgentApiModule`).\n */\n@Module({})\nexport class DurableApiModule {\n static register(options: {\n imports?: DynamicModule['imports'];\n guards?: Type<CanActivate>[];\n }): DynamicModule {\n return {\n module: DurableApiModule,\n imports: [...(options.imports ?? [])],\n controllers: [DurableApiController],\n providers: [DashboardService, ...(options.guards ?? [])],\n exports: [DashboardService],\n };\n }\n}\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 the first-class\n * `guards` option (plus `imports` for the guards' own dependencies) — see\n * {@link DurableDashboardOptions.guards}.\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 stampGuards(options.guards, DurableApiController, DurableUiController);\n return {\n module: DurableDashboardModule,\n imports: [\n ...(options.imports ?? []),\n // Guards + host imports must reach the API controller's HOST module — enhancers resolve\n // from their controller's own module, never from a parent (see DurableApiModule.register).\n // Spread-only-when-set: exactOptionalPropertyTypes rejects an explicit `undefined`.\n DurableApiModule.register({\n ...(options.imports ? { imports: options.imports } : {}),\n ...(options.guards ? { guards: options.guards } : {}),\n }),\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 // DurableUiController is hosted HERE, so its guards DI-instantiate from this module.\n ...(options.guards ?? []),\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","GUARDS_METADATA","normalize","path","replace","stampGuards","guards","controllers","controller","Reflect","defineMetadata","DurableApiModule","register","options","module","imports","DurableApiController","providers","DashboardService","exports","DurableDashboardModule","forRoot","basePath","apiBasePath","DurableUiController","RouterModule","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.31.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.54.0"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "vite build && tsup && tsc -p tsconfig.client.json",
|