@dudousxd/nestjs-durable 0.30.2 → 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/index.cjs +128 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -1
- package/dist/index.d.ts +97 -1
- package/dist/index.js +127 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { StepError, WorkflowRef, SingletonConfig, StateStore, Transport, NamedTransport, ControlPlane, ScheduledWorkflow, RetentionPolicy, QueueConfig, AdmissionBackend, WorkflowClass, WorkflowInputOf, StartOptions, RunResult, RunGateway, WorkflowEngine, EntityHandler, DurableTopology, RunDetail, RunQuery, RunListItem, GroupHealth, EngineEvent, RunRequest, RunReply, StepInvocation, TenantEvent } from '@dudousxd/nestjs-durable-core';
|
|
1
|
+
import { StepError, WorkflowRef, SingletonConfig, SearchAttributesSchema, StateStore, Transport, NamedTransport, ControlPlane, ScheduledWorkflow, RetentionPolicy, QueueConfig, AdmissionBackend, WorkflowClass, WorkflowInputOf, StartOptions, RunResult, RunGateway, WorkflowEngine, EntityHandler, DurableTopology, RunDetail, RunQuery, RunListItem, GroupHealth, RunWaiting, EngineEvent, RunRequest, RunReply, StepInvocation, TenantEvent } from '@dudousxd/nestjs-durable-core';
|
|
2
|
+
export { AttributeFilter, EngineEvent, RunDetail, RunGateway, RunListItem, RunQuery, RunStatus, RunWaiting, SearchAttributes, StepCheckpoint, StepLogger, WorkflowCtx, WorkflowEngine, WorkflowRun } from '@dudousxd/nestjs-durable-core';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { ConcurrencyOption, StartRunDeps, RunRedisWorkerOptions, RunningWorker, DurableWorkerRuntime } from '@dudousxd/durable-worker';
|
|
4
5
|
import { DynamicModule, InjectionToken, OnModuleInit, OnApplicationBootstrap, OnApplicationShutdown, Provider } from '@nestjs/common';
|
|
@@ -51,6 +52,8 @@ interface WorkflowMeta {
|
|
|
51
52
|
inputSchema?: (new (...args: any[]) => object) | undefined;
|
|
52
53
|
/** Custom input validator (throws on invalid). See `WorkflowOptions`. */
|
|
53
54
|
validateInput?: ((input: unknown) => void | Promise<void>) | undefined;
|
|
55
|
+
/** Typed/validated `searchAttributes` shape. See `WorkflowOptions`. */
|
|
56
|
+
searchAttributes?: SearchAttributesSchema | undefined;
|
|
54
57
|
/** Event names that start a fresh run of this workflow. See `WorkflowOptions`. */
|
|
55
58
|
onEvent?: string[] | undefined;
|
|
56
59
|
/** Debounce `onEvent` triggers — fire once it's quiet for this long. See `WorkflowOptions`. */
|
|
@@ -102,6 +105,32 @@ interface WorkflowOptions {
|
|
|
102
105
|
* `inputSchema`. Takes precedence over `inputSchema` if both are set.
|
|
103
106
|
*/
|
|
104
107
|
validateInput?: (input: unknown) => void | Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Validate this workflow's {@link WorkflowCtx.upsertSearchAttributes} writes against a **Standard
|
|
110
|
+
* Schema** (https://standardschema.dev — zod 3.24+, valibot, arktype, …). `ctx.upsertSearchAttributes`
|
|
111
|
+
* validates the MERGED result (existing attributes shallow-merged with the patch) on every call —
|
|
112
|
+
* an invalid merge throws, naming this workflow, the offending key(s), and the schema's issues. The
|
|
113
|
+
* schema's inferred output must be search-attribute-shaped (flat `string`/`number`/`boolean` values
|
|
114
|
+
* only) — a schema whose output has a nested object/array is a compile-time error here. Omit for
|
|
115
|
+
* the prior unvalidated behavior.
|
|
116
|
+
*
|
|
117
|
+
* ```ts
|
|
118
|
+
* import { z } from 'zod';
|
|
119
|
+
*
|
|
120
|
+
* const orderAttrs = z.object({
|
|
121
|
+
* tier: z.enum(['free', 'pro']),
|
|
122
|
+
* amount: z.number(),
|
|
123
|
+
* });
|
|
124
|
+
*
|
|
125
|
+
* @Workflow({ name: 'checkout', searchAttributes: orderAttrs })
|
|
126
|
+
* class CheckoutWorkflow {
|
|
127
|
+
* async run(ctx: WorkflowCtx<InferSearchAttributes<typeof orderAttrs>>, input: CheckoutInput) {
|
|
128
|
+
* await ctx.upsertSearchAttributes({ tier: input.tier, amount: input.total });
|
|
129
|
+
* }
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
searchAttributes?: SearchAttributesSchema;
|
|
105
134
|
/**
|
|
106
135
|
* Start a fresh run of this workflow whenever any of these events is published via
|
|
107
136
|
* `publishEvent(name, payload)` — the payload becomes the run's input. e.g.
|
|
@@ -289,6 +318,10 @@ interface DurableRetentionOptions {
|
|
|
289
318
|
*
|
|
290
319
|
* `forRoot` throws when neither `store` nor `connection` is set, and when `store` is set without a
|
|
291
320
|
* `transport`.
|
|
321
|
+
*
|
|
322
|
+
* Set {@link DurableModuleOptions.topology} to name the role explicitly instead of relying on this
|
|
323
|
+
* inference — it additionally VALIDATES the axes above (`namespace` vs `partition`) that this prose
|
|
324
|
+
* is otherwise the only source of truth for. Omitting it changes nothing.
|
|
292
325
|
*/
|
|
293
326
|
interface DurableModuleOptions {
|
|
294
327
|
/** State store — set this to play the **operator** role (see the interface doc for the full role
|
|
@@ -478,6 +511,47 @@ interface DurableModuleOptions {
|
|
|
478
511
|
/** Timeout for a thin worker's `RunGateway` round-trip over `transport` before it rejects. Defaults
|
|
479
512
|
* to 10_000ms inside `ProxyRunGateway`. Ignored for an operator (bound to `StoreRunGateway`). */
|
|
480
513
|
runGatewayTimeoutMs?: number;
|
|
514
|
+
/**
|
|
515
|
+
* An explicit preset that NAMES the deployment role instead of leaving it to `store`/`connection`
|
|
516
|
+
* inference, and VALIDATES the axes that are otherwise only prose (see the interface doc's role
|
|
517
|
+
* matrix, and {@link namespace} vs {@link partition}). Omit to keep the existing inference —
|
|
518
|
+
* `topology` is entirely additive and changes nothing when unset.
|
|
519
|
+
*
|
|
520
|
+
* **`namespace` vs `partition` — the two axes this preset locks down:**
|
|
521
|
+
* - `namespace` is the OPERATOR's poll-scoping axis: which runs a control-plane instance
|
|
522
|
+
* drives/recovers/resumes (`runPending`/`recoverIncomplete`/`resumeDueTimers`/`sweepTimeouts`).
|
|
523
|
+
* Unset makes the instance see (drive) EVERY namespace.
|
|
524
|
+
* `{ role: 'control-plane' }` is the only preset that allows it.
|
|
525
|
+
* - `partition` is the WORKER's queue-routing suffix: which queue a thin/co-located worker's
|
|
526
|
+
* consumer subscribes to (`<name>@<partition>`), matching the queue an operator's convention
|
|
527
|
+
* dispatch routes a run's `namespace` to. `{ role: 'tenant' }` sets this FOR you from `tenant` —
|
|
528
|
+
* you never set `partition` directly under `topology`.
|
|
529
|
+
*
|
|
530
|
+
* ```ts
|
|
531
|
+
* // Control plane: owns the store, dispatches over the transport, prunes old runs.
|
|
532
|
+
* DurableModule.forRoot({
|
|
533
|
+
* topology: { role: 'control-plane' },
|
|
534
|
+
* store,
|
|
535
|
+
* transport,
|
|
536
|
+
* retention: { policies: [{ statuses: ['completed', 'cancelled'], maxAge: '14d' }] },
|
|
537
|
+
* });
|
|
538
|
+
*
|
|
539
|
+
* // Tenant: store-less worker scoped to its own partition — `tenant` maps to `partition` for you.
|
|
540
|
+
* DurableModule.forRoot({
|
|
541
|
+
* topology: { role: 'tenant', tenant: 'acme-corp' },
|
|
542
|
+
* connection: process.env.REDIS_URL,
|
|
543
|
+
* });
|
|
544
|
+
* ```
|
|
545
|
+
*
|
|
546
|
+
* Validated at `forRoot`/`forRootAsync` resolution time — see {@link DurableModule} for the exact
|
|
547
|
+
* error messages, which double as the axis primer above.
|
|
548
|
+
*/
|
|
549
|
+
topology?: {
|
|
550
|
+
role: 'control-plane';
|
|
551
|
+
} | {
|
|
552
|
+
role: 'tenant';
|
|
553
|
+
tenant: string;
|
|
554
|
+
};
|
|
481
555
|
}
|
|
482
556
|
interface DurableModuleAsyncOptions {
|
|
483
557
|
useFactory: (...args: never[]) => DurableModuleOptions | Promise<DurableModuleOptions>;
|
|
@@ -486,6 +560,14 @@ interface DurableModuleAsyncOptions {
|
|
|
486
560
|
declare class DurableModule {
|
|
487
561
|
static forRoot(options: DurableModuleOptions): DynamicModule;
|
|
488
562
|
static forRootAsync(options: DurableModuleAsyncOptions): DynamicModule;
|
|
563
|
+
/**
|
|
564
|
+
* Validates role/axis constraints, in resolution order. When {@link DurableModuleOptions.topology}
|
|
565
|
+
* is set, it OWNS validation — {@link assertValidTopology}'s own store/transport/connection checks
|
|
566
|
+
* are strictly stronger than {@link assertValidRole}'s, so the topology-specific, axis-teaching
|
|
567
|
+
* message is what a `topology`-opted-in consumer sees (not the older generic one). Falls back to
|
|
568
|
+
* {@link assertValidRole} unchanged when `topology` is absent — zero behavior change.
|
|
569
|
+
*/
|
|
570
|
+
private static assertValid;
|
|
489
571
|
private static build;
|
|
490
572
|
}
|
|
491
573
|
|
|
@@ -722,6 +804,9 @@ declare class ProxyRunGateway implements RunGateway {
|
|
|
722
804
|
listRuns(query: RunQuery): Promise<RunListItem[]>;
|
|
723
805
|
/** Round-trips to the operator, which scopes the result to this tenant's own `@<tenant>` groups. */
|
|
724
806
|
workerHealth(): Promise<GroupHealth[]>;
|
|
807
|
+
/** Bulk, one request for the whole id list (like `listRuns`, not one request per id). The operator
|
|
808
|
+
* filters the reply to runs this tenant actually owns (see `RunRequestResponder`). */
|
|
809
|
+
waitingFor(runIds: string[]): Promise<Record<string, RunWaiting>>;
|
|
725
810
|
cancel(runId: string, opts?: {
|
|
726
811
|
compensate?: boolean;
|
|
727
812
|
}): Promise<RunResult | null>;
|
|
@@ -810,6 +895,17 @@ declare class StoreRunGateway implements RunGateway {
|
|
|
810
895
|
topology(): DurableTopology;
|
|
811
896
|
getRunDetail(runId: string): Promise<RunDetail | null>;
|
|
812
897
|
listRuns(query: RunQuery): Promise<RunListItem[]>;
|
|
898
|
+
/**
|
|
899
|
+
* Bulk-resolve what each of `runIds` is currently parked on — for a consumer with its own filtered/
|
|
900
|
+
* paginated run listing (e.g. "which of MY suspended runs are stuck at a breakpoint") without
|
|
901
|
+
* re-deriving `listRuns`' waiter scan or querying `durable_step_checkpoints` directly. Mirrors
|
|
902
|
+
* `listRuns`' waiting computation (the SAME bulk signal-waiter scan + `resolveRunWaiting`), but ALSO
|
|
903
|
+
* bulk-fetches the currently-suspended runs to check real status: `engine.cancel` (the non-compensate
|
|
904
|
+
* path) doesn't clear a run's signal waiter row, so a cancelled run can leave an ORPHANED waiter
|
|
905
|
+
* behind — trusting waiter presence alone would wrongly report a terminal run as still waiting.
|
|
906
|
+
* Two bulk scans total (never one query per requested id), same as `listRuns`.
|
|
907
|
+
*/
|
|
908
|
+
waitingFor(runIds: string[]): Promise<Record<string, RunWaiting>>;
|
|
813
909
|
/** Every group the engine knows about — unscoped. A tenant proxy's request is scoped by the
|
|
814
910
|
* `RunRequestResponder` (to the requester's `@<tenant>` groups); the operator's own UI sees all. */
|
|
815
911
|
workerHealth(): Promise<GroupHealth[]>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { StepError, WorkflowRef, SingletonConfig, StateStore, Transport, NamedTransport, ControlPlane, ScheduledWorkflow, RetentionPolicy, QueueConfig, AdmissionBackend, WorkflowClass, WorkflowInputOf, StartOptions, RunResult, RunGateway, WorkflowEngine, EntityHandler, DurableTopology, RunDetail, RunQuery, RunListItem, GroupHealth, EngineEvent, RunRequest, RunReply, StepInvocation, TenantEvent } from '@dudousxd/nestjs-durable-core';
|
|
1
|
+
import { StepError, WorkflowRef, SingletonConfig, SearchAttributesSchema, StateStore, Transport, NamedTransport, ControlPlane, ScheduledWorkflow, RetentionPolicy, QueueConfig, AdmissionBackend, WorkflowClass, WorkflowInputOf, StartOptions, RunResult, RunGateway, WorkflowEngine, EntityHandler, DurableTopology, RunDetail, RunQuery, RunListItem, GroupHealth, RunWaiting, EngineEvent, RunRequest, RunReply, StepInvocation, TenantEvent } from '@dudousxd/nestjs-durable-core';
|
|
2
|
+
export { AttributeFilter, EngineEvent, RunDetail, RunGateway, RunListItem, RunQuery, RunStatus, RunWaiting, SearchAttributes, StepCheckpoint, StepLogger, WorkflowCtx, WorkflowEngine, WorkflowRun } from '@dudousxd/nestjs-durable-core';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { ConcurrencyOption, StartRunDeps, RunRedisWorkerOptions, RunningWorker, DurableWorkerRuntime } from '@dudousxd/durable-worker';
|
|
4
5
|
import { DynamicModule, InjectionToken, OnModuleInit, OnApplicationBootstrap, OnApplicationShutdown, Provider } from '@nestjs/common';
|
|
@@ -51,6 +52,8 @@ interface WorkflowMeta {
|
|
|
51
52
|
inputSchema?: (new (...args: any[]) => object) | undefined;
|
|
52
53
|
/** Custom input validator (throws on invalid). See `WorkflowOptions`. */
|
|
53
54
|
validateInput?: ((input: unknown) => void | Promise<void>) | undefined;
|
|
55
|
+
/** Typed/validated `searchAttributes` shape. See `WorkflowOptions`. */
|
|
56
|
+
searchAttributes?: SearchAttributesSchema | undefined;
|
|
54
57
|
/** Event names that start a fresh run of this workflow. See `WorkflowOptions`. */
|
|
55
58
|
onEvent?: string[] | undefined;
|
|
56
59
|
/** Debounce `onEvent` triggers — fire once it's quiet for this long. See `WorkflowOptions`. */
|
|
@@ -102,6 +105,32 @@ interface WorkflowOptions {
|
|
|
102
105
|
* `inputSchema`. Takes precedence over `inputSchema` if both are set.
|
|
103
106
|
*/
|
|
104
107
|
validateInput?: (input: unknown) => void | Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Validate this workflow's {@link WorkflowCtx.upsertSearchAttributes} writes against a **Standard
|
|
110
|
+
* Schema** (https://standardschema.dev — zod 3.24+, valibot, arktype, …). `ctx.upsertSearchAttributes`
|
|
111
|
+
* validates the MERGED result (existing attributes shallow-merged with the patch) on every call —
|
|
112
|
+
* an invalid merge throws, naming this workflow, the offending key(s), and the schema's issues. The
|
|
113
|
+
* schema's inferred output must be search-attribute-shaped (flat `string`/`number`/`boolean` values
|
|
114
|
+
* only) — a schema whose output has a nested object/array is a compile-time error here. Omit for
|
|
115
|
+
* the prior unvalidated behavior.
|
|
116
|
+
*
|
|
117
|
+
* ```ts
|
|
118
|
+
* import { z } from 'zod';
|
|
119
|
+
*
|
|
120
|
+
* const orderAttrs = z.object({
|
|
121
|
+
* tier: z.enum(['free', 'pro']),
|
|
122
|
+
* amount: z.number(),
|
|
123
|
+
* });
|
|
124
|
+
*
|
|
125
|
+
* @Workflow({ name: 'checkout', searchAttributes: orderAttrs })
|
|
126
|
+
* class CheckoutWorkflow {
|
|
127
|
+
* async run(ctx: WorkflowCtx<InferSearchAttributes<typeof orderAttrs>>, input: CheckoutInput) {
|
|
128
|
+
* await ctx.upsertSearchAttributes({ tier: input.tier, amount: input.total });
|
|
129
|
+
* }
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
searchAttributes?: SearchAttributesSchema;
|
|
105
134
|
/**
|
|
106
135
|
* Start a fresh run of this workflow whenever any of these events is published via
|
|
107
136
|
* `publishEvent(name, payload)` — the payload becomes the run's input. e.g.
|
|
@@ -289,6 +318,10 @@ interface DurableRetentionOptions {
|
|
|
289
318
|
*
|
|
290
319
|
* `forRoot` throws when neither `store` nor `connection` is set, and when `store` is set without a
|
|
291
320
|
* `transport`.
|
|
321
|
+
*
|
|
322
|
+
* Set {@link DurableModuleOptions.topology} to name the role explicitly instead of relying on this
|
|
323
|
+
* inference — it additionally VALIDATES the axes above (`namespace` vs `partition`) that this prose
|
|
324
|
+
* is otherwise the only source of truth for. Omitting it changes nothing.
|
|
292
325
|
*/
|
|
293
326
|
interface DurableModuleOptions {
|
|
294
327
|
/** State store — set this to play the **operator** role (see the interface doc for the full role
|
|
@@ -478,6 +511,47 @@ interface DurableModuleOptions {
|
|
|
478
511
|
/** Timeout for a thin worker's `RunGateway` round-trip over `transport` before it rejects. Defaults
|
|
479
512
|
* to 10_000ms inside `ProxyRunGateway`. Ignored for an operator (bound to `StoreRunGateway`). */
|
|
480
513
|
runGatewayTimeoutMs?: number;
|
|
514
|
+
/**
|
|
515
|
+
* An explicit preset that NAMES the deployment role instead of leaving it to `store`/`connection`
|
|
516
|
+
* inference, and VALIDATES the axes that are otherwise only prose (see the interface doc's role
|
|
517
|
+
* matrix, and {@link namespace} vs {@link partition}). Omit to keep the existing inference —
|
|
518
|
+
* `topology` is entirely additive and changes nothing when unset.
|
|
519
|
+
*
|
|
520
|
+
* **`namespace` vs `partition` — the two axes this preset locks down:**
|
|
521
|
+
* - `namespace` is the OPERATOR's poll-scoping axis: which runs a control-plane instance
|
|
522
|
+
* drives/recovers/resumes (`runPending`/`recoverIncomplete`/`resumeDueTimers`/`sweepTimeouts`).
|
|
523
|
+
* Unset makes the instance see (drive) EVERY namespace.
|
|
524
|
+
* `{ role: 'control-plane' }` is the only preset that allows it.
|
|
525
|
+
* - `partition` is the WORKER's queue-routing suffix: which queue a thin/co-located worker's
|
|
526
|
+
* consumer subscribes to (`<name>@<partition>`), matching the queue an operator's convention
|
|
527
|
+
* dispatch routes a run's `namespace` to. `{ role: 'tenant' }` sets this FOR you from `tenant` —
|
|
528
|
+
* you never set `partition` directly under `topology`.
|
|
529
|
+
*
|
|
530
|
+
* ```ts
|
|
531
|
+
* // Control plane: owns the store, dispatches over the transport, prunes old runs.
|
|
532
|
+
* DurableModule.forRoot({
|
|
533
|
+
* topology: { role: 'control-plane' },
|
|
534
|
+
* store,
|
|
535
|
+
* transport,
|
|
536
|
+
* retention: { policies: [{ statuses: ['completed', 'cancelled'], maxAge: '14d' }] },
|
|
537
|
+
* });
|
|
538
|
+
*
|
|
539
|
+
* // Tenant: store-less worker scoped to its own partition — `tenant` maps to `partition` for you.
|
|
540
|
+
* DurableModule.forRoot({
|
|
541
|
+
* topology: { role: 'tenant', tenant: 'acme-corp' },
|
|
542
|
+
* connection: process.env.REDIS_URL,
|
|
543
|
+
* });
|
|
544
|
+
* ```
|
|
545
|
+
*
|
|
546
|
+
* Validated at `forRoot`/`forRootAsync` resolution time — see {@link DurableModule} for the exact
|
|
547
|
+
* error messages, which double as the axis primer above.
|
|
548
|
+
*/
|
|
549
|
+
topology?: {
|
|
550
|
+
role: 'control-plane';
|
|
551
|
+
} | {
|
|
552
|
+
role: 'tenant';
|
|
553
|
+
tenant: string;
|
|
554
|
+
};
|
|
481
555
|
}
|
|
482
556
|
interface DurableModuleAsyncOptions {
|
|
483
557
|
useFactory: (...args: never[]) => DurableModuleOptions | Promise<DurableModuleOptions>;
|
|
@@ -486,6 +560,14 @@ interface DurableModuleAsyncOptions {
|
|
|
486
560
|
declare class DurableModule {
|
|
487
561
|
static forRoot(options: DurableModuleOptions): DynamicModule;
|
|
488
562
|
static forRootAsync(options: DurableModuleAsyncOptions): DynamicModule;
|
|
563
|
+
/**
|
|
564
|
+
* Validates role/axis constraints, in resolution order. When {@link DurableModuleOptions.topology}
|
|
565
|
+
* is set, it OWNS validation — {@link assertValidTopology}'s own store/transport/connection checks
|
|
566
|
+
* are strictly stronger than {@link assertValidRole}'s, so the topology-specific, axis-teaching
|
|
567
|
+
* message is what a `topology`-opted-in consumer sees (not the older generic one). Falls back to
|
|
568
|
+
* {@link assertValidRole} unchanged when `topology` is absent — zero behavior change.
|
|
569
|
+
*/
|
|
570
|
+
private static assertValid;
|
|
489
571
|
private static build;
|
|
490
572
|
}
|
|
491
573
|
|
|
@@ -722,6 +804,9 @@ declare class ProxyRunGateway implements RunGateway {
|
|
|
722
804
|
listRuns(query: RunQuery): Promise<RunListItem[]>;
|
|
723
805
|
/** Round-trips to the operator, which scopes the result to this tenant's own `@<tenant>` groups. */
|
|
724
806
|
workerHealth(): Promise<GroupHealth[]>;
|
|
807
|
+
/** Bulk, one request for the whole id list (like `listRuns`, not one request per id). The operator
|
|
808
|
+
* filters the reply to runs this tenant actually owns (see `RunRequestResponder`). */
|
|
809
|
+
waitingFor(runIds: string[]): Promise<Record<string, RunWaiting>>;
|
|
725
810
|
cancel(runId: string, opts?: {
|
|
726
811
|
compensate?: boolean;
|
|
727
812
|
}): Promise<RunResult | null>;
|
|
@@ -810,6 +895,17 @@ declare class StoreRunGateway implements RunGateway {
|
|
|
810
895
|
topology(): DurableTopology;
|
|
811
896
|
getRunDetail(runId: string): Promise<RunDetail | null>;
|
|
812
897
|
listRuns(query: RunQuery): Promise<RunListItem[]>;
|
|
898
|
+
/**
|
|
899
|
+
* Bulk-resolve what each of `runIds` is currently parked on — for a consumer with its own filtered/
|
|
900
|
+
* paginated run listing (e.g. "which of MY suspended runs are stuck at a breakpoint") without
|
|
901
|
+
* re-deriving `listRuns`' waiter scan or querying `durable_step_checkpoints` directly. Mirrors
|
|
902
|
+
* `listRuns`' waiting computation (the SAME bulk signal-waiter scan + `resolveRunWaiting`), but ALSO
|
|
903
|
+
* bulk-fetches the currently-suspended runs to check real status: `engine.cancel` (the non-compensate
|
|
904
|
+
* path) doesn't clear a run's signal waiter row, so a cancelled run can leave an ORPHANED waiter
|
|
905
|
+
* behind — trusting waiter presence alone would wrongly report a terminal run as still waiting.
|
|
906
|
+
* Two bulk scans total (never one query per requested id), same as `listRuns`.
|
|
907
|
+
*/
|
|
908
|
+
waitingFor(runIds: string[]): Promise<Record<string, RunWaiting>>;
|
|
813
909
|
/** Every group the engine knows about — unscoped. A tenant proxy's request is scoped by the
|
|
814
910
|
* `RunRequestResponder` (to the requester's `@<tenant>` groups); the operator's own UI sees all. */
|
|
815
911
|
workerHealth(): Promise<GroupHealth[]>;
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ function Workflow(options) {
|
|
|
22
22
|
executionTimeout: options.executionTimeout,
|
|
23
23
|
inputSchema: options.inputSchema,
|
|
24
24
|
validateInput: options.validateInput,
|
|
25
|
+
searchAttributes: options.searchAttributes,
|
|
25
26
|
onEvent: options.onEvent,
|
|
26
27
|
debounce: options.debounce,
|
|
27
28
|
batch: options.batch
|
|
@@ -436,6 +437,9 @@ function unavailableRunGateway() {
|
|
|
436
437
|
listRuns(_query) {
|
|
437
438
|
return tenantGatewayUnavailable("listRuns");
|
|
438
439
|
},
|
|
440
|
+
waitingFor(_runIds) {
|
|
441
|
+
return tenantGatewayUnavailable("waitingFor");
|
|
442
|
+
},
|
|
439
443
|
workerHealth() {
|
|
440
444
|
return tenantGatewayUnavailable("workerHealth");
|
|
441
445
|
},
|
|
@@ -853,6 +857,14 @@ var ProxyRunGateway = class {
|
|
|
853
857
|
kind: "workerHealth"
|
|
854
858
|
});
|
|
855
859
|
}
|
|
860
|
+
/** Bulk, one request for the whole id list (like `listRuns`, not one request per id). The operator
|
|
861
|
+
* filters the reply to runs this tenant actually owns (see `RunRequestResponder`). */
|
|
862
|
+
waitingFor(runIds) {
|
|
863
|
+
return this.request({
|
|
864
|
+
kind: "waitingFor",
|
|
865
|
+
runIds
|
|
866
|
+
});
|
|
867
|
+
}
|
|
856
868
|
cancel(runId, opts) {
|
|
857
869
|
return this.request(opts === void 0 ? {
|
|
858
870
|
kind: "cancel",
|
|
@@ -1066,6 +1078,27 @@ var RunRequestResponder = class {
|
|
|
1066
1078
|
}
|
|
1067
1079
|
};
|
|
1068
1080
|
}
|
|
1081
|
+
if (body.kind === "waitingFor") {
|
|
1082
|
+
const all = await this.gateway.waitingFor(body.runIds);
|
|
1083
|
+
const owned = await Promise.all(Object.entries(all).map(async ([runId, waiting]) => {
|
|
1084
|
+
const runDetail = await this.gateway.getRunDetail(runId);
|
|
1085
|
+
return runDetail && runDetail.run.namespace === msg.tenant ? [
|
|
1086
|
+
runId,
|
|
1087
|
+
waiting
|
|
1088
|
+
] : void 0;
|
|
1089
|
+
}));
|
|
1090
|
+
const data = {};
|
|
1091
|
+
for (const entry of owned) {
|
|
1092
|
+
if (entry) data[entry[0]] = entry[1];
|
|
1093
|
+
}
|
|
1094
|
+
return {
|
|
1095
|
+
requestId: msg.requestId,
|
|
1096
|
+
result: {
|
|
1097
|
+
ok: true,
|
|
1098
|
+
data
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1069
1102
|
const detail = await this.gateway.getRunDetail(body.runId);
|
|
1070
1103
|
if (detail && detail.run.namespace !== msg.tenant) {
|
|
1071
1104
|
return {
|
|
@@ -1184,6 +1217,36 @@ var StoreRunGateway = class {
|
|
|
1184
1217
|
} : run;
|
|
1185
1218
|
});
|
|
1186
1219
|
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Bulk-resolve what each of `runIds` is currently parked on — for a consumer with its own filtered/
|
|
1222
|
+
* paginated run listing (e.g. "which of MY suspended runs are stuck at a breakpoint") without
|
|
1223
|
+
* re-deriving `listRuns`' waiter scan or querying `durable_step_checkpoints` directly. Mirrors
|
|
1224
|
+
* `listRuns`' waiting computation (the SAME bulk signal-waiter scan + `resolveRunWaiting`), but ALSO
|
|
1225
|
+
* bulk-fetches the currently-suspended runs to check real status: `engine.cancel` (the non-compensate
|
|
1226
|
+
* path) doesn't clear a run's signal waiter row, so a cancelled run can leave an ORPHANED waiter
|
|
1227
|
+
* behind — trusting waiter presence alone would wrongly report a terminal run as still waiting.
|
|
1228
|
+
* Two bulk scans total (never one query per requested id), same as `listRuns`.
|
|
1229
|
+
*/
|
|
1230
|
+
async waitingFor(runIds) {
|
|
1231
|
+
if (runIds.length === 0) return {};
|
|
1232
|
+
const idSet = new Set(runIds);
|
|
1233
|
+
const [suspended, waiters] = await Promise.all([
|
|
1234
|
+
this.store.listRuns({
|
|
1235
|
+
statuses: [
|
|
1236
|
+
"suspended"
|
|
1237
|
+
]
|
|
1238
|
+
}),
|
|
1239
|
+
this.store.listSignalWaiters("")
|
|
1240
|
+
]);
|
|
1241
|
+
const waiterByRun = indexWaitersByRun(waiters);
|
|
1242
|
+
const result = {};
|
|
1243
|
+
for (const run of suspended) {
|
|
1244
|
+
if (!idSet.has(run.id)) continue;
|
|
1245
|
+
const waiting = resolveRunWaiting(run, waiterByRun);
|
|
1246
|
+
if (waiting) result[run.id] = waiting;
|
|
1247
|
+
}
|
|
1248
|
+
return result;
|
|
1249
|
+
}
|
|
1187
1250
|
/** Every group the engine knows about — unscoped. A tenant proxy's request is scoped by the
|
|
1188
1251
|
* `RunRequestResponder` (to the requester's `@<tenant>` groups); the operator's own UI sees all. */
|
|
1189
1252
|
workerHealth() {
|
|
@@ -1475,6 +1538,7 @@ var WorkflowRegistrar = class {
|
|
|
1475
1538
|
singleton: meta.singleton,
|
|
1476
1539
|
executionTimeout: meta.executionTimeout,
|
|
1477
1540
|
validateInput,
|
|
1541
|
+
searchAttributesSchema: meta.searchAttributes,
|
|
1478
1542
|
onEvent: getOnEvents(meta, workflowCtor),
|
|
1479
1543
|
eventBatch,
|
|
1480
1544
|
// Uniform dispatch (opt-in): when an in-app worker is configured, register the body GROUP-SERVED
|
|
@@ -1707,6 +1771,47 @@ function assertValidRole(options) {
|
|
|
1707
1771
|
}
|
|
1708
1772
|
}
|
|
1709
1773
|
__name(assertValidRole, "assertValidRole");
|
|
1774
|
+
function assertValidTopology(options) {
|
|
1775
|
+
const topology = options.topology;
|
|
1776
|
+
if (topology === void 0) return;
|
|
1777
|
+
if (topology.role === "control-plane") {
|
|
1778
|
+
if (options.store === void 0 || options.transport === void 0 && options.transports === void 0) {
|
|
1779
|
+
throw new Error("topology: { role: 'control-plane' } needs `store` AND (`transport` or `transports`).\nA control-plane node is the durable operator: it owns the state store and dispatches runs to workers over a transport \u2014 without both, there is nothing for it to operate.");
|
|
1780
|
+
}
|
|
1781
|
+
if (options.partition !== void 0) {
|
|
1782
|
+
throw new Error("topology: { role: 'control-plane' } forbids `partition`.\npartition is the WORKER queue-routing suffix \u2014 on a control-plane node, runs are dispatched to tenant partitions via each run's namespace, not via this option. Set `partition` (or `topology: { role: 'tenant', tenant }`) on the WORKER node instead.");
|
|
1783
|
+
}
|
|
1784
|
+
if ("tenant" in topology) {
|
|
1785
|
+
throw new Error("topology: { role: 'control-plane' } forbids a `tenant` field.\ntenant names a WORKER's isolation partition \u2014 a control-plane node serves every tenant's runs (via their namespace), it isn't scoped to one itself.");
|
|
1786
|
+
}
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (options.connection === void 0) {
|
|
1790
|
+
throw new Error(`topology: { role: 'tenant', tenant: '${topology.tenant}' } needs \`connection\`.
|
|
1791
|
+
A tenant is a store-less worker: it connects to the broker directly to pull its own partition of tasks instead of polling a store it does not have.`);
|
|
1792
|
+
}
|
|
1793
|
+
if (options.store !== void 0) {
|
|
1794
|
+
throw new Error("topology: { role: 'tenant' } forbids `store`.\nA tenant is store-less BY DEFINITION \u2014 a `store` present means you actually wanted `topology: { role: 'control-plane' }` (the store-owning role).");
|
|
1795
|
+
}
|
|
1796
|
+
if (options.namespace !== void 0) {
|
|
1797
|
+
throw new Error("topology: { role: 'tenant' } forbids `namespace`.\nnamespace is the OPERATOR's poll-scoping axis (which runs a control-plane instance drives/recovers/resumes) \u2014 a tenant worker has no poll loop for it to scope. Use this preset's `tenant` field for the worker's own routing axis instead.");
|
|
1798
|
+
}
|
|
1799
|
+
if (options.partition !== void 0 && options.partition !== topology.tenant) {
|
|
1800
|
+
throw new Error(`topology: { role: 'tenant', tenant: '${topology.tenant}' } conflicts with \`partition: '${options.partition}'\`.
|
|
1801
|
+
tenant maps 1:1 onto partition (the worker queue-routing suffix) \u2014 set only \`tenant\`, or set \`partition\` to the same value.`);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
__name(assertValidTopology, "assertValidTopology");
|
|
1805
|
+
function resolveTopology(options) {
|
|
1806
|
+
const topology = options.topology;
|
|
1807
|
+
if (topology === void 0 || topology.role !== "tenant") return options;
|
|
1808
|
+
if (options.partition !== void 0) return options;
|
|
1809
|
+
return {
|
|
1810
|
+
...options,
|
|
1811
|
+
partition: topology.tenant
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
__name(resolveTopology, "resolveTopology");
|
|
1710
1815
|
var RunGatewayBootstrap = class RunGatewayBootstrap2 {
|
|
1711
1816
|
static {
|
|
1712
1817
|
__name(this, "RunGatewayBootstrap");
|
|
@@ -1767,10 +1872,10 @@ var DurableModule = class _DurableModule {
|
|
|
1767
1872
|
__name(this, "DurableModule");
|
|
1768
1873
|
}
|
|
1769
1874
|
static forRoot(options) {
|
|
1770
|
-
|
|
1875
|
+
_DurableModule.assertValid(options);
|
|
1771
1876
|
return _DurableModule.build({
|
|
1772
1877
|
provide: DURABLE_OPTIONS_CANONICAL7,
|
|
1773
|
-
useValue: options
|
|
1878
|
+
useValue: resolveTopology(options)
|
|
1774
1879
|
});
|
|
1775
1880
|
}
|
|
1776
1881
|
static forRootAsync(options) {
|
|
@@ -1778,12 +1883,26 @@ var DurableModule = class _DurableModule {
|
|
|
1778
1883
|
provide: DURABLE_OPTIONS_CANONICAL7,
|
|
1779
1884
|
useFactory: /* @__PURE__ */ __name(async (...args) => {
|
|
1780
1885
|
const resolved = await options.useFactory(...args);
|
|
1781
|
-
|
|
1782
|
-
return resolved;
|
|
1886
|
+
_DurableModule.assertValid(resolved);
|
|
1887
|
+
return resolveTopology(resolved);
|
|
1783
1888
|
}, "useFactory"),
|
|
1784
1889
|
inject: options.inject ?? []
|
|
1785
1890
|
});
|
|
1786
1891
|
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Validates role/axis constraints, in resolution order. When {@link DurableModuleOptions.topology}
|
|
1894
|
+
* is set, it OWNS validation — {@link assertValidTopology}'s own store/transport/connection checks
|
|
1895
|
+
* are strictly stronger than {@link assertValidRole}'s, so the topology-specific, axis-teaching
|
|
1896
|
+
* message is what a `topology`-opted-in consumer sees (not the older generic one). Falls back to
|
|
1897
|
+
* {@link assertValidRole} unchanged when `topology` is absent — zero behavior change.
|
|
1898
|
+
*/
|
|
1899
|
+
static assertValid(options) {
|
|
1900
|
+
if (options.topology !== void 0) {
|
|
1901
|
+
assertValidTopology(options);
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
assertValidRole(options);
|
|
1905
|
+
}
|
|
1787
1906
|
static build(optionsProvider) {
|
|
1788
1907
|
return {
|
|
1789
1908
|
module: _DurableModule,
|
|
@@ -1925,6 +2044,9 @@ var DurableModule = class _DurableModule {
|
|
|
1925
2044
|
DurableModule = _ts_decorate12([
|
|
1926
2045
|
Module({})
|
|
1927
2046
|
], DurableModule);
|
|
2047
|
+
|
|
2048
|
+
// src/index.ts
|
|
2049
|
+
import { WorkflowEngine as WorkflowEngine7 } from "@dudousxd/nestjs-durable-core";
|
|
1928
2050
|
export {
|
|
1929
2051
|
CONTEXT_ACCESSOR,
|
|
1930
2052
|
DEAD_LETTER_METADATA,
|
|
@@ -1961,6 +2083,7 @@ export {
|
|
|
1961
2083
|
ThinWorkflowRegistrar,
|
|
1962
2084
|
WORKFLOW_METADATA,
|
|
1963
2085
|
Workflow,
|
|
2086
|
+
WorkflowEngine7 as WorkflowEngine,
|
|
1964
2087
|
WorkflowService,
|
|
1965
2088
|
entityConfigFor,
|
|
1966
2089
|
getDurableStepMeta,
|