@dudousxd/nestjs-durable 0.30.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +142 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +103 -1
- package/dist/index.d.ts +103 -1
- package/dist/index.js +141 -4
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -56,6 +56,7 @@ __export(index_exports, {
|
|
|
56
56
|
ThinWorkflowRegistrar: () => ThinWorkflowRegistrar,
|
|
57
57
|
WORKFLOW_METADATA: () => WORKFLOW_METADATA,
|
|
58
58
|
Workflow: () => Workflow,
|
|
59
|
+
WorkflowEngine: () => import_nestjs_durable_core13.WorkflowEngine,
|
|
59
60
|
WorkflowService: () => WorkflowService,
|
|
60
61
|
entityConfigFor: () => entityConfigFor,
|
|
61
62
|
getDurableStepMeta: () => getDurableStepMeta,
|
|
@@ -87,6 +88,7 @@ function Workflow(options) {
|
|
|
87
88
|
executionTimeout: options.executionTimeout,
|
|
88
89
|
inputSchema: options.inputSchema,
|
|
89
90
|
validateInput: options.validateInput,
|
|
91
|
+
searchAttributes: options.searchAttributes,
|
|
90
92
|
onEvent: options.onEvent,
|
|
91
93
|
debounce: options.debounce,
|
|
92
94
|
batch: options.batch
|
|
@@ -501,6 +503,9 @@ function unavailableRunGateway() {
|
|
|
501
503
|
listRuns(_query) {
|
|
502
504
|
return tenantGatewayUnavailable("listRuns");
|
|
503
505
|
},
|
|
506
|
+
waitingFor(_runIds) {
|
|
507
|
+
return tenantGatewayUnavailable("waitingFor");
|
|
508
|
+
},
|
|
504
509
|
workerHealth() {
|
|
505
510
|
return tenantGatewayUnavailable("workerHealth");
|
|
506
511
|
},
|
|
@@ -516,6 +521,9 @@ function unavailableRunGateway() {
|
|
|
516
521
|
retryWithInput(_runId, _input) {
|
|
517
522
|
return tenantGatewayUnavailable("retryWithInput");
|
|
518
523
|
},
|
|
524
|
+
redispatchPending(_runId) {
|
|
525
|
+
return tenantGatewayUnavailable("redispatchPending");
|
|
526
|
+
},
|
|
519
527
|
subscribe(_runId, _onEvent) {
|
|
520
528
|
throw new Error("RunGateway.subscribe() is not available \u2014 pass `transport` to use RunGateway.");
|
|
521
529
|
}
|
|
@@ -915,6 +923,14 @@ var ProxyRunGateway = class {
|
|
|
915
923
|
kind: "workerHealth"
|
|
916
924
|
});
|
|
917
925
|
}
|
|
926
|
+
/** Bulk, one request for the whole id list (like `listRuns`, not one request per id). The operator
|
|
927
|
+
* filters the reply to runs this tenant actually owns (see `RunRequestResponder`). */
|
|
928
|
+
waitingFor(runIds) {
|
|
929
|
+
return this.request({
|
|
930
|
+
kind: "waitingFor",
|
|
931
|
+
runIds
|
|
932
|
+
});
|
|
933
|
+
}
|
|
918
934
|
cancel(runId, opts) {
|
|
919
935
|
return this.request(opts === void 0 ? {
|
|
920
936
|
kind: "cancel",
|
|
@@ -944,6 +960,12 @@ var ProxyRunGateway = class {
|
|
|
944
960
|
input
|
|
945
961
|
});
|
|
946
962
|
}
|
|
963
|
+
redispatchPending(runId) {
|
|
964
|
+
return this.request({
|
|
965
|
+
kind: "redispatch",
|
|
966
|
+
runId
|
|
967
|
+
});
|
|
968
|
+
}
|
|
947
969
|
subscribe(runId, onEvent) {
|
|
948
970
|
const unsubscribe = this.transport.onTenantEvent?.(this.tenant, (evt) => {
|
|
949
971
|
if (evt.event.runId === runId) onEvent(evt.event);
|
|
@@ -1122,6 +1144,27 @@ var RunRequestResponder = class {
|
|
|
1122
1144
|
}
|
|
1123
1145
|
};
|
|
1124
1146
|
}
|
|
1147
|
+
if (body.kind === "waitingFor") {
|
|
1148
|
+
const all = await this.gateway.waitingFor(body.runIds);
|
|
1149
|
+
const owned = await Promise.all(Object.entries(all).map(async ([runId, waiting]) => {
|
|
1150
|
+
const runDetail = await this.gateway.getRunDetail(runId);
|
|
1151
|
+
return runDetail && runDetail.run.namespace === msg.tenant ? [
|
|
1152
|
+
runId,
|
|
1153
|
+
waiting
|
|
1154
|
+
] : void 0;
|
|
1155
|
+
}));
|
|
1156
|
+
const data = {};
|
|
1157
|
+
for (const entry of owned) {
|
|
1158
|
+
if (entry) data[entry[0]] = entry[1];
|
|
1159
|
+
}
|
|
1160
|
+
return {
|
|
1161
|
+
requestId: msg.requestId,
|
|
1162
|
+
result: {
|
|
1163
|
+
ok: true,
|
|
1164
|
+
data
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1125
1168
|
const detail = await this.gateway.getRunDetail(body.runId);
|
|
1126
1169
|
if (detail && detail.run.namespace !== msg.tenant) {
|
|
1127
1170
|
return {
|
|
@@ -1175,6 +1218,8 @@ var RunRequestResponder = class {
|
|
|
1175
1218
|
return this.gateway.continue(body.runId);
|
|
1176
1219
|
case "retryWithInput":
|
|
1177
1220
|
return this.gateway.retryWithInput(body.runId, body.input);
|
|
1221
|
+
case "redispatch":
|
|
1222
|
+
return this.gateway.redispatchPending(body.runId);
|
|
1178
1223
|
}
|
|
1179
1224
|
}
|
|
1180
1225
|
};
|
|
@@ -1238,6 +1283,36 @@ var StoreRunGateway = class {
|
|
|
1238
1283
|
} : run;
|
|
1239
1284
|
});
|
|
1240
1285
|
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Bulk-resolve what each of `runIds` is currently parked on — for a consumer with its own filtered/
|
|
1288
|
+
* paginated run listing (e.g. "which of MY suspended runs are stuck at a breakpoint") without
|
|
1289
|
+
* re-deriving `listRuns`' waiter scan or querying `durable_step_checkpoints` directly. Mirrors
|
|
1290
|
+
* `listRuns`' waiting computation (the SAME bulk signal-waiter scan + `resolveRunWaiting`), but ALSO
|
|
1291
|
+
* bulk-fetches the currently-suspended runs to check real status: `engine.cancel` (the non-compensate
|
|
1292
|
+
* path) doesn't clear a run's signal waiter row, so a cancelled run can leave an ORPHANED waiter
|
|
1293
|
+
* behind — trusting waiter presence alone would wrongly report a terminal run as still waiting.
|
|
1294
|
+
* Two bulk scans total (never one query per requested id), same as `listRuns`.
|
|
1295
|
+
*/
|
|
1296
|
+
async waitingFor(runIds) {
|
|
1297
|
+
if (runIds.length === 0) return {};
|
|
1298
|
+
const idSet = new Set(runIds);
|
|
1299
|
+
const [suspended, waiters] = await Promise.all([
|
|
1300
|
+
this.store.listRuns({
|
|
1301
|
+
statuses: [
|
|
1302
|
+
"suspended"
|
|
1303
|
+
]
|
|
1304
|
+
}),
|
|
1305
|
+
this.store.listSignalWaiters("")
|
|
1306
|
+
]);
|
|
1307
|
+
const waiterByRun = (0, import_nestjs_durable_core8.indexWaitersByRun)(waiters);
|
|
1308
|
+
const result = {};
|
|
1309
|
+
for (const run of suspended) {
|
|
1310
|
+
if (!idSet.has(run.id)) continue;
|
|
1311
|
+
const waiting = (0, import_nestjs_durable_core8.resolveRunWaiting)(run, waiterByRun);
|
|
1312
|
+
if (waiting) result[run.id] = waiting;
|
|
1313
|
+
}
|
|
1314
|
+
return result;
|
|
1315
|
+
}
|
|
1241
1316
|
/** Every group the engine knows about — unscoped. A tenant proxy's request is scoped by the
|
|
1242
1317
|
* `RunRequestResponder` (to the requester's `@<tenant>` groups); the operator's own UI sees all. */
|
|
1243
1318
|
workerHealth() {
|
|
@@ -1256,6 +1331,9 @@ var StoreRunGateway = class {
|
|
|
1256
1331
|
retryWithInput(runId, input) {
|
|
1257
1332
|
return this.engine.retryWithInput(runId, input);
|
|
1258
1333
|
}
|
|
1334
|
+
redispatchPending(runId) {
|
|
1335
|
+
return this.engine.redispatchPending(runId);
|
|
1336
|
+
}
|
|
1259
1337
|
subscribe(runId, onEvent) {
|
|
1260
1338
|
return this.engine.subscribe((event) => {
|
|
1261
1339
|
if (event.runId === runId) onEvent(event);
|
|
@@ -1526,6 +1604,7 @@ var WorkflowRegistrar = class {
|
|
|
1526
1604
|
singleton: meta.singleton,
|
|
1527
1605
|
executionTimeout: meta.executionTimeout,
|
|
1528
1606
|
validateInput,
|
|
1607
|
+
searchAttributesSchema: meta.searchAttributes,
|
|
1529
1608
|
onEvent: getOnEvents(meta, workflowCtor),
|
|
1530
1609
|
eventBatch,
|
|
1531
1610
|
// Uniform dispatch (opt-in): when an in-app worker is configured, register the body GROUP-SERVED
|
|
@@ -1758,6 +1837,47 @@ function assertValidRole(options) {
|
|
|
1758
1837
|
}
|
|
1759
1838
|
}
|
|
1760
1839
|
__name(assertValidRole, "assertValidRole");
|
|
1840
|
+
function assertValidTopology(options) {
|
|
1841
|
+
const topology = options.topology;
|
|
1842
|
+
if (topology === void 0) return;
|
|
1843
|
+
if (topology.role === "control-plane") {
|
|
1844
|
+
if (options.store === void 0 || options.transport === void 0 && options.transports === void 0) {
|
|
1845
|
+
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.");
|
|
1846
|
+
}
|
|
1847
|
+
if (options.partition !== void 0) {
|
|
1848
|
+
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.");
|
|
1849
|
+
}
|
|
1850
|
+
if ("tenant" in topology) {
|
|
1851
|
+
throw new Error("topology: { role: 'control-plane' } forbids a `tenant` field.\ntenant names a WORKER's isolation partition \u2014 a control-plane node serves every tenant's runs (via their namespace), it isn't scoped to one itself.");
|
|
1852
|
+
}
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
if (options.connection === void 0) {
|
|
1856
|
+
throw new Error(`topology: { role: 'tenant', tenant: '${topology.tenant}' } needs \`connection\`.
|
|
1857
|
+
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.`);
|
|
1858
|
+
}
|
|
1859
|
+
if (options.store !== void 0) {
|
|
1860
|
+
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).");
|
|
1861
|
+
}
|
|
1862
|
+
if (options.namespace !== void 0) {
|
|
1863
|
+
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.");
|
|
1864
|
+
}
|
|
1865
|
+
if (options.partition !== void 0 && options.partition !== topology.tenant) {
|
|
1866
|
+
throw new Error(`topology: { role: 'tenant', tenant: '${topology.tenant}' } conflicts with \`partition: '${options.partition}'\`.
|
|
1867
|
+
tenant maps 1:1 onto partition (the worker queue-routing suffix) \u2014 set only \`tenant\`, or set \`partition\` to the same value.`);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
__name(assertValidTopology, "assertValidTopology");
|
|
1871
|
+
function resolveTopology(options) {
|
|
1872
|
+
const topology = options.topology;
|
|
1873
|
+
if (topology === void 0 || topology.role !== "tenant") return options;
|
|
1874
|
+
if (options.partition !== void 0) return options;
|
|
1875
|
+
return {
|
|
1876
|
+
...options,
|
|
1877
|
+
partition: topology.tenant
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
__name(resolveTopology, "resolveTopology");
|
|
1761
1881
|
var RunGatewayBootstrap = class RunGatewayBootstrap2 {
|
|
1762
1882
|
static {
|
|
1763
1883
|
__name(this, "RunGatewayBootstrap");
|
|
@@ -1818,10 +1938,10 @@ var DurableModule = class _DurableModule {
|
|
|
1818
1938
|
__name(this, "DurableModule");
|
|
1819
1939
|
}
|
|
1820
1940
|
static forRoot(options) {
|
|
1821
|
-
|
|
1941
|
+
_DurableModule.assertValid(options);
|
|
1822
1942
|
return _DurableModule.build({
|
|
1823
1943
|
provide: import_nestjs_durable_core12.DURABLE_OPTIONS_CANONICAL,
|
|
1824
|
-
useValue: options
|
|
1944
|
+
useValue: resolveTopology(options)
|
|
1825
1945
|
});
|
|
1826
1946
|
}
|
|
1827
1947
|
static forRootAsync(options) {
|
|
@@ -1829,12 +1949,26 @@ var DurableModule = class _DurableModule {
|
|
|
1829
1949
|
provide: import_nestjs_durable_core12.DURABLE_OPTIONS_CANONICAL,
|
|
1830
1950
|
useFactory: /* @__PURE__ */ __name(async (...args) => {
|
|
1831
1951
|
const resolved = await options.useFactory(...args);
|
|
1832
|
-
|
|
1833
|
-
return resolved;
|
|
1952
|
+
_DurableModule.assertValid(resolved);
|
|
1953
|
+
return resolveTopology(resolved);
|
|
1834
1954
|
}, "useFactory"),
|
|
1835
1955
|
inject: options.inject ?? []
|
|
1836
1956
|
});
|
|
1837
1957
|
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Validates role/axis constraints, in resolution order. When {@link DurableModuleOptions.topology}
|
|
1960
|
+
* is set, it OWNS validation — {@link assertValidTopology}'s own store/transport/connection checks
|
|
1961
|
+
* are strictly stronger than {@link assertValidRole}'s, so the topology-specific, axis-teaching
|
|
1962
|
+
* message is what a `topology`-opted-in consumer sees (not the older generic one). Falls back to
|
|
1963
|
+
* {@link assertValidRole} unchanged when `topology` is absent — zero behavior change.
|
|
1964
|
+
*/
|
|
1965
|
+
static assertValid(options) {
|
|
1966
|
+
if (options.topology !== void 0) {
|
|
1967
|
+
assertValidTopology(options);
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
assertValidRole(options);
|
|
1971
|
+
}
|
|
1838
1972
|
static build(optionsProvider) {
|
|
1839
1973
|
return {
|
|
1840
1974
|
module: _DurableModule,
|
|
@@ -1976,6 +2110,9 @@ var DurableModule = class _DurableModule {
|
|
|
1976
2110
|
DurableModule = _ts_decorate12([
|
|
1977
2111
|
(0, import_common12.Module)({})
|
|
1978
2112
|
], DurableModule);
|
|
2113
|
+
|
|
2114
|
+
// src/index.ts
|
|
2115
|
+
var import_nestjs_durable_core13 = require("@dudousxd/nestjs-durable-core");
|
|
1979
2116
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1980
2117
|
0 && (module.exports = {
|
|
1981
2118
|
CONTEXT_ACCESSOR,
|
|
@@ -2013,6 +2150,7 @@ DurableModule = _ts_decorate12([
|
|
|
2013
2150
|
ThinWorkflowRegistrar,
|
|
2014
2151
|
WORKFLOW_METADATA,
|
|
2015
2152
|
Workflow,
|
|
2153
|
+
WorkflowEngine,
|
|
2016
2154
|
WorkflowService,
|
|
2017
2155
|
entityConfigFor,
|
|
2018
2156
|
getDurableStepMeta,
|