@dudousxd/nestjs-durable 0.30.2 → 0.32.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 +136 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +106 -1
- package/dist/index.d.ts +106 -1
- package/dist/index.js +135 -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,56 @@ 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
|
+
* **One `tenant` word, two axes — the preset maps it to the right one per role:** on a
|
|
531
|
+
* `control-plane` an optional `tenant` scopes the OPERATOR to its own runs (maps to `namespace` —
|
|
532
|
+
* a self-contained local stack sharing a broker with a deployed cluster names itself so neither
|
|
533
|
+
* drives the other's runs; leave it unset on a deployed operator to drive every tenant). On a
|
|
534
|
+
* `tenant` role it is required and maps to `partition` (the worker's queue suffix).
|
|
535
|
+
*
|
|
536
|
+
* ```ts
|
|
537
|
+
* // Control plane: owns the store, dispatches over the transport, prunes old runs. The optional
|
|
538
|
+
* // `tenant` (e.g. from an env var) scopes this operator to its own runs — undefined on a
|
|
539
|
+
* // deployed operator (drives everything), set on a local stack sharing the broker.
|
|
540
|
+
* DurableModule.forRoot({
|
|
541
|
+
* topology: { role: 'control-plane', tenant: process.env.DURABLE_TENANT },
|
|
542
|
+
* store,
|
|
543
|
+
* transport,
|
|
544
|
+
* retention: { policies: [{ statuses: ['completed', 'cancelled'], maxAge: '14d' }] },
|
|
545
|
+
* });
|
|
546
|
+
*
|
|
547
|
+
* // Tenant: store-less worker scoped to its own partition — `tenant` maps to `partition` for you.
|
|
548
|
+
* DurableModule.forRoot({
|
|
549
|
+
* topology: { role: 'tenant', tenant: 'acme-corp' },
|
|
550
|
+
* connection: process.env.REDIS_URL,
|
|
551
|
+
* });
|
|
552
|
+
* ```
|
|
553
|
+
*
|
|
554
|
+
* Validated at `forRoot`/`forRootAsync` resolution time — see {@link DurableModule} for the exact
|
|
555
|
+
* error messages, which double as the axis primer above.
|
|
556
|
+
*/
|
|
557
|
+
topology?: {
|
|
558
|
+
role: 'control-plane';
|
|
559
|
+
tenant?: string;
|
|
560
|
+
} | {
|
|
561
|
+
role: 'tenant';
|
|
562
|
+
tenant: string;
|
|
563
|
+
};
|
|
481
564
|
}
|
|
482
565
|
interface DurableModuleAsyncOptions {
|
|
483
566
|
useFactory: (...args: never[]) => DurableModuleOptions | Promise<DurableModuleOptions>;
|
|
@@ -486,6 +569,14 @@ interface DurableModuleAsyncOptions {
|
|
|
486
569
|
declare class DurableModule {
|
|
487
570
|
static forRoot(options: DurableModuleOptions): DynamicModule;
|
|
488
571
|
static forRootAsync(options: DurableModuleAsyncOptions): DynamicModule;
|
|
572
|
+
/**
|
|
573
|
+
* Validates role/axis constraints, in resolution order. When {@link DurableModuleOptions.topology}
|
|
574
|
+
* is set, it OWNS validation — {@link assertValidTopology}'s own store/transport/connection checks
|
|
575
|
+
* are strictly stronger than {@link assertValidRole}'s, so the topology-specific, axis-teaching
|
|
576
|
+
* message is what a `topology`-opted-in consumer sees (not the older generic one). Falls back to
|
|
577
|
+
* {@link assertValidRole} unchanged when `topology` is absent — zero behavior change.
|
|
578
|
+
*/
|
|
579
|
+
private static assertValid;
|
|
489
580
|
private static build;
|
|
490
581
|
}
|
|
491
582
|
|
|
@@ -722,6 +813,9 @@ declare class ProxyRunGateway implements RunGateway {
|
|
|
722
813
|
listRuns(query: RunQuery): Promise<RunListItem[]>;
|
|
723
814
|
/** Round-trips to the operator, which scopes the result to this tenant's own `@<tenant>` groups. */
|
|
724
815
|
workerHealth(): Promise<GroupHealth[]>;
|
|
816
|
+
/** Bulk, one request for the whole id list (like `listRuns`, not one request per id). The operator
|
|
817
|
+
* filters the reply to runs this tenant actually owns (see `RunRequestResponder`). */
|
|
818
|
+
waitingFor(runIds: string[]): Promise<Record<string, RunWaiting>>;
|
|
725
819
|
cancel(runId: string, opts?: {
|
|
726
820
|
compensate?: boolean;
|
|
727
821
|
}): Promise<RunResult | null>;
|
|
@@ -810,6 +904,17 @@ declare class StoreRunGateway implements RunGateway {
|
|
|
810
904
|
topology(): DurableTopology;
|
|
811
905
|
getRunDetail(runId: string): Promise<RunDetail | null>;
|
|
812
906
|
listRuns(query: RunQuery): Promise<RunListItem[]>;
|
|
907
|
+
/**
|
|
908
|
+
* Bulk-resolve what each of `runIds` is currently parked on — for a consumer with its own filtered/
|
|
909
|
+
* paginated run listing (e.g. "which of MY suspended runs are stuck at a breakpoint") without
|
|
910
|
+
* re-deriving `listRuns`' waiter scan or querying `durable_step_checkpoints` directly. Mirrors
|
|
911
|
+
* `listRuns`' waiting computation (the SAME bulk signal-waiter scan + `resolveRunWaiting`), but ALSO
|
|
912
|
+
* bulk-fetches the currently-suspended runs to check real status: `engine.cancel` (the non-compensate
|
|
913
|
+
* path) doesn't clear a run's signal waiter row, so a cancelled run can leave an ORPHANED waiter
|
|
914
|
+
* behind — trusting waiter presence alone would wrongly report a terminal run as still waiting.
|
|
915
|
+
* Two bulk scans total (never one query per requested id), same as `listRuns`.
|
|
916
|
+
*/
|
|
917
|
+
waitingFor(runIds: string[]): Promise<Record<string, RunWaiting>>;
|
|
813
918
|
/** Every group the engine knows about — unscoped. A tenant proxy's request is scoped by the
|
|
814
919
|
* `RunRequestResponder` (to the requester's `@<tenant>` groups); the operator's own UI sees all. */
|
|
815
920
|
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,56 @@ 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
|
+
* **One `tenant` word, two axes — the preset maps it to the right one per role:** on a
|
|
531
|
+
* `control-plane` an optional `tenant` scopes the OPERATOR to its own runs (maps to `namespace` —
|
|
532
|
+
* a self-contained local stack sharing a broker with a deployed cluster names itself so neither
|
|
533
|
+
* drives the other's runs; leave it unset on a deployed operator to drive every tenant). On a
|
|
534
|
+
* `tenant` role it is required and maps to `partition` (the worker's queue suffix).
|
|
535
|
+
*
|
|
536
|
+
* ```ts
|
|
537
|
+
* // Control plane: owns the store, dispatches over the transport, prunes old runs. The optional
|
|
538
|
+
* // `tenant` (e.g. from an env var) scopes this operator to its own runs — undefined on a
|
|
539
|
+
* // deployed operator (drives everything), set on a local stack sharing the broker.
|
|
540
|
+
* DurableModule.forRoot({
|
|
541
|
+
* topology: { role: 'control-plane', tenant: process.env.DURABLE_TENANT },
|
|
542
|
+
* store,
|
|
543
|
+
* transport,
|
|
544
|
+
* retention: { policies: [{ statuses: ['completed', 'cancelled'], maxAge: '14d' }] },
|
|
545
|
+
* });
|
|
546
|
+
*
|
|
547
|
+
* // Tenant: store-less worker scoped to its own partition — `tenant` maps to `partition` for you.
|
|
548
|
+
* DurableModule.forRoot({
|
|
549
|
+
* topology: { role: 'tenant', tenant: 'acme-corp' },
|
|
550
|
+
* connection: process.env.REDIS_URL,
|
|
551
|
+
* });
|
|
552
|
+
* ```
|
|
553
|
+
*
|
|
554
|
+
* Validated at `forRoot`/`forRootAsync` resolution time — see {@link DurableModule} for the exact
|
|
555
|
+
* error messages, which double as the axis primer above.
|
|
556
|
+
*/
|
|
557
|
+
topology?: {
|
|
558
|
+
role: 'control-plane';
|
|
559
|
+
tenant?: string;
|
|
560
|
+
} | {
|
|
561
|
+
role: 'tenant';
|
|
562
|
+
tenant: string;
|
|
563
|
+
};
|
|
481
564
|
}
|
|
482
565
|
interface DurableModuleAsyncOptions {
|
|
483
566
|
useFactory: (...args: never[]) => DurableModuleOptions | Promise<DurableModuleOptions>;
|
|
@@ -486,6 +569,14 @@ interface DurableModuleAsyncOptions {
|
|
|
486
569
|
declare class DurableModule {
|
|
487
570
|
static forRoot(options: DurableModuleOptions): DynamicModule;
|
|
488
571
|
static forRootAsync(options: DurableModuleAsyncOptions): DynamicModule;
|
|
572
|
+
/**
|
|
573
|
+
* Validates role/axis constraints, in resolution order. When {@link DurableModuleOptions.topology}
|
|
574
|
+
* is set, it OWNS validation — {@link assertValidTopology}'s own store/transport/connection checks
|
|
575
|
+
* are strictly stronger than {@link assertValidRole}'s, so the topology-specific, axis-teaching
|
|
576
|
+
* message is what a `topology`-opted-in consumer sees (not the older generic one). Falls back to
|
|
577
|
+
* {@link assertValidRole} unchanged when `topology` is absent — zero behavior change.
|
|
578
|
+
*/
|
|
579
|
+
private static assertValid;
|
|
489
580
|
private static build;
|
|
490
581
|
}
|
|
491
582
|
|
|
@@ -722,6 +813,9 @@ declare class ProxyRunGateway implements RunGateway {
|
|
|
722
813
|
listRuns(query: RunQuery): Promise<RunListItem[]>;
|
|
723
814
|
/** Round-trips to the operator, which scopes the result to this tenant's own `@<tenant>` groups. */
|
|
724
815
|
workerHealth(): Promise<GroupHealth[]>;
|
|
816
|
+
/** Bulk, one request for the whole id list (like `listRuns`, not one request per id). The operator
|
|
817
|
+
* filters the reply to runs this tenant actually owns (see `RunRequestResponder`). */
|
|
818
|
+
waitingFor(runIds: string[]): Promise<Record<string, RunWaiting>>;
|
|
725
819
|
cancel(runId: string, opts?: {
|
|
726
820
|
compensate?: boolean;
|
|
727
821
|
}): Promise<RunResult | null>;
|
|
@@ -810,6 +904,17 @@ declare class StoreRunGateway implements RunGateway {
|
|
|
810
904
|
topology(): DurableTopology;
|
|
811
905
|
getRunDetail(runId: string): Promise<RunDetail | null>;
|
|
812
906
|
listRuns(query: RunQuery): Promise<RunListItem[]>;
|
|
907
|
+
/**
|
|
908
|
+
* Bulk-resolve what each of `runIds` is currently parked on — for a consumer with its own filtered/
|
|
909
|
+
* paginated run listing (e.g. "which of MY suspended runs are stuck at a breakpoint") without
|
|
910
|
+
* re-deriving `listRuns`' waiter scan or querying `durable_step_checkpoints` directly. Mirrors
|
|
911
|
+
* `listRuns`' waiting computation (the SAME bulk signal-waiter scan + `resolveRunWaiting`), but ALSO
|
|
912
|
+
* bulk-fetches the currently-suspended runs to check real status: `engine.cancel` (the non-compensate
|
|
913
|
+
* path) doesn't clear a run's signal waiter row, so a cancelled run can leave an ORPHANED waiter
|
|
914
|
+
* behind — trusting waiter presence alone would wrongly report a terminal run as still waiting.
|
|
915
|
+
* Two bulk scans total (never one query per requested id), same as `listRuns`.
|
|
916
|
+
*/
|
|
917
|
+
waitingFor(runIds: string[]): Promise<Record<string, RunWaiting>>;
|
|
813
918
|
/** Every group the engine knows about — unscoped. A tenant proxy's request is scoped by the
|
|
814
919
|
* `RunRequestResponder` (to the requester's `@<tenant>` groups); the operator's own UI sees all. */
|
|
815
920
|
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,55 @@ 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 (topology.tenant !== void 0 && options.namespace !== void 0 && options.namespace !== topology.tenant) {
|
|
1785
|
+
throw new Error(`topology: { role: 'control-plane', tenant: '${topology.tenant}' } conflicts with \`namespace: '${options.namespace}'\`.
|
|
1786
|
+
tenant maps 1:1 onto namespace (the operator's poll-scoping axis) \u2014 set only \`tenant\`, or set \`namespace\` to the same value.`);
|
|
1787
|
+
}
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
if (options.connection === void 0) {
|
|
1791
|
+
throw new Error(`topology: { role: 'tenant', tenant: '${topology.tenant}' } needs \`connection\`.
|
|
1792
|
+
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.`);
|
|
1793
|
+
}
|
|
1794
|
+
if (options.store !== void 0) {
|
|
1795
|
+
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).");
|
|
1796
|
+
}
|
|
1797
|
+
if (options.namespace !== void 0) {
|
|
1798
|
+
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.");
|
|
1799
|
+
}
|
|
1800
|
+
if (options.partition !== void 0 && options.partition !== topology.tenant) {
|
|
1801
|
+
throw new Error(`topology: { role: 'tenant', tenant: '${topology.tenant}' } conflicts with \`partition: '${options.partition}'\`.
|
|
1802
|
+
tenant maps 1:1 onto partition (the worker queue-routing suffix) \u2014 set only \`tenant\`, or set \`partition\` to the same value.`);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
__name(assertValidTopology, "assertValidTopology");
|
|
1806
|
+
function resolveTopology(options) {
|
|
1807
|
+
const topology = options.topology;
|
|
1808
|
+
if (topology === void 0) return options;
|
|
1809
|
+
if (topology.role === "control-plane") {
|
|
1810
|
+
if (topology.tenant === void 0 || options.namespace !== void 0) return options;
|
|
1811
|
+
return {
|
|
1812
|
+
...options,
|
|
1813
|
+
namespace: topology.tenant
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
if (options.partition !== void 0) return options;
|
|
1817
|
+
return {
|
|
1818
|
+
...options,
|
|
1819
|
+
partition: topology.tenant
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
__name(resolveTopology, "resolveTopology");
|
|
1710
1823
|
var RunGatewayBootstrap = class RunGatewayBootstrap2 {
|
|
1711
1824
|
static {
|
|
1712
1825
|
__name(this, "RunGatewayBootstrap");
|
|
@@ -1767,10 +1880,10 @@ var DurableModule = class _DurableModule {
|
|
|
1767
1880
|
__name(this, "DurableModule");
|
|
1768
1881
|
}
|
|
1769
1882
|
static forRoot(options) {
|
|
1770
|
-
|
|
1883
|
+
_DurableModule.assertValid(options);
|
|
1771
1884
|
return _DurableModule.build({
|
|
1772
1885
|
provide: DURABLE_OPTIONS_CANONICAL7,
|
|
1773
|
-
useValue: options
|
|
1886
|
+
useValue: resolveTopology(options)
|
|
1774
1887
|
});
|
|
1775
1888
|
}
|
|
1776
1889
|
static forRootAsync(options) {
|
|
@@ -1778,12 +1891,26 @@ var DurableModule = class _DurableModule {
|
|
|
1778
1891
|
provide: DURABLE_OPTIONS_CANONICAL7,
|
|
1779
1892
|
useFactory: /* @__PURE__ */ __name(async (...args) => {
|
|
1780
1893
|
const resolved = await options.useFactory(...args);
|
|
1781
|
-
|
|
1782
|
-
return resolved;
|
|
1894
|
+
_DurableModule.assertValid(resolved);
|
|
1895
|
+
return resolveTopology(resolved);
|
|
1783
1896
|
}, "useFactory"),
|
|
1784
1897
|
inject: options.inject ?? []
|
|
1785
1898
|
});
|
|
1786
1899
|
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Validates role/axis constraints, in resolution order. When {@link DurableModuleOptions.topology}
|
|
1902
|
+
* is set, it OWNS validation — {@link assertValidTopology}'s own store/transport/connection checks
|
|
1903
|
+
* are strictly stronger than {@link assertValidRole}'s, so the topology-specific, axis-teaching
|
|
1904
|
+
* message is what a `topology`-opted-in consumer sees (not the older generic one). Falls back to
|
|
1905
|
+
* {@link assertValidRole} unchanged when `topology` is absent — zero behavior change.
|
|
1906
|
+
*/
|
|
1907
|
+
static assertValid(options) {
|
|
1908
|
+
if (options.topology !== void 0) {
|
|
1909
|
+
assertValidTopology(options);
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
assertValidRole(options);
|
|
1913
|
+
}
|
|
1787
1914
|
static build(optionsProvider) {
|
|
1788
1915
|
return {
|
|
1789
1916
|
module: _DurableModule,
|
|
@@ -1925,6 +2052,9 @@ var DurableModule = class _DurableModule {
|
|
|
1925
2052
|
DurableModule = _ts_decorate12([
|
|
1926
2053
|
Module({})
|
|
1927
2054
|
], DurableModule);
|
|
2055
|
+
|
|
2056
|
+
// src/index.ts
|
|
2057
|
+
import { WorkflowEngine as WorkflowEngine7 } from "@dudousxd/nestjs-durable-core";
|
|
1928
2058
|
export {
|
|
1929
2059
|
CONTEXT_ACCESSOR,
|
|
1930
2060
|
DEAD_LETTER_METADATA,
|
|
@@ -1961,6 +2091,7 @@ export {
|
|
|
1961
2091
|
ThinWorkflowRegistrar,
|
|
1962
2092
|
WORKFLOW_METADATA,
|
|
1963
2093
|
Workflow,
|
|
2094
|
+
WorkflowEngine7 as WorkflowEngine,
|
|
1964
2095
|
WorkflowService,
|
|
1965
2096
|
entityConfigFor,
|
|
1966
2097
|
getDurableStepMeta,
|