@dudousxd/nestjs-durable 0.30.1 → 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 +142 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +103 -1
- package/dist/index.d.ts +103 -1
- package/dist/index.js +141 -4
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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>;
|
|
@@ -730,6 +815,9 @@ declare class ProxyRunGateway implements RunGateway {
|
|
|
730
815
|
retryWithInput(runId: string, input: unknown): Promise<{
|
|
731
816
|
runId: string;
|
|
732
817
|
} | null>;
|
|
818
|
+
redispatchPending(runId: string): Promise<(RunResult & {
|
|
819
|
+
redispatched: number;
|
|
820
|
+
}) | null>;
|
|
733
821
|
subscribe(runId: string, onEvent: (event: EngineEvent) => void): () => void;
|
|
734
822
|
}
|
|
735
823
|
|
|
@@ -807,6 +895,17 @@ declare class StoreRunGateway implements RunGateway {
|
|
|
807
895
|
topology(): DurableTopology;
|
|
808
896
|
getRunDetail(runId: string): Promise<RunDetail | null>;
|
|
809
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>>;
|
|
810
909
|
/** Every group the engine knows about — unscoped. A tenant proxy's request is scoped by the
|
|
811
910
|
* `RunRequestResponder` (to the requester's `@<tenant>` groups); the operator's own UI sees all. */
|
|
812
911
|
workerHealth(): Promise<GroupHealth[]>;
|
|
@@ -819,6 +918,9 @@ declare class StoreRunGateway implements RunGateway {
|
|
|
819
918
|
retryWithInput(runId: string, input: unknown): Promise<{
|
|
820
919
|
runId: string;
|
|
821
920
|
} | null>;
|
|
921
|
+
redispatchPending(runId: string): Promise<(RunResult & {
|
|
922
|
+
redispatched: number;
|
|
923
|
+
}) | null>;
|
|
822
924
|
subscribe(runId: string, onEvent: (event: EngineEvent) => void): () => void;
|
|
823
925
|
}
|
|
824
926
|
|
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>;
|
|
@@ -730,6 +815,9 @@ declare class ProxyRunGateway implements RunGateway {
|
|
|
730
815
|
retryWithInput(runId: string, input: unknown): Promise<{
|
|
731
816
|
runId: string;
|
|
732
817
|
} | null>;
|
|
818
|
+
redispatchPending(runId: string): Promise<(RunResult & {
|
|
819
|
+
redispatched: number;
|
|
820
|
+
}) | null>;
|
|
733
821
|
subscribe(runId: string, onEvent: (event: EngineEvent) => void): () => void;
|
|
734
822
|
}
|
|
735
823
|
|
|
@@ -807,6 +895,17 @@ declare class StoreRunGateway implements RunGateway {
|
|
|
807
895
|
topology(): DurableTopology;
|
|
808
896
|
getRunDetail(runId: string): Promise<RunDetail | null>;
|
|
809
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>>;
|
|
810
909
|
/** Every group the engine knows about — unscoped. A tenant proxy's request is scoped by the
|
|
811
910
|
* `RunRequestResponder` (to the requester's `@<tenant>` groups); the operator's own UI sees all. */
|
|
812
911
|
workerHealth(): Promise<GroupHealth[]>;
|
|
@@ -819,6 +918,9 @@ declare class StoreRunGateway implements RunGateway {
|
|
|
819
918
|
retryWithInput(runId: string, input: unknown): Promise<{
|
|
820
919
|
runId: string;
|
|
821
920
|
} | null>;
|
|
921
|
+
redispatchPending(runId: string): Promise<(RunResult & {
|
|
922
|
+
redispatched: number;
|
|
923
|
+
}) | null>;
|
|
822
924
|
subscribe(runId: string, onEvent: (event: EngineEvent) => void): () => void;
|
|
823
925
|
}
|
|
824
926
|
|
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
|
},
|
|
@@ -451,6 +455,9 @@ function unavailableRunGateway() {
|
|
|
451
455
|
retryWithInput(_runId, _input) {
|
|
452
456
|
return tenantGatewayUnavailable("retryWithInput");
|
|
453
457
|
},
|
|
458
|
+
redispatchPending(_runId) {
|
|
459
|
+
return tenantGatewayUnavailable("redispatchPending");
|
|
460
|
+
},
|
|
454
461
|
subscribe(_runId, _onEvent) {
|
|
455
462
|
throw new Error("RunGateway.subscribe() is not available \u2014 pass `transport` to use RunGateway.");
|
|
456
463
|
}
|
|
@@ -850,6 +857,14 @@ var ProxyRunGateway = class {
|
|
|
850
857
|
kind: "workerHealth"
|
|
851
858
|
});
|
|
852
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
|
+
}
|
|
853
868
|
cancel(runId, opts) {
|
|
854
869
|
return this.request(opts === void 0 ? {
|
|
855
870
|
kind: "cancel",
|
|
@@ -879,6 +894,12 @@ var ProxyRunGateway = class {
|
|
|
879
894
|
input
|
|
880
895
|
});
|
|
881
896
|
}
|
|
897
|
+
redispatchPending(runId) {
|
|
898
|
+
return this.request({
|
|
899
|
+
kind: "redispatch",
|
|
900
|
+
runId
|
|
901
|
+
});
|
|
902
|
+
}
|
|
882
903
|
subscribe(runId, onEvent) {
|
|
883
904
|
const unsubscribe = this.transport.onTenantEvent?.(this.tenant, (evt) => {
|
|
884
905
|
if (evt.event.runId === runId) onEvent(evt.event);
|
|
@@ -1057,6 +1078,27 @@ var RunRequestResponder = class {
|
|
|
1057
1078
|
}
|
|
1058
1079
|
};
|
|
1059
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
|
+
}
|
|
1060
1102
|
const detail = await this.gateway.getRunDetail(body.runId);
|
|
1061
1103
|
if (detail && detail.run.namespace !== msg.tenant) {
|
|
1062
1104
|
return {
|
|
@@ -1110,6 +1152,8 @@ var RunRequestResponder = class {
|
|
|
1110
1152
|
return this.gateway.continue(body.runId);
|
|
1111
1153
|
case "retryWithInput":
|
|
1112
1154
|
return this.gateway.retryWithInput(body.runId, body.input);
|
|
1155
|
+
case "redispatch":
|
|
1156
|
+
return this.gateway.redispatchPending(body.runId);
|
|
1113
1157
|
}
|
|
1114
1158
|
}
|
|
1115
1159
|
};
|
|
@@ -1173,6 +1217,36 @@ var StoreRunGateway = class {
|
|
|
1173
1217
|
} : run;
|
|
1174
1218
|
});
|
|
1175
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
|
+
}
|
|
1176
1250
|
/** Every group the engine knows about — unscoped. A tenant proxy's request is scoped by the
|
|
1177
1251
|
* `RunRequestResponder` (to the requester's `@<tenant>` groups); the operator's own UI sees all. */
|
|
1178
1252
|
workerHealth() {
|
|
@@ -1191,6 +1265,9 @@ var StoreRunGateway = class {
|
|
|
1191
1265
|
retryWithInput(runId, input) {
|
|
1192
1266
|
return this.engine.retryWithInput(runId, input);
|
|
1193
1267
|
}
|
|
1268
|
+
redispatchPending(runId) {
|
|
1269
|
+
return this.engine.redispatchPending(runId);
|
|
1270
|
+
}
|
|
1194
1271
|
subscribe(runId, onEvent) {
|
|
1195
1272
|
return this.engine.subscribe((event) => {
|
|
1196
1273
|
if (event.runId === runId) onEvent(event);
|
|
@@ -1461,6 +1538,7 @@ var WorkflowRegistrar = class {
|
|
|
1461
1538
|
singleton: meta.singleton,
|
|
1462
1539
|
executionTimeout: meta.executionTimeout,
|
|
1463
1540
|
validateInput,
|
|
1541
|
+
searchAttributesSchema: meta.searchAttributes,
|
|
1464
1542
|
onEvent: getOnEvents(meta, workflowCtor),
|
|
1465
1543
|
eventBatch,
|
|
1466
1544
|
// Uniform dispatch (opt-in): when an in-app worker is configured, register the body GROUP-SERVED
|
|
@@ -1693,6 +1771,47 @@ function assertValidRole(options) {
|
|
|
1693
1771
|
}
|
|
1694
1772
|
}
|
|
1695
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");
|
|
1696
1815
|
var RunGatewayBootstrap = class RunGatewayBootstrap2 {
|
|
1697
1816
|
static {
|
|
1698
1817
|
__name(this, "RunGatewayBootstrap");
|
|
@@ -1753,10 +1872,10 @@ var DurableModule = class _DurableModule {
|
|
|
1753
1872
|
__name(this, "DurableModule");
|
|
1754
1873
|
}
|
|
1755
1874
|
static forRoot(options) {
|
|
1756
|
-
|
|
1875
|
+
_DurableModule.assertValid(options);
|
|
1757
1876
|
return _DurableModule.build({
|
|
1758
1877
|
provide: DURABLE_OPTIONS_CANONICAL7,
|
|
1759
|
-
useValue: options
|
|
1878
|
+
useValue: resolveTopology(options)
|
|
1760
1879
|
});
|
|
1761
1880
|
}
|
|
1762
1881
|
static forRootAsync(options) {
|
|
@@ -1764,12 +1883,26 @@ var DurableModule = class _DurableModule {
|
|
|
1764
1883
|
provide: DURABLE_OPTIONS_CANONICAL7,
|
|
1765
1884
|
useFactory: /* @__PURE__ */ __name(async (...args) => {
|
|
1766
1885
|
const resolved = await options.useFactory(...args);
|
|
1767
|
-
|
|
1768
|
-
return resolved;
|
|
1886
|
+
_DurableModule.assertValid(resolved);
|
|
1887
|
+
return resolveTopology(resolved);
|
|
1769
1888
|
}, "useFactory"),
|
|
1770
1889
|
inject: options.inject ?? []
|
|
1771
1890
|
});
|
|
1772
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
|
+
}
|
|
1773
1906
|
static build(optionsProvider) {
|
|
1774
1907
|
return {
|
|
1775
1908
|
module: _DurableModule,
|
|
@@ -1911,6 +2044,9 @@ var DurableModule = class _DurableModule {
|
|
|
1911
2044
|
DurableModule = _ts_decorate12([
|
|
1912
2045
|
Module({})
|
|
1913
2046
|
], DurableModule);
|
|
2047
|
+
|
|
2048
|
+
// src/index.ts
|
|
2049
|
+
import { WorkflowEngine as WorkflowEngine7 } from "@dudousxd/nestjs-durable-core";
|
|
1914
2050
|
export {
|
|
1915
2051
|
CONTEXT_ACCESSOR,
|
|
1916
2052
|
DEAD_LETTER_METADATA,
|
|
@@ -1947,6 +2083,7 @@ export {
|
|
|
1947
2083
|
ThinWorkflowRegistrar,
|
|
1948
2084
|
WORKFLOW_METADATA,
|
|
1949
2085
|
Workflow,
|
|
2086
|
+
WorkflowEngine7 as WorkflowEngine,
|
|
1950
2087
|
WorkflowService,
|
|
1951
2088
|
entityConfigFor,
|
|
1952
2089
|
getDurableStepMeta,
|