@dudousxd/nestjs-durable
Version:
Durable workflows for NestJS — module, decorators, discovery and boot recovery
2,228 lines • 87.9 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
CONTEXT_ACCESSOR: () => CONTEXT_ACCESSOR,
DEAD_LETTER_METADATA: () => DEAD_LETTER_METADATA,
DURABLE_STEP_METADATA: () => DURABLE_STEP_METADATA,
DURABLE_WORKER_RUNNERS: () => DURABLE_WORKER_RUNNERS,
DeadLetter: () => DeadLetter,
DurableModule: () => DurableModule,
DurableStartClient: () => DurableStartClient,
DurableStep: () => DurableStep,
ENTITY_METADATA: () => ENTITY_METADATA,
ENTITY_ON_METADATA: () => ENTITY_ON_METADATA,
Entity: () => Entity,
EntityService: () => EntityService,
IN_APP_RUN_REDIS_WORKER: () => IN_APP_RUN_REDIS_WORKER,
IN_APP_WORKER_BINDING: () => IN_APP_WORKER_BINDING,
IN_APP_WORKER_RUNNERS: () => IN_APP_WORKER_RUNNERS,
IN_APP_WORKER_RUNTIME: () => IN_APP_WORKER_RUNTIME,
InAppWorkerBootstrap: () => InAppWorkerBootstrap,
ON_EVENT_METADATA: () => ON_EVENT_METADATA,
On: () => On,
OnDurableEvent: () => OnDurableEvent,
OnEvent: () => OnEvent,
ProxyRunGateway: () => ProxyRunGateway,
RUN_GATEWAY: () => RUN_GATEWAY,
RUN_REDIS_WORKER: () => RUN_REDIS_WORKER,
RunGateway: () => import_nestjs_durable_core15.RunGateway,
RunRequestResponder: () => RunRequestResponder,
STEP_INTERCEPTOR_METADATA: () => STEP_INTERCEPTOR_METADATA,
Step: () => Step,
StepInterceptor: () => StepInterceptor,
StoreRunGateway: () => StoreRunGateway,
TenantEventRepublisher: () => TenantEventRepublisher,
ThinStepRegistrar: () => ThinStepRegistrar,
ThinWorkerBootstrap: () => ThinWorkerBootstrap,
ThinWorkflowRegistrar: () => ThinWorkflowRegistrar,
WORKFLOW_METADATA: () => WORKFLOW_METADATA,
Workflow: () => Workflow,
WorkflowEngine: () => import_nestjs_durable_core15.WorkflowEngine,
WorkflowService: () => WorkflowService,
attributesOf: () => attributesOf,
entityConfigFor: () => entityConfigFor,
getDurableStepMeta: () => getDurableStepMeta,
getEntityMeta: () => getEntityMeta,
getOnEvents: () => getOnEvents,
getWorkflowMeta: () => getWorkflowMeta,
inAppWorkerProviders: () => inAppWorkerProviders,
isDeadLetterHandler: () => isDeadLetterHandler,
isDrivingOperator: () => isDrivingOperator,
isOperatorRole: () => isOperatorRole,
isStepInterceptor: () => isStepInterceptor,
readSearchAttributes: () => import_nestjs_durable_core15.readSearchAttributes,
thinWorkerProviders: () => thinWorkerProviders,
unavailableRunGateway: () => unavailableRunGateway
});
module.exports = __toCommonJS(index_exports);
// src/attributes-of.ts
var import_nestjs_durable_core2 = require("@dudousxd/nestjs-durable-core");
// src/decorators.ts
var import_nestjs_durable_core = require("@dudousxd/nestjs-durable-core");
var import_reflect_metadata = require("reflect-metadata");
var WORKFLOW_METADATA = Symbol("nestjs-durable:workflow");
function Workflow(options) {
return (target) => {
const meta = {
name: options.name,
version: options.version ?? "1",
deadLetterWorkflow: options.deadLetterWorkflow,
tags: options.tags,
singleton: options.singleton,
executionTimeout: options.executionTimeout,
inputSchema: options.inputSchema,
validateInput: options.validateInput,
searchAttributes: options.searchAttributes,
onEvent: options.onEvent,
debounce: options.debounce,
batch: options.batch,
requires: options.requires
};
Reflect.defineMetadata(WORKFLOW_METADATA, meta, target);
Object.defineProperty(target, import_nestjs_durable_core.WORKFLOW_NAME_KEY, {
value: options.name,
configurable: true
});
};
}
__name(Workflow, "Workflow");
function getWorkflowMeta(target) {
return Reflect.getMetadata(WORKFLOW_METADATA, target);
}
__name(getWorkflowMeta, "getWorkflowMeta");
var DURABLE_STEP_METADATA = Symbol("nestjs-durable:step-handler");
function stepConfigFrom(options) {
const config = {
retries: options.retries,
backoff: options.backoff,
backoffMs: options.backoffMs,
backoffMaxMs: options.backoffMaxMs,
jitter: options.jitter,
timeoutMs: options.timeoutMs,
requires: options.requires
};
const hasAnyField = Object.values(config).some((value) => value !== void 0);
return hasAnyField ? config : void 0;
}
__name(stepConfigFrom, "stepConfigFrom");
function Step(nameOrOptions) {
return (target, propertyKey, descriptor) => {
const options = typeof nameOrOptions === "string" ? {
name: nameOrOptions
} : nameOrOptions ?? {};
const derivedName = options.name ?? `${target.constructor.name}.${String(propertyKey)}`;
const meta = {
name: derivedName,
input: options.input,
output: options.output
};
Reflect.defineMetadata(DURABLE_STEP_METADATA, meta, descriptor.value);
descriptor.value[import_nestjs_durable_core.DURABLE_STEP_NAME] = derivedName;
const config = stepConfigFrom(options);
if (config !== void 0) {
descriptor.value[import_nestjs_durable_core.DURABLE_STEP_CONFIG] = config;
}
return descriptor;
};
}
__name(Step, "Step");
var DurableStep = Step;
function getDurableStepMeta(method) {
return Reflect.getMetadata(DURABLE_STEP_METADATA, method);
}
__name(getDurableStepMeta, "getDurableStepMeta");
var DEAD_LETTER_METADATA = Symbol("nestjs-durable:dead-letter");
function DeadLetter() {
return (_target, _propertyKey, descriptor) => {
Reflect.defineMetadata(DEAD_LETTER_METADATA, true, descriptor.value);
return descriptor;
};
}
__name(DeadLetter, "DeadLetter");
function isDeadLetterHandler(method) {
return Reflect.getMetadata(DEAD_LETTER_METADATA, method) === true;
}
__name(isDeadLetterHandler, "isDeadLetterHandler");
var ON_EVENT_METADATA = Symbol("nestjs-durable:on-event");
function OnDurableEvent(...events) {
return (target) => {
const existing = Reflect.getMetadata(ON_EVENT_METADATA, target) ?? [];
Reflect.defineMetadata(ON_EVENT_METADATA, [
...existing,
...events
], target);
};
}
__name(OnDurableEvent, "OnDurableEvent");
var OnEvent = OnDurableEvent;
function getOnEvents(meta, target) {
const fromDecorator = Reflect.getMetadata(ON_EVENT_METADATA, target) ?? [];
return [
.../* @__PURE__ */ new Set([
...meta.onEvent ?? [],
...fromDecorator
])
];
}
__name(getOnEvents, "getOnEvents");
// src/attributes-of.ts
function attributesOf(workflow, run) {
const meta = getWorkflowMeta(workflow);
if (!meta) {
throw new Error(`attributesOf: ${workflow.name} is not a @Workflow class \u2014 is it decorated with @Workflow({ name, searchAttributes })?`);
}
if (!meta.searchAttributes) {
throw new Error(`attributesOf: workflow '${meta.name}' declares no searchAttributes schema \u2014 reading attributes by class requires the workflow to declare its schema: @Workflow({ name: '${meta.name}', searchAttributes: mySchema }).`);
}
return (0, import_nestjs_durable_core2.readSearchAttributes)(meta.searchAttributes, run);
}
__name(attributesOf, "attributesOf");
// src/durable-start-client.ts
var import_durable_worker = require("@dudousxd/durable-worker");
var import_nestjs_durable_core3 = require("@dudousxd/nestjs-durable-core");
var import_common = require("@nestjs/common");
function _ts_decorate(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
__name(_ts_decorate, "_ts_decorate");
function _ts_metadata(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
}
__name(_ts_metadata, "_ts_metadata");
var DurableStartClient = class {
static {
__name(this, "DurableStartClient");
}
options;
deps;
tenant;
constructor(options, deps) {
this.options = options;
this.deps = deps;
this.tenant = options.partition ?? "default";
}
async start(workflow, input, runId = globalThis.crypto.randomUUID(), opts) {
const name = (0, import_nestjs_durable_core3.workflowName)(workflow);
await (0, import_durable_worker.startRun)(this.options.connection, {
tenant: this.tenant,
workflow: name,
input,
runId,
// Option B: DO NOT pass `namespace` — the start-run queue stays the shared
// `durable-start-run` the operator consumes; tenant rides only as message data.
...this.options.prefix !== void 0 ? {
prefix: this.options.prefix
} : {},
...opts?.tags !== void 0 ? {
tags: opts.tags
} : {},
...opts?.searchAttributes !== void 0 ? {
searchAttributes: opts.searchAttributes
} : {},
...this.deps !== void 0 ? {
deps: this.deps
} : {}
});
return {
runId,
status: "pending"
};
}
cancel(_runId) {
return tenantUnsupported("cancel");
}
deleteRun(_runId) {
return tenantUnsupported("deleteRun");
}
// — everything below rejects: a tenant worker holds no store/driver, only the start channel. —
// The remaining WorkflowEngine surface WorkflowService delegates to (resume/waitForRun/signal/
// signalWithStart/publishEvent) all need the store or driver a tenant does not have. A tenant only
// ever calls `start`; these exist so a mistaken call fails with a CLEAR, named tenant error instead
// of a cryptic `this.engine.X is not a function` — the facade is honest about what it cannot do.
// Params mirror the WorkflowEngine surface being faced; every method rejects without reading them.
resume(_runId) {
return tenantUnsupported("resume");
}
waitForRun(_runId, _opts) {
return tenantUnsupported("waitForRun");
}
signal(_token, _payload) {
return tenantUnsupported("signal");
}
signalWithStart(_workflow, _input, _runId, _signal, _opts) {
return tenantUnsupported("signalWithStart");
}
publishEvent(_name, _payload, _opts) {
return tenantUnsupported("publishEvent");
}
};
DurableStartClient = _ts_decorate([
(0, import_common.Injectable)(),
_ts_metadata("design:type", Function),
_ts_metadata("design:paramtypes", [
typeof DurableModuleOptions === "undefined" ? Object : DurableModuleOptions,
typeof StartRunDeps === "undefined" ? Object : StartRunDeps
])
], DurableStartClient);
function tenantUnsupported(method) {
return Promise.reject(new Error(`${method}() is not available on a tenant worker (no store). Use the control plane for it.`));
}
__name(tenantUnsupported, "tenantUnsupported");
// src/durable-worker.module.ts
var import_durable_worker2 = require("@dudousxd/durable-worker");
var import_nestjs_durable_core4 = require("@dudousxd/nestjs-durable-core");
var import_common2 = require("@nestjs/common");
var import_core = require("@nestjs/core");
// src/discovery-helpers.ts
function scanWorkflows(discovery, register) {
for (const wrapper of discovery.getProviders()) {
const { instance } = wrapper;
if (!instance || typeof instance !== "object") continue;
const meta = getWorkflowMeta(instance.constructor);
if (!meta) continue;
const workflow = instance;
if (typeof workflow.run !== "function") {
throw new Error(`@Workflow ${meta.name} must define a run(ctx, input) method`);
}
register(meta, workflow);
}
}
__name(scanWorkflows, "scanWorkflows");
function scanSteps(discovery, scanner, register) {
for (const wrapper of discovery.getProviders()) {
const { instance } = wrapper;
if (!instance || typeof instance !== "object") continue;
const prototype = Object.getPrototypeOf(instance);
for (const methodName of scanner.getAllMethodNames(prototype)) {
const method = instance[methodName];
if (typeof method !== "function") continue;
const meta = getDurableStepMeta(method);
if (!meta) continue;
const boundMethod = method;
const handler = /* @__PURE__ */ __name(async (input, log) => {
const validInput = meta.input ? meta.input.parse(input) : input;
const output = await boundMethod.call(instance, validInput, log);
return meta.output ? meta.output.parse(output) : output;
}, "handler");
register(meta, handler);
}
}
}
__name(scanSteps, "scanSteps");
// src/durable-worker.module.ts
function _ts_decorate2(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
__name(_ts_decorate2, "_ts_decorate");
function _ts_metadata2(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
}
__name(_ts_metadata2, "_ts_metadata");
function _ts_param(paramIndex, decorator) {
return function(target, key) {
decorator(target, key, paramIndex);
};
}
__name(_ts_param, "_ts_param");
var RUN_REDIS_WORKER = Symbol("nestjs-durable:run-redis-worker");
var DURABLE_WORKER_RUNNERS = Symbol("nestjs-durable:worker-runners");
function isPureThinWorker(options) {
return options.store === void 0 && options.connection !== void 0;
}
__name(isPureThinWorker, "isPureThinWorker");
var ThinWorkflowRegistrar = class {
static {
__name(this, "ThinWorkflowRegistrar");
}
discovery;
runtime;
options;
constructor(discovery, runtime, options) {
this.discovery = discovery;
this.runtime = runtime;
this.options = options;
}
onModuleInit() {
if (!isPureThinWorker(this.options)) return;
scanWorkflows(this.discovery, (meta, instance) => this.runtime.registerWorkflow(meta.name, (ctx, input) => instance.run(ctx, input)));
}
};
ThinWorkflowRegistrar = _ts_decorate2([
(0, import_common2.Injectable)(),
_ts_param(2, (0, import_common2.Inject)(import_nestjs_durable_core4.DURABLE_OPTIONS_CANONICAL)),
_ts_metadata2("design:type", Function),
_ts_metadata2("design:paramtypes", [
typeof import_core.DiscoveryService === "undefined" ? Object : import_core.DiscoveryService,
typeof import_durable_worker2.DurableWorkerRuntime === "undefined" ? Object : import_durable_worker2.DurableWorkerRuntime,
typeof DurableModuleOptions === "undefined" ? Object : DurableModuleOptions
])
], ThinWorkflowRegistrar);
var ThinStepRegistrar = class {
static {
__name(this, "ThinStepRegistrar");
}
discovery;
metadataScanner;
runtime;
options;
constructor(discovery, metadataScanner, runtime, options) {
this.discovery = discovery;
this.metadataScanner = metadataScanner;
this.runtime = runtime;
this.options = options;
}
onModuleInit() {
if (!isPureThinWorker(this.options)) return;
scanSteps(this.discovery, this.metadataScanner, (meta, handler) => this.runtime.registerStep(meta.name, handler));
}
};
ThinStepRegistrar = _ts_decorate2([
(0, import_common2.Injectable)(),
_ts_param(3, (0, import_common2.Inject)(import_nestjs_durable_core4.DURABLE_OPTIONS_CANONICAL)),
_ts_metadata2("design:type", Function),
_ts_metadata2("design:paramtypes", [
typeof import_core.DiscoveryService === "undefined" ? Object : import_core.DiscoveryService,
typeof import_core.MetadataScanner === "undefined" ? Object : import_core.MetadataScanner,
typeof import_durable_worker2.DurableWorkerRuntime === "undefined" ? Object : import_durable_worker2.DurableWorkerRuntime,
typeof DurableModuleOptions === "undefined" ? Object : DurableModuleOptions
])
], ThinStepRegistrar);
var ThinWorkerBootstrap = class {
static {
__name(this, "ThinWorkerBootstrap");
}
runtime;
options;
runRedisWorker;
runnersSink;
runners = [];
constructor(runtime, options, runRedisWorker, runnersSink) {
this.runtime = runtime;
this.options = options;
this.runRedisWorker = runRedisWorker;
this.runnersSink = runnersSink;
}
async onApplicationBootstrap() {
const options = this.options;
if (!isPureThinWorker(options) || options.connection === void 0) return;
const handle = await this.runRedisWorker({
runtime: this.runtime,
connection: options.connection,
...options.partition !== void 0 ? {
partition: options.partition
} : {},
...options.prefix !== void 0 ? {
prefix: options.prefix
} : {},
...options.instanceId !== void 0 ? {
instanceId: options.instanceId
} : {},
...options.concurrency !== void 0 ? {
concurrency: options.concurrency
} : {}
});
this.runners.push(handle);
this.runnersSink.push(handle);
}
async onApplicationShutdown() {
await Promise.allSettled(this.runners.map((h) => h.close()));
}
};
ThinWorkerBootstrap = _ts_decorate2([
(0, import_common2.Injectable)(),
_ts_param(1, (0, import_common2.Inject)(import_nestjs_durable_core4.DURABLE_OPTIONS_CANONICAL)),
_ts_param(2, (0, import_common2.Inject)(RUN_REDIS_WORKER)),
_ts_param(3, (0, import_common2.Inject)(DURABLE_WORKER_RUNNERS)),
_ts_metadata2("design:type", Function),
_ts_metadata2("design:paramtypes", [
typeof import_durable_worker2.DurableWorkerRuntime === "undefined" ? Object : import_durable_worker2.DurableWorkerRuntime,
typeof DurableModuleOptions === "undefined" ? Object : DurableModuleOptions,
typeof RunRedisWorkerFn === "undefined" ? Object : RunRedisWorkerFn,
Array
])
], ThinWorkerBootstrap);
function thinWorkerProviders() {
return [
{
provide: import_durable_worker2.DurableWorkerRuntime,
// The runtime's `WorkflowWorker` falls back to its own `group` ctor param as the WORKFLOW's
// `workflowPartition` for any `ctx.step` call (see `workflow-context.ts`'s `resolveCallGroup`)
// — that fallback MUST equal this module's own `partition`, or a dispatched step's decision
// carries a mismatched token and the engine dispatches it to a queue nothing here subscribes
// to. Explicit `''` (never
// `undefined`) so it does NOT fall through to `WorkflowWorker`'s unrelated `'workflows'`
// default parameter.
useFactory: /* @__PURE__ */ __name((options) => new import_durable_worker2.DurableWorkerRuntime({
workflowGroup: options.partition ?? ""
}), "useFactory"),
inject: [
import_nestjs_durable_core4.DURABLE_OPTIONS_CANONICAL
]
},
{
provide: RUN_REDIS_WORKER,
useValue: import_durable_worker2.runRedisWorker
},
{
provide: DURABLE_WORKER_RUNNERS,
useValue: []
},
ThinWorkflowRegistrar,
ThinStepRegistrar,
ThinWorkerBootstrap
];
}
__name(thinWorkerProviders, "thinWorkerProviders");
function tenantGatewayUnavailable(method) {
return Promise.reject(new Error(`RunGateway.${method}() is not available \u2014 pass \`transport\` to use RunGateway.`));
}
__name(tenantGatewayUnavailable, "tenantGatewayUnavailable");
function unavailableRunGateway() {
return {
// Topology is local metadata, always answerable — a thin worker with no transport is still a tenant.
topology() {
return {
role: "tenant"
};
},
getRunDetail(_runId) {
return tenantGatewayUnavailable("getRunDetail");
},
listRuns(_query) {
return tenantGatewayUnavailable("listRuns");
},
waitingFor(_runIds) {
return tenantGatewayUnavailable("waitingFor");
},
workerHealth() {
return tenantGatewayUnavailable("workerHealth");
},
cancel(_runId) {
return tenantGatewayUnavailable("cancel");
},
retry(_runId) {
return tenantGatewayUnavailable("retry");
},
continue(_runId) {
return tenantGatewayUnavailable("continue");
},
retryWithInput(_runId, _input) {
return tenantGatewayUnavailable("retryWithInput");
},
redispatchPending(_runId) {
return tenantGatewayUnavailable("redispatchPending");
},
subscribe(_runId, _onEvent) {
throw new Error("RunGateway.subscribe() is not available \u2014 pass `transport` to use RunGateway.");
}
};
}
__name(unavailableRunGateway, "unavailableRunGateway");
// src/durable.module.ts
var import_nestjs_durable_core14 = require("@dudousxd/nestjs-durable-core");
var import_common12 = require("@nestjs/common");
var import_core5 = require("@nestjs/core");
// src/durable-step.registrar.ts
var import_nestjs_durable_core5 = require("@dudousxd/nestjs-durable-core");
var import_common3 = require("@nestjs/common");
var import_core2 = require("@nestjs/core");
// src/role.ts
function isOperatorRole(options) {
return options.store !== void 0;
}
__name(isOperatorRole, "isOperatorRole");
function isDrivingOperator(options) {
return isOperatorRole(options) && options.drive !== false;
}
__name(isDrivingOperator, "isDrivingOperator");
// src/durable-step.registrar.ts
function _ts_decorate3(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
__name(_ts_decorate3, "_ts_decorate");
function _ts_metadata3(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
}
__name(_ts_metadata3, "_ts_metadata");
function _ts_param2(paramIndex, decorator) {
return function(target, key) {
decorator(target, key, paramIndex);
};
}
__name(_ts_param2, "_ts_param");
function supportsHandle(transport) {
return typeof transport?.handle === "function";
}
__name(supportsHandle, "supportsHandle");
var DurableStepRegistrar = class {
static {
__name(this, "DurableStepRegistrar");
}
discovery;
metadataScanner;
transport;
options;
constructor(discovery, metadataScanner, transport, options) {
this.discovery = discovery;
this.metadataScanner = metadataScanner;
this.transport = transport;
this.options = options;
}
onModuleInit() {
if (!isDrivingOperator(this.options)) return;
if (!supportsHandle(this.transport)) return;
const transport = this.transport;
scanSteps(this.discovery, this.metadataScanner, (meta, handler) => transport.handle(meta.name, handler, this.options.partition));
}
};
DurableStepRegistrar = _ts_decorate3([
(0, import_common3.Injectable)(),
_ts_param2(2, (0, import_common3.Inject)(import_nestjs_durable_core5.TRANSPORT_CANONICAL)),
_ts_param2(3, (0, import_common3.Inject)(import_nestjs_durable_core5.DURABLE_OPTIONS_CANONICAL)),
_ts_metadata3("design:type", Function),
_ts_metadata3("design:paramtypes", [
typeof import_core2.DiscoveryService === "undefined" ? Object : import_core2.DiscoveryService,
typeof import_core2.MetadataScanner === "undefined" ? Object : import_core2.MetadataScanner,
Object,
typeof DurableModuleOptions === "undefined" ? Object : DurableModuleOptions
])
], DurableStepRegistrar);
// src/entity.ts
var import_nestjs_durable_core6 = require("@dudousxd/nestjs-durable-core");
var import_common4 = require("@nestjs/common");
var import_reflect_metadata2 = require("reflect-metadata");
function _ts_decorate4(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
__name(_ts_decorate4, "_ts_decorate");
function _ts_metadata4(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
}
__name(_ts_metadata4, "_ts_metadata");
var ENTITY_METADATA = Symbol("nestjs-durable:entity");
var ENTITY_ON_METADATA = Symbol("nestjs-durable:entity-on");
function Entity(options) {
return (target) => {
Reflect.defineMetadata(ENTITY_METADATA, options, target);
};
}
__name(Entity, "Entity");
function On(op) {
return (target, propertyKey) => {
const ctor = target.constructor;
const ops = Reflect.getMetadata(ENTITY_ON_METADATA, ctor) ?? /* @__PURE__ */ new Map();
ops.set(op, propertyKey);
Reflect.defineMetadata(ENTITY_ON_METADATA, ops, ctor);
};
}
__name(On, "On");
function getEntityMeta(target) {
return Reflect.getMetadata(ENTITY_METADATA, target);
}
__name(getEntityMeta, "getEntityMeta");
function entityConfigFor(ctor) {
const ops = Reflect.getMetadata(ENTITY_ON_METADATA, ctor) ?? /* @__PURE__ */ new Map();
const Cls = ctor;
const handlers = {};
for (const [op, method] of ops) {
handlers[op] = (state, arg) => {
Object.setPrototypeOf(state, Cls.prototype);
const fn = state[method];
if (typeof fn !== "function") throw new Error(`entity handler "${method}" is not a method`);
return fn.call(state, arg);
};
}
return {
initialState: /* @__PURE__ */ __name(() => new Cls(), "initialState"),
handlers
};
}
__name(entityConfigFor, "entityConfigFor");
var EntityService = class {
static {
__name(this, "EntityService");
}
engine;
constructor(engine) {
this.engine = engine;
}
/** Send an operation to an entity (fire-and-forget; ordered + exactly-once per key). */
signal(name, key, op, arg) {
return this.engine.signalEntity(name, key, op, arg);
}
/** Read an entity's current durable state (or undefined if it has none yet). */
getState(name, key) {
return this.engine.getEntityState(name, key);
}
};
EntityService = _ts_decorate4([
(0, import_common4.Injectable)(),
_ts_metadata4("design:type", Function),
_ts_metadata4("design:paramtypes", [
typeof import_nestjs_durable_core6.WorkflowEngine === "undefined" ? Object : import_nestjs_durable_core6.WorkflowEngine
])
], EntityService);
// src/in-app-worker.ts
var import_durable_worker3 = require("@dudousxd/durable-worker");
var import_nestjs_durable_core7 = require("@dudousxd/nestjs-durable-core");
var import_common5 = require("@nestjs/common");
var import_core3 = require("@nestjs/core");
function _ts_decorate5(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
__name(_ts_decorate5, "_ts_decorate");
function _ts_metadata5(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
}
__name(_ts_metadata5, "_ts_metadata");
function _ts_param3(paramIndex, decorator) {
return function(target, key) {
decorator(target, key, paramIndex);
};
}
__name(_ts_param3, "_ts_param");
var IN_APP_WORKER_BINDING = Symbol("nestjs-durable:in-app-worker-binding");
var IN_APP_WORKER_RUNTIME = Symbol("nestjs-durable:in-app-worker-runtime");
var IN_APP_RUN_REDIS_WORKER = Symbol("nestjs-durable:in-app-run-redis-worker");
var IN_APP_WORKER_RUNNERS = Symbol("nestjs-durable:in-app-worker-runners");
function isCoLocatedWorker(options) {
return options.store !== void 0 && options.connection !== void 0;
}
__name(isCoLocatedWorker, "isCoLocatedWorker");
function inAppWorkerBinding(transport, options) {
if (!isCoLocatedWorker(options)) return null;
if (!transport?.dispatchWorkflowTask || !transport.onDecision) {
throw new Error("a co-located worker (store + connection) requires a transport that carries workflow tasks (dispatchWorkflowTask + onDecision), e.g. BullMQTransport. An in-process transport cannot serve a group-served workflow.");
}
return {
transport,
...options.partition !== void 0 ? {
partition: options.partition
} : {}
};
}
__name(inAppWorkerBinding, "inAppWorkerBinding");
var InAppWorkerBootstrap = class {
static {
__name(this, "InAppWorkerBootstrap");
}
discovery;
metadataScanner;
options;
runtime;
runRedisWorker;
runnersSink;
runners = [];
constructor(discovery, metadataScanner, options, runtime, runRedisWorker, runnersSink) {
this.discovery = discovery;
this.metadataScanner = metadataScanner;
this.options = options;
this.runtime = runtime;
this.runRedisWorker = runRedisWorker;
this.runnersSink = runnersSink;
}
onModuleInit() {
if (!isCoLocatedWorker(this.options)) return;
scanWorkflows(this.discovery, (meta, instance) => this.runtime.registerWorkflow(meta.name, (ctx, input) => instance.run(ctx, input)));
scanSteps(this.discovery, this.metadataScanner, (meta, handler) => this.runtime.registerStep(meta.name, handler));
}
async onApplicationBootstrap() {
const options = this.options;
if (!isCoLocatedWorker(options) || options.connection === void 0) return;
const handle = await this.runRedisWorker({
runtime: this.runtime,
connection: options.connection,
...options.partition !== void 0 ? {
partition: options.partition
} : {},
...options.prefix !== void 0 ? {
prefix: options.prefix
} : {},
...options.instanceId !== void 0 ? {
instanceId: options.instanceId
} : {},
...options.concurrency !== void 0 ? {
concurrency: options.concurrency
} : {}
});
this.runners.push(handle);
this.runnersSink.push(handle);
}
async onApplicationShutdown() {
await Promise.allSettled(this.runners.map((handle) => handle.close()));
}
};
InAppWorkerBootstrap = _ts_decorate5([
(0, import_common5.Injectable)(),
_ts_param3(2, (0, import_common5.Inject)(import_nestjs_durable_core7.DURABLE_OPTIONS_CANONICAL)),
_ts_param3(3, (0, import_common5.Inject)(IN_APP_WORKER_RUNTIME)),
_ts_param3(4, (0, import_common5.Inject)(IN_APP_RUN_REDIS_WORKER)),
_ts_param3(5, (0, import_common5.Inject)(IN_APP_WORKER_RUNNERS)),
_ts_metadata5("design:type", Function),
_ts_metadata5("design:paramtypes", [
typeof import_core3.DiscoveryService === "undefined" ? Object : import_core3.DiscoveryService,
typeof import_core3.MetadataScanner === "undefined" ? Object : import_core3.MetadataScanner,
typeof DurableModuleOptions === "undefined" ? Object : DurableModuleOptions,
typeof import_durable_worker3.DurableWorkerRuntime === "undefined" ? Object : import_durable_worker3.DurableWorkerRuntime,
typeof RunRedisWorkerFn === "undefined" ? Object : RunRedisWorkerFn,
Array
])
], InAppWorkerBootstrap);
function inAppWorkerProviders() {
return [
{
provide: IN_APP_WORKER_BINDING,
useFactory: /* @__PURE__ */ __name((transport, options) => inAppWorkerBinding(transport, options), "useFactory"),
inject: [
import_nestjs_durable_core7.TRANSPORT_CANONICAL,
import_nestjs_durable_core7.DURABLE_OPTIONS_CANONICAL
]
},
{
provide: IN_APP_WORKER_RUNTIME,
// The runtime's `WorkflowWorker` uses its `group` ctor param as the WORKFLOW's
// `workflowPartition` for any `ctx.step` call (see `workflow-context.ts`'s `resolveCallGroup`)
// — that fallback MUST equal this app's own `partition`, or a dispatched step's decision
// carries a mismatched token and the engine dispatches it to a queue nothing here subscribes
// to. Explicit `''` (never
// `undefined`) so it does NOT fall through to `WorkflowWorker`'s unrelated `'workflows'`
// default parameter.
useFactory: /* @__PURE__ */ __name((options) => new import_durable_worker3.DurableWorkerRuntime({
workflowGroup: options.partition ?? ""
}), "useFactory"),
inject: [
import_nestjs_durable_core7.DURABLE_OPTIONS_CANONICAL
]
},
{
provide: IN_APP_RUN_REDIS_WORKER,
useValue: import_durable_worker3.runRedisWorker
},
{
provide: IN_APP_WORKER_RUNNERS,
useValue: []
},
InAppWorkerBootstrap
];
}
__name(inAppWorkerProviders, "inAppWorkerProviders");
// src/proxy-run-gateway.ts
var import_common6 = require("@nestjs/common");
function _ts_decorate6(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
__name(_ts_decorate6, "_ts_decorate");
function _ts_metadata6(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
}
__name(_ts_metadata6, "_ts_metadata");
var ProxyRunGateway = class {
static {
__name(this, "ProxyRunGateway");
}
transport;
tenant;
timeoutMs;
pending = /* @__PURE__ */ new Map();
constructor(transport, tenant, timeoutMs = 1e4) {
this.transport = transport;
this.tenant = tenant;
this.timeoutMs = timeoutMs;
this.transport.onRunReply?.((reply) => this.handleReply(reply));
}
handleReply(reply) {
const pending = this.pending.get(reply.requestId);
if (!pending) return;
clearTimeout(pending.timer);
this.pending.delete(reply.requestId);
if (reply.result.ok) {
pending.resolve(reply.result.data);
} else {
pending.reject(new Error(reply.result.error.message));
}
}
request(body) {
return new Promise((resolve, reject) => {
const requestId = globalThis.crypto.randomUUID();
const timer = setTimeout(() => {
this.pending.delete(requestId);
reject(new Error(`control plane did not respond to ${body.kind} within ${this.timeoutMs}ms`));
}, this.timeoutMs);
this.pending.set(requestId, {
resolve,
reject,
timer
});
this.transport.dispatchRunRequest?.({
requestId,
tenant: this.tenant,
body
}).catch((error) => {
const stillPending = this.pending.get(requestId);
if (!stillPending) return;
clearTimeout(stillPending.timer);
this.pending.delete(requestId);
stillPending.reject(error instanceof Error ? error : new Error(String(error)));
});
});
}
topology() {
return {
role: "tenant",
tenant: this.tenant
};
}
getRunDetail(runId) {
return this.request({
kind: "getRunDetail",
runId
});
}
/** The control plane's `StoreRunGateway` resolves each run's `waiting` descriptor; the reply carries
* it through as plain JSON, so a tenant's list rows name the wait too. */
listRuns(query) {
return this.request({
kind: "listRuns",
query
});
}
/** Round-trips to the operator, which scopes the result to this tenant's own `@<tenant>` groups. */
workerHealth() {
return this.request({
kind: "workerHealth"
});
}
/** Bulk, one request for the whole id list (like `listRuns`, not one request per id). The operator
* filters the reply to runs this tenant actually owns (see `RunRequestResponder`). */
waitingFor(runIds) {
return this.request({
kind: "waitingFor",
runIds
});
}
cancel(runId, opts) {
return this.request(opts === void 0 ? {
kind: "cancel",
runId
} : {
kind: "cancel",
runId,
opts
});
}
retry(runId) {
return this.request({
kind: "retry",
runId
});
}
continue(runId) {
return this.request({
kind: "continue",
runId
});
}
retryWithInput(runId, input) {
return this.request({
kind: "retryWithInput",
runId,
input
});
}
redispatchPending(runId) {
return this.request({
kind: "redispatch",
runId
});
}
subscribe(runId, onEvent) {
const unsubscribe = this.transport.onTenantEvent?.(this.tenant, (evt) => {
if (evt.event.runId === runId) onEvent(evt.event);
});
return unsubscribe ?? (() => {
});
}
};
ProxyRunGateway = _ts_decorate6([
(0, import_common6.Injectable)(),
_ts_metadata6("design:type", Function),
_ts_metadata6("design:paramtypes", [
typeof Transport === "undefined" ? Object : Transport,
String,
void 0
])
], ProxyRunGateway);
// src/retention-poller.ts
var import_nestjs_durable_core8 = require("@dudousxd/nestjs-durable-core");
var import_common7 = require("@nestjs/common");
function _ts_decorate7(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
__name(_ts_decorate7, "_ts_decorate");
function _ts_metadata7(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
}
__name(_ts_metadata7, "_ts_metadata");
function _ts_param4(paramIndex, decorator) {
return function(target, key) {
decorator(target, key, paramIndex);
};
}
__name(_ts_param4, "_ts_param");
var DEFAULT_SWEEP_INTERVAL_MS = 6e4;
var DEFAULT_BATCH_SIZE = 1e3;
var MAX_BATCHES_PER_POLICY = 100;
function validateRetention(retention) {
const seen = /* @__PURE__ */ new Set();
for (const policy of retention.policies) {
if (policy.statuses.length === 0) {
throw new Error("durable retention: each policy must list at least one status");
}
if (policy.maxAge == null && policy.maxCount == null) {
throw new Error("durable retention: each policy must set maxAge and/or maxCount");
}
if (policy.maxAge != null) (0, import_nestjs_durable_core8.parseDuration)(policy.maxAge);
for (const status of policy.statuses) {
if (!import_nestjs_durable_core8.TERMINAL_RUN_STATUSES.includes(status)) {
throw new Error(`durable retention: status "${status}" is not terminal; only ${import_nestjs_durable_core8.TERMINAL_RUN_STATUSES.join(", ")} can be pruned`);
}
if (seen.has(status)) {
throw new Error(`durable retention: status "${status}" appears in more than one policy; status sets must be disjoint`);
}
seen.add(status);
}
}
}
__name(validateRetention, "validateRetention");
var RetentionPoller = class {
static {
__name(this, "RetentionPoller");
}
store;
options;
timer;
sweeping = false;
constructor(store, options) {
this.store = store;
this.options = options;
}
/** Non-null once past the `isDrivingOperator` guard (which requires `options.store` to be set). */
requireStore() {
if (!this.store) {
throw new Error("unreachable: STATE_STORE_CANONICAL must resolve a store when options.store is set");
}
return this.store;
}
async onApplicationBootstrap() {
if (!isDrivingOperator(this.options)) return;
const retention = this.options.retention;
if (!retention || retention.policies.length === 0) return;
validateRetention(retention);
if (typeof this.requireStore().pruneTerminalRuns !== "function") {
console.warn("[nestjs-durable] `retention` is configured but the store adapter does not implement pruneTerminalRuns; retention is disabled.");
return;
}
await this.sweep();
const intervalMs = retention.sweepInterval != null ? (0, import_nestjs_durable_core8.parseDuration)(retention.sweepInterval) : DEFAULT_SWEEP_INTERVAL_MS;
if (intervalMs > 0) {
this.timer = setInterval(() => void this.sweep(), intervalMs);
this.timer.unref?.();
}
}
onModuleDestroy() {
if (this.timer) clearInterval(this.timer);
}
async sweep() {
if (this.sweeping) return;
const retention = this.options.retention;
const store = this.requireStore();
const prune = store.pruneTerminalRuns;
if (!retention || typeof prune !== "function") return;
this.sweeping = true;
try {
const batchSize = retention.batchSize ?? DEFAULT_BATCH_SIZE;
const now = Date.now();
for (const policy of retention.policies) {
for (let batch = 0; batch < MAX_BATCHES_PER_POLICY; batch++) {
const deleted = await prune.call(store, policy, now, batchSize);
if (deleted < batchSize) break;
}
}
} finally {
this.sweeping = false;
}
}
};
RetentionPoller = _ts_decorate7([
(0, import_common7.Injectable)(),
_ts_param4(0, (0, import_common7.Inject)(import_nestjs_durable_core8.STATE_STORE_CANONICAL)),
_ts_param4(1, (0, import_common7.Inject)(import_nestjs_durable_core8.DURABLE_OPTIONS_CANONICAL)),
_ts_metadata7("design:type", Function),
_ts_metadata7("design:paramtypes", [
Object,
typeof DurableModuleOptions === "undefined" ? Object : DurableModuleOptions
])
], RetentionPoller);
// src/run-request-responder.ts
var RunRequestResponder = class {
static {
__name(this, "RunRequestResponder");
}
transport;
gateway;
constructor(transport, gateway) {
this.transport = transport;
this.gateway = gateway;
}
/** Register the consumer on the transport. Each request is answered independently; a handler
* failure never throws back into the transport (errors are captured into an error reply). */
start() {
this.transport.onRunRequest(async (msg) => {
const reply = await this.handle(msg);
await this.transport.publishRunReply(reply);
});
}
async handle(msg) {
const { body } = msg;
if (body.kind === "listRuns") {
const data = await this.gateway.listRuns({
...body.query,
namespace: msg.tenant
});
return {
requestId: msg.requestId,
result: {
ok: true,
data
}
};
}
if (body.kind === "workerHealth") {
const all = await this.gateway.workerHealth();
const data = all.filter((h) => h.group.endsWith(`@${msg.tenant}`));
return {
requestId: msg.requestId,
result: {
ok: true,
data
}
};
}
if (body.kind === "waitingFor") {
const all = await this.gateway.waitingFor(body.runIds);
const owned = await Promise.all(Object.entries(all).map(async ([runId, waiting]) => {
const runDetail = await this.gateway.getRunDetail(runId);
return runDetail && runDetail.run.namespace === msg.tenant ? [
runId,
waiting
] : void 0;
}));
const data = {};
for (const entry of owned) {
if (entry) data[entry[0]] = entry[1];
}
return {
requestId: msg.requestId,
result: {
ok: true,
data
}
};
}
const detail = await this.gateway.getRunDetail(body.runId);
if (detail && detail.run.namespace !== msg.tenant) {
return {
requestId: msg.requestId,
result: {
ok: false,
error: {
message: "run belongs to another tenant",
code: "cross-tenant"
}
}
};
}
if (body.kind === "getRunDetail") {
return {
requestId: msg.requestId,
result: {
ok: true,
data: detail
}
};
}
try {
const data = await this.callVerb(body);
return {
requestId: msg.requestId,
result: {
ok: true,
data
}
};
} catch (err) {
return {
requestId: msg.requestId,
result: {
ok: false,
error: {
message: err instanceof Error ? err.message : String(err)
}
}
};
}
}
callVerb(body) {
switch (body.kind) {
case "cancel":
return this.gateway.cancel(body.runId, body.opts);
case "retry":
return this.gateway.retry(body.runId);
case "continue":
return this.gateway.continue(body.runId);
case "retryWithInput":
return this.gateway.retryWithInput(body.runId, body.input);
case "redispatch":
return this.gateway.redispatchPending(body.runId);
}
}
};
// src/store-run-gateway.ts
var import_nestjs_durable_core9 = require("@dudousxd/nestjs-durable-core");
var import_common8 = require("@nestjs/common");
function _ts_decorate8(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
__name(_ts_decorate8, "_ts_decorate");
function _ts_metadata8(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
}
__name(_ts_metadata8, "_ts_metadata");
function _ts_param5(paramIndex, decorator) {
return function(target, key) {
decorator(target, key, paramIndex);
};
}
__name(_ts_param5, "_ts_param");
var StoreRunGateway = class {
static {
__name(this, "StoreRunGateway");
}
store;
engine;
constructor(store, engine) {
this.store = store;
this.engine = engine;
}
topology() {
return {
role: "control-plane"
};
}
async getRunDetail(runId) {
const run = await this.store.getRun(runId);
if (!run) return null;
const [timeline, children] = await Promise.all([
this.store.listCheckpoints(runId),
this.engine.getRunChildren(runId)
]);
return {
run,
timeline,
children
};
}
async listRuns(query) {
const runs = await this.store.listRuns(query);
const waiterByRun = (0, import_nestjs_durable_core9.indexWaitersByRun)(await this.store.listSignalWaiters(""));
return runs.map((run) => {
const waiting = (0, import_nestjs_durable_core9.resolveRunWaiting)(run, waiterByRun);
return waiting ? {
...run,
waiting
} : run;
});
}
/**
* Bulk-resolve what each of `runIds` is currently parked on — for a consumer with its own filtered/
* paginated run listing (e.g. "which of MY suspended runs are stuck at a breakpoint") without
* re-deriving `listRuns`' waiter scan or querying `durable_step_checkpoints` directly. Mirrors
* `listRuns`' waiting computation (the SAME bulk signal-waiter scan + `resolveRunWaiting`), but ALSO
* bulk-fetches the currently-suspended runs to check real status: `engine.cancel` (the non-compensate
* path) doesn't clear a run's signal waiter row, so a cancelled run can leave an ORPHANED waiter
* behind — trusting waiter presence alone would wrongly report a terminal run as still waiting.
* Two bulk scans total (never one query per requested id), same as `listRuns`.
*/
async waitingFor(runIds) {
if (runIds.length === 0) return {};
const idSet = new Set(runIds);
const [suspended, waiters] = await Promise.all([
this.store.listRuns({
statuses: [
"suspended"
]
}),
this.store.listSignalWaiters("")
]);
const waiterByRun = (0, import_nestjs_durable_core9.indexWaitersByRun)(waiters);
const result = {};
for (const run of suspended) {
if (!idSet.has(run.id)) continue;
const waiting = (0, import_nestjs_durable_core9.resolveRunWaiting)(run, waiterByRun);
if (waiting) result[run.id] = waiting;
}
return result;
}
/** Every group the engine knows about — unscoped. A tenant proxy's request is scoped by the
* `RunRequestResponder` (to the requester's `@<tenant>` groups); the operator's own UI sees all. */
workerHealth() {
return this.engine.workerHealth();
}
cancel(runId, opts) {
return this.engine.cancel(runId, opts);
}
/** Re-enqueue (dispatch model) instead of resuming inline — a worker picks the run up and replays it. */
retry(runId) {
return this.engine.requeue(runId);
}
continue(runId) {
return this.engine.continue(runId);
}
retryWithInput(runId, input) {
return this.engine.retryWithInput(runId, input);
}
redispatchPending(runId) {
return this.engine.redispatchPending(runId);
}
subscribe(runId, onEvent) {
return this.engine.subscribe((event) => {
if (event.runId === runId) onEvent(event);
});
}
};
StoreRunGateway = _ts_decorate8([
(0, import_common8.Injectable)(),
_ts_param5(0, (0, import_common8.Inject)(import_nestjs_durable_core9.STATE_STORE_CANONICAL)),
_ts_metadata8("design:type", Function),
_ts_metadata8("design:paramtypes", [
typeof StateStore === "undefined" ? Object : StateStore,
typeof import_nestjs_durable_core9.WorkflowEngine === "undefined" ? Object : import_nestjs_durable_core9.WorkflowEngine
])
], StoreRunGateway);
// src/tenant-event-republisher.ts
function isTerminalRunEvent(event) {
return event.type === "run.completed" || event.type === "run.failed";
}
__name(isTerminalRunEvent, "isTerminalRunEvent");
var TenantEventRepublisher = class {
static {
__name(this, "TenantEventRepublisher");
}
store;
publish;
runNamespaces = /* @__PURE__ */ new Map();
constructor(store, publish) {
this.store = store;
this.publish = publish;
}
async handle(event) {
const namespace = await this.namespaceFor(event);
if (isTerminalRunEvent(event)) this.runNamespaces.delete(event.runId);
if (!namespace || namespace === "default") return;
await this.publish({
tenant: namespace,
event
}).catch(() => void 0);
}
async namespaceFor(event) {
if (this.runNamespaces.has(event.runId)) {
const cached = this.runNamespaces.get(event.runId);
return cached === null ? void 0 : cached;
}
if (event.namespace !== void 0) {
this.runNamespaces.set(event.runId, event.namespace === "default" ? null : event.namespace);
return event.namespace;
}
const run = await this.store.getRun(event.runId);
const tenantNamespace = run?.namespace !== void 0 && run.namespace !== "default" ? run.namespace : null;
this.runNamespaces.set(event.runId, tenantNamespace);
return tenantNamespace === null ? void 0 : tenantNamespace;
}
};
// src/timer-poller.ts
var import_nestjs_durable_core10 = require("@dudousxd/nestjs-durable-core");
var import_common9 = require("@nestjs/common");
function _ts_decorate9(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
__name(_ts_decorate9, "_ts_decorate");
function _ts_metadata9(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
}
__name(_ts_metadata9, "_ts_metadata");
function _ts_param6(paramIndex, decorator) {
return function(target, key) {
decorator(target, key, paramIndex);
};
}
__name(_ts_param6, "_ts_param");
var TimerPoller = class {
static {
__name(this, "TimerPoller");
}
engine;
options;
timer;
polling = false;
unsubscribeEnqueued;
constructor(engine, options) {
this.engine = engine;
this.options = options;
}
async onApplicationBootstrap() {
if (!isDrivingOperator(this.options)) return;
this.unsubscribeEnqueued = this.engine.onEnqueued((runId) => void this.engine.runOne(runId));
await this.poll();
const intervalMs = this.options.timerPollMs ?? 1e3;
if (intervalMs > 0) {
this.timer = setInterval(() => void this.poll(), intervalMs);
this.timer.unref?.();
}
}
onModuleDestroy() {
if (this.timer) clearInterval(this.timer);
this.unsubscribeEnqueued?.();
}
async poll() {
if (this.polling) return;
this.polling = true;
try {
await this.engine.runPending();
await this.engine.recoverIncomplete();
await this.engine.resumeDueTimers();
await this.engine.sweepTimeouts();
const schedules = this.options.schedules;
if (schedules && schedules.length > 0) {
await (0, import_nestjs_durable_core10.runSchedules)(this.engine, schedules, Date.now());
}
} finally {
this.polling = false;
}
}
};
TimerPoller = _ts_decorate9([
(0, import_common9.Injectable)(),
_ts_param6(1, (0, import_common9.Inject)(import_nestjs_durable_core10.DURABLE_OPTIONS_CANONICAL)),
_ts_metadata9("design:type", Function),
_ts_metadata9("design:paramtypes", [
typeof import_nestjs_durable_core10.WorkflowEngine === "undefined" ? Object : import_nestjs_durable_core10.WorkflowEngine,
typeof DurableModuleOptions === "undefined" ? Object : DurableModuleOptions
])
], TimerPoller);
// src/tokens.ts
var import_nestjs_durable_core11 = require("@dudousxd/nestjs-durable-core");
var CONTEXT_ACCESSOR = Symbol.for("@dudousxd/nestjs-context:accessor");
var RUN_GATEWAY = import_nestjs_durable_core11.RunGateway;
// src/workflow.registrar.ts
var import_nestjs_durable_core12 = require("@dudousxd/nestjs-durable-core");
var import_common10 = require("@nestjs/common");
var import_core4 = require("@nestjs/core");
// src/input-validation.ts
function classValidatorInput(cls) {
let cv;
let ct;
try {
cv = require("class-validator");
ct = require("class-transformer");
} catch {
throw new Error('@Workflow({ inputSchema }) needs the optional peers "class-validator" and "class-transformer" \u2014 install them, or pass a `validateInput` function instead.');
}
return async (input) => {
const instance = ct.plainToInstance(cls, input);
const errors = await cv.validate(instance, {
whitelist: true
});
if (errors.length > 0) {
const message = errors.map((e) => Object.values(e.constraints ?? {
_: e.property
}).join(", ")).join("; ");
throw new Error(`invalid input for workflow: ${message}`);
}
};
}
__name(classValidatorInput, "classValidatorInput");
// src/step-interceptor.ts
var import_reflect_metadata3 = require("reflect-metadata");
var STEP_INTERCEPTOR_METADATA = Symbol("nestjs-durable:step-interceptor");
function StepInterceptor() {
return (target) => {
Reflect.defineMetadata(STEP_INTERCEPTOR_METADATA, true, target);
};
}
__name(StepInterceptor, "StepInterceptor");
function isStepInterceptor(target) {
return Reflect.getMetadata(STEP_INTERCEPTOR_METADATA, target) === true;
}
__name(isStepInterceptor, "isStepInterceptor");
// src/workflow.registrar.ts
function _ts_decorate10(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
__name(_ts_decorate10, "_ts_decorate");
function _ts_metadata10(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
}
__name(_ts_metadata10, "_ts_metadata");
function _ts_param7(paramIndex, decorator) {
return function(target, key) {
decorator(target, key, paramIndex);
};
}
__name(_ts_param7, "_ts_param");
var WorkflowRegistrar = class {
static {
__name(this, "WorkflowRegistrar");
}
discovery;
metadataScanner;
engine;
store;
options;
inAppWorker;
constructor(discovery, metadataScanner, engine, store, options, inAppWorker) {
this.discovery = discovery;
this.metadataScanner = metadataScanner;
this.engine = engine;
this.store = store;
this.options = options;
this.inAppWorker = inAppWorker;
}
async onApplicationBootstrap() {
if (!isDrivingOperator(this.options)) return;
await this.engine.recoverIncomplete();
}
/** On deploy/shutdown: stop picking up new runs and wait for in-flight ones to settle, then close
* the transport(s) so the broker workers stop consuming and connections are released. Closing
* AFTER the drain keeps the transport alive while in-flight runs dispatch/await their remote steps.
* Operator only — a thin worker (no `store`) holds no engine to drain. */
async onApplicationShutdown() {
if (!isOperatorRole(this.options)) return;
await this.engine.drain(this.options.shutdownTimeoutMs);
const transports = [
this.options.transport,
...(this.options.transports ?? []).map((t) => t.transport)
];
await Promise.allSettled(transports.map((t) => t?.close?.()));
}
async onModuleInit() {
if (!isOperatorRole(this.options)) return;
const store = this.store;
if (!store) {
throw new Error("unreachable: STATE_STORE_CANONICAL must resolve a store when options.store is set");
}
if (this.options.autoSchema !== false) {
await store.ensureSchema?.();
}
const deadLetterByWorkflow = /* @__PURE__ */ new Map();
for (const wrapper of this.discovery.getProviders()) {
const { instance } = wrapper;
if (!instance || typeof instance !== "object") continue;
if (isStepInterceptor(instance.constructor)) {
const interceptor = instance;
this.engine.use((invocation, next) => interceptor.intercept(invocation, next));
}
const entityMeta = getEntityMeta(instance.constructor);
if (entityMeta) {
this.engine.registerEntity(entityMeta.name, entityConfigFor(instance.constructor));
}
}
scanWorkflows(this.discovery, (meta, workflow) => {
const validateInput = meta.validateInput ?? (meta.inputSchema ? classValidatorInput(meta.inputSchema) : void 0);
const eventBatch = meta.debounce ? {
mode: "debounce",
windowMs: (0, import_nestjs_durable_core12.parseDuration)(meta.debounce)
} : meta.batch ? {
mode: "batch",
maxSize: meta.batch.maxSize,
windowMs: (0, import_nestjs_durable_core12.parseDuration)(meta.batch.within)
} : void 0;
const workflowCtor = workflow.constructor;
this.engine.register(meta.name, meta.version, (ctx, input) => workflow.run(ctx, input), {
tags: meta.tags,
singleton: meta.singleton,
executionTimeout: meta.executionTimeout,
requires: meta.requires,
validateInput,
searchAttributesSchema: meta.searchAttributes,
onEvent: getOnEvents(meta, workflowCtor),
eventBatch,
// Uniform dispatch (opt-in): when an in-app worker is configured, register the body GROUP-SERVED
// so the engine dispatches its turns over the transport instead of running it inline; the
// co-located worker consumer (Task 5: one queue PER REGISTERED NAME) replays the same body.
// The routing token — and the executor that dispatches under it — MUST be keyed by THIS
// workflow's own name (`tenantGroup(sanitizeQueueToken(meta.name), partition)`), not a single
// group shared across every discovered `@Workflow`: a fixed shared token would dispatch every
// workflow's turns to one queue while the co-located worker subscribes one queue per name,
// so a turn dispatched under the wrong token would never be consumed. Absent → the inline fast
// path (unchanged).
...this.inAppWorker ? {
group: (0, import_nestjs_durable_core12.tenantGroup)((0, import_nestjs_durable_core12.sanitizeQueueToken)(meta.name), this.inAppWorker.partition),
executor: new import_nestjs_durable_core12.RemoteWorkflowExecutor(this.inAppWorker.transport, meta.name, this.inAppWorker.partition)
} : {}
});
(0, import_nestjs_durable_core12.bindWorkflowClass)(workflowCtor, {
start: /* @__PURE__ */ __name((name, input, runId, opts) => this.engine.start(name, input, runId, opts), "start"),
waitForRun: /* @__PURE__ */ __name((runId, opts) => this.engine.waitForRun(runId, opts), "waitForRun")
});
const inline = this.findDeadLetterHandler(workflow);
if (inline && meta.deadLetterWorkflow) {
throw new Error(`@Workflow ${meta.name} declares both an inline @DeadLetter() method and a deadLetterWorkflow option. Use one: the inline handler, or the reference.`);
}
if (inline) {
const dlqName = `${meta.name}.dlq`;
this.engine.register(dlqName, meta.version, inline);
deadLetterByWorkflow.set(meta.name, dlqName);
} else if (meta.deadLetterWorkflow) {
deadLetterByWorkflow.set(meta.name, (0, import_nestjs_durable_core12.workflowName)(meta.deadLetterWorkflow));
}
});
this.installDeadLetterRouting(deadLetterByWorkflow);
}
/** Returns the instance's `@DeadLetter()` method bound to the instance, or undefined if none. */
findDeadLetterHandler(instance) {
const prototype = Object.getPrototypeOf(instance);
for (const methodName of this.metadataScanner.getAllMethodNames(prototype)) {
const method = instance[methodName];
if (typeof method === "function" && isDeadLetterHandler(method)) {
return (ctx, input) => method.call(instance, ctx, input);
}
}
return void 0;
}
/**
* Installs a single onDead listener that routes a dead run to its workflow's handler (from the
* map) or the module-level `deadLetterWorkflow` default. The handler is started idempotently with
* a `dlq:<runId>` id, so re-recovery never double-dispatches.
*/
installDeadLetterRouting(byWorkflow) {
const fallback = this.options.deadLetterWorkflow ? (0, import_nestjs_durable_core12.workflowName)(this.options.deadLetterWorkflow) : void 0;
if (byWorkflow.size === 0 && !fallback) return;
this.engine.onDead((run) => {
const target = byWorkflow.get(run.workflow) ?? fallback;
if (!target) return;
void this.engine.start(
target,
{
deadRunId: run.id,
workflow: run.workflow,
input: run.input,
error: run.error
},
`dlq:${run.id}`,
// Route the dead-letter handler to the dead run's OWN tenant so an operator dispatches it to
// that tenant's worker group (`target@<tenant>`), not the bare group.
{
namespace: run.namespace
}
).catch(() => void 0);
});
}
};
WorkflowRegistrar = _ts_decorate10([
(0, import_common10.Injectable)(),
_ts_param7(3, (0, import_common10.Inject)(import_nestjs_durable_core12.STATE_STORE_CANONICAL)),
_ts_param7(4, (0, import_common10.Inject)(import_nestjs_durable_core12.DURABLE_OPTIONS_CANONICAL)),
_ts_param7(5, (0, import_common10.Inject)(IN_APP_WORKER_BINDING)),
_ts_metadata10("design:type", Function),
_ts_metadata10("design:paramtypes", [
typeof import_core4.DiscoveryService === "undefined" ? Object : import_core4.DiscoveryService,
typeof import_core4.MetadataScanner === "undefined" ? Object : import_core4.MetadataScanner,
typeof import_nestjs_durable_core12.WorkflowEngine === "undefined" ? Object : import_nestjs_durable_core12.WorkflowEngine,
Object,
typeof DurableModuleOptions === "undefined" ? Object : DurableModuleOptions,
Object
])
], WorkflowRegistrar);
// src/workflow.service.ts
var import_node_crypto = require("crypto");
var import_nestjs_durable_core13 = require("@dudousxd/nestjs-durable-core");
var import_common11 = require("@nestjs/common");
function _ts_decorate11(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
__name(_ts_decorate11, "_ts_decorate");
function _ts_metadata11(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
}
__name(_ts_metadata11, "_ts_metadata");
var WorkflowService = class {
static {
__name(this, "WorkflowService");
}
engine;
constructor(engine) {
this.engine = engine;
}
start(workflow, input, runId = (0, import_node_crypto.randomUUID)(), opts) {
return this.engine.start(workflow, input, runId, opts);
}
resume(runId) {
return this.engine.resume(runId);
}
/**
* Resolve once a run settles — terminal (completed/failed/cancelled/dead) or suspended. `start`
* only enqueues (a worker runs the body), so pair them when a request needs the outcome:
* `const { runId } = await svc.start(...); const result = await svc.waitForRun(runId)`.
*/
waitForRun(runId, opts) {
return this.engine.waitForRun(runId, opts);
}
/** Deliver an external signal (e.g. from a webhook) to the run waiting on `token`. */
signal(token, payload) {
return this.engine.signal(token, payload);
}
signalWithStart(workflow, input, runId, signal, opts) {
return this.engine.signalWithStart(workflow, input, runId, signal, opts);
}
/**
* Publish a named event. Resumes runs waiting on it via `ctx.waitForEvent(name, { match })` and
* starts a fresh run of every workflow subscribed via `@Workflow({ onEvent })` / `@OnDurableEvent` (the
* payload becomes its input). Pass `opts.id` to dedupe redeliveries. Returns how many runs it
* touched (resumed + started).
*
* Reliable by default: a publish that touches NOBODY (no live waiter, no subscriber) buffers ONE
* copy so a LATER `waitForEvent(name, { match })` still consumes it instead of it being dropped —
* see {@link WorkflowEngine.publishEvent}'s full semantics doc. Pass `opts.buffer: false` to opt out.
*/
publishEvent(name, payload, opts) {
return this.engine.publishEvent(name, payload, opts);
}
};
WorkflowService = _ts_decorate11([
(0, import_common11.Injectable)(),
_ts_metadata11("design:type", Function),
_ts_metadata11("design:paramtypes", [
typeof import_nestjs_durable_core13.WorkflowEngine === "undefined" ? Object : import_nestjs_durable_core13.WorkflowEngine
])
], WorkflowService);
// src/durable.module.ts
function _ts_decorate12(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
__name(_ts_decorate12, "_ts_decorate");
function _ts_metadata12(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
}
__name(_ts_metadata12, "_ts_metadata");
function _ts_param8(paramIndex, decorator) {
return function(target, key) {
decorator(target, key, paramIndex);
};
}
__name(_ts_param8, "_ts_param");
function resolveAccessor(moduleRef) {
try {
return moduleRef.get(CONTEXT_ACCESSOR, {
strict: false
});
} catch {
return void 0;
}
}
__name(resolveAccessor, "resolveAccessor");
function carrierFromAccessor(accessor) {
const carrier = {};
const traceId = accessor.traceId();
if (traceId !== void 0) carrier.traceId = traceId;
const tenantId = accessor.tenantId();
if (tenantId !== void 0) carrier.tenantId = tenantId;
const userRef = accessor.userRef();
if (userRef !== void 0) carrier.userRef = userRef;
return carrier;
}
__name(carrierFromAccessor, "carrierFromAccessor");
function isContextRuntime(x) {
return !!x && typeof x.deserialize === "function";
}
__name(isContextRuntime, "isContextRuntime");
async function resolveContextRuntime() {
try {
const specifier = "@dudousxd/nestjs-context";
const mod = await import(specifier);
return isContextRuntime(mod.Context) ? mod.Context : void 0;
} catch {
return void 0;
}
}
__name(resolveContextRuntime, "resolveContextRuntime");
function isControlPlane(x) {
return !!x && typeof x.publishControl === "function" && typeof x.onControl === "function";
}
__name(isControlPlane, "isControlPlane");
function isScopeableStore(store) {
return typeof store.withScope === "function";
}
__name(isScopeableStore, "isScopeableStore");
function scopedStore(store, options) {
if (options.scopeReads !== true || options.namespace === void 0) return store;
if (!isScopeableStore(store)) return store;
return store.withScope({
namespace: options.namespace
});
}
__name(scopedStore, "scopedStore");
function assertValidRole(options) {
const hasStore = options.store !== void 0;
const hasConnection = options.connection !== void 0;
if (!hasStore && !hasConnection) {
throw new Error("a durable module needs either a `store` (operator) or a `connection` (worker)");
}
if (hasStore && options.transport === void 0 && options.transports === void 0) {
throw new Error("an operator (`store`) needs a `transport` (or `transports`)");
}
}
__name(assertValidRole, "assertValidRole");
function assertValidTopology(options) {
const topology = options.topology;
if (topology === void 0) return;
if (topology.role === "control-plane") {
if (options.store === void 0 || options.transport === void 0 && options.transports === void 0) {
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.");
}
if (options.partition !== void 0) {
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.");
}
if (topology.tenant !== void 0 && options.namespace !== void 0 && options.namespace !== topology.tenant) {
throw new Error(`topology: { role: 'control-plane', tenant: '${topology.tenant}' } conflicts with \`namespace: '${options.namespace}'\`.
tenant maps 1:1 onto namespace (the operator's poll-scoping axis) \u2014 set only \`tenant\`, or set \`namespace\` to the same value.`);
}
return;
}
if (options.connection === void 0) {
throw new Error(`topology: { role: 'tenant', tenant: '${topology.tenant}' } needs \`connection\`.
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.`);
}
if (options.store !== void 0) {
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).");
}
if (options.namespace !== void 0) {
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.");
}
if (options.partition !== void 0 && options.partition !== topology.tenant) {
throw new Error(`topology: { role: 'tenant', tenant: '${topology.tenant}' } conflicts with \`partition: '${options.partition}'\`.
tenant maps 1:1 onto partition (the worker queue-routing suffix) \u2014 set only \`tenant\`, or set \`partition\` to the same value.`);
}
}
__name(assertValidTopology, "assertValidTopology");
function resolveTopology(options) {
return servingPartition(resolveTopologyPreset(options));
}
__name(resolveTopology, "resolveTopology");
function servingPartition(options) {
if (options.partition !== void 0 || options.namespace === void 0) return options;
return {
...options,
partition: options.namespace
};
}
__name(servingPartition, "servingPartition");
function resolveTopologyPreset(options) {
const topology = options.topology;
if (topology === void 0) return options;
if (topology.role === "control-plane") {
if (topology.tenant === void 0) return options;
return {
...options,
// An explicit `namespace` is already validated equal to `tenant` (assertValidTopology), so
// keeping it is a no-op rather than a conflict.
namespace: options.namespace ?? topology.tenant,
// The tenant is ALSO this node's worker-routing suffix: its engine now dispatches a run's steps
// to `<name>@<tenant>` (core's `stepGroup`), so its own co-located worker must SUBSCRIBE those
// same tokens. Without this the node would dispatch into queues it is not itself listening on.
// `partition` is user-forbidden on this role (assertValidTopology), so we own it here.
partition: topology.tenant
};
}
if (options.partition !== void 0) return options;
return {
...options,
partition: topology.tenant
};
}
__name(resolveTopologyPreset, "resolveTopologyPreset");
var RunGatewayBootstrap = class RunGatewayBootstrap2 {
static {
__name(this, "RunGatewayBootstrap");
}
engine;
transport;
gateway;
store;
options;
unsubscribe;
constructor(engine, transport, gateway, store, options) {
this.engine = engine;
this.transport = transport;
this.gateway = gateway;
this.store = store;
this.options = options;
}
onApplicationBootstrap() {
if (!isDrivingOperator(this.options) || !this.transport) return;
const store = this.store;
if (!store) return;
const { onRunRequest, publishRunReply, publishTenantEvent } = this.transport;
if (typeof onRunRequest === "function" && typeof publishRunReply === "function") {
const runRequestTransport = {
onRunRequest: onRunRequest.bind(this.transport),
publishRunReply: publishRunReply.bind(this.transport)
};
new RunRequestResponder(runRequestTransport, this.gateway).start();
}
if (typeof publishTenantEvent === "function") {
const republisher = new TenantEventRepublisher(store, publishTenantEvent.bind(this.transport));
this.unsubscribe = this.engine.subscribe((event) => {
void republisher.handle(event);
});
}
}
onModuleDestroy() {
this.unsubscribe?.();
}
};
RunGatewayBootstrap = _ts_decorate12([
(0, import_common12.Injectable)(),
_ts_param8(1, (0, import_common12.Inject)(import_nestjs_durable_core14.TRANSPORT_CANONICAL)),
_ts_param8(3, (0, import_common12.Inject)(import_nestjs_durable_core14.STATE_STORE_CANONICAL)),
_ts_param8(4, (0, import_common12.Inject)(import_nestjs_durable_core14.DURABLE_OPTIONS_CANONICAL)),
_ts_metadata12("design:type", Function),
_ts_metadata12("design:paramtypes", [
typeof import_nestjs_durable_core14.WorkflowEngine === "undefined" ? Object : import_nestjs_durable_core14.WorkflowEngine,
Object,
typeof import_nestjs_durable_core14.RunGateway === "undefined" ? Object : import_nestjs_durable_core14.RunGateway,
Object,
typeof DurableModuleOptions === "undefined" ? Object : DurableModuleOptions
])
], RunGatewayBootstrap);
var DurableModule = class _DurableModule {
static {
__name(this, "DurableModule");
}
static forRoot(options) {
_DurableModule.assertValid(options);
return _DurableModule.build({
provide: import_nestjs_durable_core14.DURABLE_OPTIONS_CANONICAL,
useValue: resolveTopology(options)
});
}
static forRootAsync(options) {
return _DurableModule.build({
provide: import_nestjs_durable_core14.DURABLE_OPTIONS_CANONICAL,
useFactory: /* @__PURE__ */ __name(async (...args) => {
const resolved = await options.useFactory(...args);
_DurableModule.assertValid(resolved);
return resolveTopology(resolved);
}, "useFactory"),
inject: options.inject ?? []
});
}
/**
* Validates role/axis constraints, in resolution order. When {@link DurableModuleOptions.topology}
* is set, it OWNS validation — {@link assertValidTopology}'s own store/transport/connection checks
* are strictly stronger than {@link assertValidRole}'s, so the topology-specific, axis-teaching
* message is what a `topology`-opted-in consumer sees (not the older generic one). Falls back to
* {@link assertValidRole} unchanged when `topology` is absent — zero behavior change.
*/
static assertValid(options) {
if (options.topology !== void 0) {
assertValidTopology(options);
return;
}
assertValidRole(options);
}
static build(optionsProvider) {
return {
module: _DurableModule,
global: true,
imports: [
import_core5.DiscoveryModule
],
providers: [
optionsProvider,
{
provide: import_nestjs_durable_core14.STATE_STORE_CANONICAL,
useFactory: /* @__PURE__ */ __name((options) => options.store !== void 0 ? scopedStore(options.store, options) : null, "useFactory"),
inject: [
import_nestjs_durable_core14.DURABLE_OPTIONS_CANONICAL
]
},
{
provide: import_nestjs_durable_core14.TRANSPORT_CANONICAL,
// With a POOL (`transports`) and no singular `transport`, the canonical transport is the
// pool's PRIMARY — the same one the engine dispatches on first. Leaving it null starved
// the step registrar / in-app worker of a transport, so a pool-configured operator
// registered NO step handlers and its own steps parked in `wait` with no consumer.
useFactory: /* @__PURE__ */ __name((options) => options.transport ?? options.transports?.[0]?.transport ?? null, "useFactory"),
inject: [
import_nestjs_durable_core14.DURABLE_OPTIONS_CANONICAL
]
},
// Legacy back-compat aliases (deprecated tokens still resolve to the same instances):
{
provide: import_nestjs_durable_core14.STATE_STORE,
useExisting: import_nestjs_durable_core14.STATE_STORE_CANONICAL
},
{
provide: import_nestjs_durable_core14.TRANSPORT,
useExisting: import_nestjs_durable_core14.TRANSPORT_CANONICAL
},
{
provide: import_nestjs_durable_core14.DURABLE_OPTIONS,
useExisting: import_nestjs_durable_core14.DURABLE_OPTIONS_CANONICAL
},
{
// Shared token, bound EXACTLY once: a real engine for the operator role, or a store-less
// `DurableStartClient` facade for a thin worker (no `store`) — either way, `WorkflowService`
// and app code call `engine.start(...)` unchanged.
provide: import_nestjs_durable_core14.WorkflowEngine,
useFactory: /* @__PURE__ */ __name(async (options, store, transport, moduleRef) => {
if (options.store === void 0) {
return new DurableStartClient(options);
}
if (!store) {
throw new Error("unreachable: STATE_STORE_CANONICAL must resolve a store when options.store is set");
}
const primary = transport ?? options.transports?.[0]?.transport;
const accessor = resolveAccessor(moduleRef);
const context = options.context ?? (accessor ? () => carrierFromAccessor(accessor) : void 0);
const runtime = accessor ? await resolveContextRuntime() : void 0;
const rehydrate = runtime && ((carrier, fn) => carrier && Object.keys(carrier).length > 0 ? runtime.deserialize(carrier, fn) : fn());
const engine = new import_nestjs_durable_core14.WorkflowEngine({
store,
transport: transport ?? void 0,
transports: options.transports,
controlPlane: options.controlPlane ?? (isControlPlane(primary) ? primary : void 0),
leaseMs: options.leaseMs,
admission: options.admission,
maxRecoveryAttempts: options.maxRecoveryAttempts,
remoteAdvanceSilenceMs: options.remoteAdvanceSilenceMs,
instanceId: options.instanceId,
namespace: options.namespace,
webhookUrl: options.webhookUrl,
traceparent: options.traceparent,
context,
rehydrate: rehydrate || void 0,
compensationRetries: options.compensationRetries,
// A non-driving (dashboard/API) operator must not run workflows: enqueue-only, leaving
// each `pending` run in the store for a DRIVING instance's poll. A driving operator gets
// the engine's default in-process dispatcher: for a body registered inline, it runs
// locally; for one registered GROUP-SERVED (co-located worker) or left unregistered, the
// SAME default dispatcher routes it out remotely (group-served executor, or convention
// dispatch — always on) instead.
runDispatcher: options.drive === false ? {
dispatch: /* @__PURE__ */ __name(() => {
}, "dispatch")
} : void 0
});
for (const queue of options.queues ?? []) engine.registerQueue(queue);
return engine;
}, "useFactory"),
inject: [
import_nestjs_durable_core14.DURABLE_OPTIONS_CANONICAL,
import_nestjs_durable_core14.STATE_STORE_CANONICAL,
import_nestjs_durable_core14.TRANSPORT_CANONICAL,
import_core5.ModuleRef
]
},
WorkflowService,
EntityService,
WorkflowRegistrar,
DurableStepRegistrar,
TimerPoller,
RetentionPoller,
// Tenant run gateway: bound EXACTLY once — the store-backed `StoreRunGateway` for an operator,
// or (for a thin worker, no `store`) a `ProxyRunGateway` over `transport` when given, else a
// gateway whose every method rejects with a clear tenant error.
{
provide: import_nestjs_durable_core14.RunGateway,
useFactory: /* @__PURE__ */ __name((options, store, engine) => {
if (options.store !== void 0) {
if (!store) {
throw new Error("unreachable: STATE_STORE_CANONICAL must resolve a store when options.store is set");
}
return new StoreRunGateway(store, engine);
}
return options.transport ? new ProxyRunGateway(options.transport, options.partition ?? "default", options.runGatewayTimeoutMs) : unavailableRunGateway();
}, "useFactory"),
inject: [
import_nestjs_durable_core14.DURABLE_OPTIONS_CANONICAL,
import_nestjs_durable_core14.STATE_STORE_CANONICAL,
import_nestjs_durable_core14.WorkflowEngine
]
},
RunGatewayBootstrap,
// Co-located in-app worker (uniform dispatch): inert unless BOTH `store` and `connection` are
// set — see `in-app-worker.ts`.
...inAppWorkerProviders(),
// Pure thin-worker consumer: inert unless `connection` is set WITHOUT `store` — see
// `durable-worker.module.ts`.
...thinWorkerProviders()
],
exports: [
WorkflowService,
EntityService,
import_nestjs_durable_core14.WorkflowEngine,
import_nestjs_durable_core14.STATE_STORE,
import_nestjs_durable_core14.STATE_STORE_CANONICAL,
import_nestjs_durable_core14.TRANSPORT,
import_nestjs_durable_core14.TRANSPORT_CANONICAL,
import_nestjs_durable_core14.DURABLE_OPTIONS_CANONICAL,
import_nestjs_durable_core14.RunGateway
]
};
}
};
DurableModule = _ts_decorate12([
(0, import_common12.Module)({})
], DurableModule);
// src/index.ts
var import_nestjs_durable_core15 = require("@dudousxd/nestjs-durable-core");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
CONTEXT_ACCESSOR,
DEAD_LETTER_METADATA,
DURABLE_STEP_METADATA,
DURABLE_WORKER_RUNNERS,
DeadLetter,
DurableModule,
DurableStartClient,
DurableStep,
ENTITY_METADATA,
ENTITY_ON_METADATA,
Entity,
EntityService,
IN_APP_RUN_REDIS_WORKER,
IN_APP_WORKER_BINDING,
IN_APP_WORKER_RUNNERS,
IN_APP_WORKER_RUNTIME,
InAppWorkerBootstrap,
ON_EVENT_METADATA,
On,
OnDurableEvent,
OnEvent,
ProxyRunGateway,
RUN_GATEWAY,
RUN_REDIS_WORKER,
RunGateway,
RunRequestResponder,
STEP_INTERCEPTOR_METADATA,
Step,
StepInterceptor,
StoreRunGateway,
TenantEventRepublisher,
ThinStepRegistrar,
ThinWorkerBootstrap,
ThinWorkflowRegistrar,
WORKFLOW_METADATA,
Workflow,
WorkflowEngine,
WorkflowService,
attributesOf,
entityConfigFor,
getDurableStepMeta,
getEntityMeta,
getOnEvents,
getWorkflowMeta,
inAppWorkerProviders,
isDeadLetterHandler,
isDrivingOperator,
isOperatorRole,
isStepInterceptor,
readSearchAttributes,
thinWorkerProviders,
unavailableRunGateway
});
//# sourceMappingURL=index.cjs.map