@dudousxd/nestjs-durable-core 0.71.1 → 0.72.1
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 +976 -171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +514 -2
- package/dist/index.d.ts +514 -2
- package/dist/index.js +961 -171
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -172,6 +172,27 @@ function currentWorkflowCtx() {
|
|
|
172
172
|
return storage.getStore();
|
|
173
173
|
}
|
|
174
174
|
__name(currentWorkflowCtx, "currentWorkflowCtx");
|
|
175
|
+
var STEP_SCOPE_KEY = Symbol.for("nestjs-durable:ambient-step-scope");
|
|
176
|
+
var stepScopeRef = globalThis;
|
|
177
|
+
if (!stepScopeRef[STEP_SCOPE_KEY]) {
|
|
178
|
+
stepScopeRef[STEP_SCOPE_KEY] = new AsyncLocalStorage();
|
|
179
|
+
}
|
|
180
|
+
var stepScopeStorage = stepScopeRef[STEP_SCOPE_KEY];
|
|
181
|
+
function runInStepScope(owner, step, fn) {
|
|
182
|
+
return stepScopeStorage.run({
|
|
183
|
+
owner,
|
|
184
|
+
step
|
|
185
|
+
}, fn);
|
|
186
|
+
}
|
|
187
|
+
__name(runInStepScope, "runInStepScope");
|
|
188
|
+
function currentStepScope() {
|
|
189
|
+
return stepScopeStorage.getStore();
|
|
190
|
+
}
|
|
191
|
+
__name(currentStepScope, "currentStepScope");
|
|
192
|
+
function runOutsideWorkflowCtx(fn) {
|
|
193
|
+
return storage.exit(() => stepScopeStorage.exit(fn));
|
|
194
|
+
}
|
|
195
|
+
__name(runOutsideWorkflowCtx, "runOutsideWorkflowCtx");
|
|
175
196
|
|
|
176
197
|
// src/ambient-step.ts
|
|
177
198
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
@@ -250,6 +271,23 @@ var FatalError = class extends Error {
|
|
|
250
271
|
this.code = code;
|
|
251
272
|
}
|
|
252
273
|
};
|
|
274
|
+
var ConcurrencyLimitError = class extends Error {
|
|
275
|
+
static {
|
|
276
|
+
__name(this, "ConcurrencyLimitError");
|
|
277
|
+
}
|
|
278
|
+
workflow;
|
|
279
|
+
key;
|
|
280
|
+
limit;
|
|
281
|
+
active;
|
|
282
|
+
constructor(workflow, key, limit, active) {
|
|
283
|
+
super(`concurrency limit reached for key "${key}" (${active}/${limit} in flight); ${workflow} was not started \u2014 retry later`);
|
|
284
|
+
this.name = "ConcurrencyLimitError";
|
|
285
|
+
this.workflow = workflow;
|
|
286
|
+
this.key = key;
|
|
287
|
+
this.limit = limit;
|
|
288
|
+
this.active = active;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
253
291
|
var SingletonQueueFullError = class extends Error {
|
|
254
292
|
static {
|
|
255
293
|
__name(this, "SingletonQueueFullError");
|
|
@@ -317,6 +355,21 @@ var NonDeterminismError = class extends Error {
|
|
|
317
355
|
this.seq = seq;
|
|
318
356
|
}
|
|
319
357
|
};
|
|
358
|
+
var NestedWorkflowCallError = class extends FatalError {
|
|
359
|
+
static {
|
|
360
|
+
__name(this, "NestedWorkflowCallError");
|
|
361
|
+
}
|
|
362
|
+
runId;
|
|
363
|
+
primitive;
|
|
364
|
+
step;
|
|
365
|
+
constructor(runId, primitive, step) {
|
|
366
|
+
super(`ctx.${primitive}() was called inside the body of step "${step}" (run ${runId}). Workflow primitives (child/startChild/step/sleep/signals/sideEffect/\u2026, and MyWorkflow.start()/.execute() via the ambient ctx) must be called from the workflow body, never from inside a step body: the step's checkpoint replays without re-running it, so the nested call would shift every later journal position and corrupt the run on replay. Move the call out of the step.`, "nested_workflow_call");
|
|
367
|
+
this.name = "NestedWorkflowCallError";
|
|
368
|
+
this.runId = runId;
|
|
369
|
+
this.primitive = primitive;
|
|
370
|
+
this.step = step;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
320
373
|
var GatherError = class extends Error {
|
|
321
374
|
static {
|
|
322
375
|
__name(this, "GatherError");
|
|
@@ -673,14 +726,19 @@ async function runStepHandler(task, handler) {
|
|
|
673
726
|
} : result, "withEvents");
|
|
674
727
|
try {
|
|
675
728
|
const logger = createStepLogger(events, Date.now);
|
|
676
|
-
const output = await runDurableExecution(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
729
|
+
const output = await runDurableExecution(
|
|
730
|
+
{
|
|
731
|
+
unit: "step",
|
|
732
|
+
runId: task.runId,
|
|
733
|
+
workflow: void 0,
|
|
734
|
+
name: task.name,
|
|
735
|
+
seq: task.seq,
|
|
736
|
+
attempt: task.attempt
|
|
737
|
+
},
|
|
738
|
+
// Outside any ambient workflow ctx: an in-process transport runs the handler on the parent
|
|
739
|
+
// body's async path, but a handler is not the body (see `runOutsideWorkflowCtx`).
|
|
740
|
+
async () => runOutsideWorkflowCtx(() => runInStepLogger(logger, () => handler(task.input, logger)))
|
|
741
|
+
);
|
|
684
742
|
return withEvents({
|
|
685
743
|
...base,
|
|
686
744
|
status: "completed",
|
|
@@ -763,6 +821,58 @@ function instantCheckpoint(p) {
|
|
|
763
821
|
}
|
|
764
822
|
__name(instantCheckpoint, "instantCheckpoint");
|
|
765
823
|
|
|
824
|
+
// src/concurrency.ts
|
|
825
|
+
var DEFAULT_CONCURRENCY_STATUSES = [
|
|
826
|
+
"pending",
|
|
827
|
+
"running",
|
|
828
|
+
"suspended",
|
|
829
|
+
"cancelling",
|
|
830
|
+
"blocked"
|
|
831
|
+
];
|
|
832
|
+
function concurrencyTag(key) {
|
|
833
|
+
return `concurrency:${key}`;
|
|
834
|
+
}
|
|
835
|
+
__name(concurrencyTag, "concurrencyTag");
|
|
836
|
+
async function resolveConcurrency(override, config, input) {
|
|
837
|
+
if (override) return override;
|
|
838
|
+
if (!config) return void 0;
|
|
839
|
+
const key = config.key(input);
|
|
840
|
+
if (key === void 0) return void 0;
|
|
841
|
+
const limit = typeof config.limit === "function" ? await config.limit(key) : config.limit;
|
|
842
|
+
return {
|
|
843
|
+
key,
|
|
844
|
+
limit,
|
|
845
|
+
countStatuses: config.countStatuses
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
__name(resolveConcurrency, "resolveConcurrency");
|
|
849
|
+
async function countRuns(store, query) {
|
|
850
|
+
if (store.countRuns) return store.countRuns(query);
|
|
851
|
+
if (store.runFacets && !query.status && !query.statuses && query.origin === void 0) {
|
|
852
|
+
const cells = await store.runFacets(query);
|
|
853
|
+
return cells.reduce((sum, c) => sum + c.count, 0);
|
|
854
|
+
}
|
|
855
|
+
if (store.runFacets && query.statuses && !query.status && query.origin === void 0) {
|
|
856
|
+
const { statuses, ...rest } = query;
|
|
857
|
+
const cells = await store.runFacets(rest);
|
|
858
|
+
return cells.filter((c) => statuses.includes(c.status)).reduce((sum, c) => sum + c.count, 0);
|
|
859
|
+
}
|
|
860
|
+
return (await store.listRuns(query)).length;
|
|
861
|
+
}
|
|
862
|
+
__name(countRuns, "countRuns");
|
|
863
|
+
async function assertConcurrency(store, workflow, quota) {
|
|
864
|
+
const active = quota.limit <= 0 ? 0 : await countRuns(store, {
|
|
865
|
+
tag: concurrencyTag(quota.key),
|
|
866
|
+
statuses: [
|
|
867
|
+
...quota.countStatuses ?? DEFAULT_CONCURRENCY_STATUSES
|
|
868
|
+
]
|
|
869
|
+
});
|
|
870
|
+
if (quota.limit <= 0 || active >= quota.limit) {
|
|
871
|
+
throw new ConcurrencyLimitError(workflow, quota.key, quota.limit, active);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
__name(assertConcurrency, "assertConcurrency");
|
|
875
|
+
|
|
766
876
|
// src/entities.ts
|
|
767
877
|
var ENTITY_WORKFLOW = "__entity";
|
|
768
878
|
var entityRunId = /* @__PURE__ */ __name((name, key) => `entity:${name}:${key}`, "entityRunId");
|
|
@@ -1595,6 +1705,19 @@ function planDispatch(requires, liveDescriptors, cp, token) {
|
|
|
1595
1705
|
}
|
|
1596
1706
|
__name(planDispatch, "planDispatch");
|
|
1597
1707
|
|
|
1708
|
+
// src/interfaces.ts
|
|
1709
|
+
var TERMINAL_RUN_STATUSES = [
|
|
1710
|
+
"completed",
|
|
1711
|
+
"failed",
|
|
1712
|
+
"cancelled",
|
|
1713
|
+
"dead"
|
|
1714
|
+
];
|
|
1715
|
+
function isNonEmptyRunScope(scope) {
|
|
1716
|
+
if (!scope) return false;
|
|
1717
|
+
return scope.workflow !== void 0 || scope.workflows !== void 0 || scope.tag !== void 0 || scope.tags !== void 0 || scope.namespace !== void 0 || scope.namespaces !== void 0 || scope.attributes !== void 0 && scope.attributes.length > 0;
|
|
1718
|
+
}
|
|
1719
|
+
__name(isNonEmptyRunScope, "isNonEmptyRunScope");
|
|
1720
|
+
|
|
1598
1721
|
// src/tenant-group.ts
|
|
1599
1722
|
function tenantGroup(baseGroup, tenant) {
|
|
1600
1723
|
return tenant !== void 0 && tenant !== "" && tenant !== "default" ? `${baseGroup}@${tenant}` : baseGroup;
|
|
@@ -1685,6 +1808,486 @@ function indexWaitersByRun(waiters) {
|
|
|
1685
1808
|
}
|
|
1686
1809
|
__name(indexWaitersByRun, "indexWaitersByRun");
|
|
1687
1810
|
|
|
1811
|
+
// src/scheduler.ts
|
|
1812
|
+
import { createRequire } from "node:module";
|
|
1813
|
+
|
|
1814
|
+
// src/cron-compat.ts
|
|
1815
|
+
function isIndexable(value) {
|
|
1816
|
+
return typeof value === "object" && value !== null || typeof value === "function";
|
|
1817
|
+
}
|
|
1818
|
+
__name(isIndexable, "isIndexable");
|
|
1819
|
+
function unwrapDefault(module_) {
|
|
1820
|
+
if (!isIndexable(module_)) return module_;
|
|
1821
|
+
if (Reflect.get(module_, "parseExpression") !== void 0) return module_;
|
|
1822
|
+
if (Reflect.get(module_, "CronExpressionParser") !== void 0) return module_;
|
|
1823
|
+
const inner = Reflect.get(module_, "default");
|
|
1824
|
+
return inner === void 0 ? module_ : inner;
|
|
1825
|
+
}
|
|
1826
|
+
__name(unwrapDefault, "unwrapDefault");
|
|
1827
|
+
function v4Entry(module_) {
|
|
1828
|
+
if (!isIndexable(module_)) return void 0;
|
|
1829
|
+
const parse = Reflect.get(module_, "parseExpression");
|
|
1830
|
+
if (typeof parse !== "function") return void 0;
|
|
1831
|
+
return (expr, options) => parse.call(module_, expr, options);
|
|
1832
|
+
}
|
|
1833
|
+
__name(v4Entry, "v4Entry");
|
|
1834
|
+
function v5Entry(module_) {
|
|
1835
|
+
if (!isIndexable(module_)) return void 0;
|
|
1836
|
+
const parser = Reflect.get(module_, "CronExpressionParser");
|
|
1837
|
+
if (!isIndexable(parser)) return void 0;
|
|
1838
|
+
const parse = Reflect.get(parser, "parse");
|
|
1839
|
+
if (typeof parse !== "function") return void 0;
|
|
1840
|
+
return (expr, options) => parse.call(parser, expr, options);
|
|
1841
|
+
}
|
|
1842
|
+
__name(v5Entry, "v5Entry");
|
|
1843
|
+
function resolveCronParse(module_) {
|
|
1844
|
+
const resolved = unwrapDefault(module_);
|
|
1845
|
+
return v4Entry(resolved) ?? v5Entry(resolved);
|
|
1846
|
+
}
|
|
1847
|
+
__name(resolveCronParse, "resolveCronParse");
|
|
1848
|
+
|
|
1849
|
+
// src/scheduler.ts
|
|
1850
|
+
var nodeRequire = createRequire(import.meta.url);
|
|
1851
|
+
var defaultSleep = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "defaultSleep");
|
|
1852
|
+
function scheduledRunId(key, everyMs, nowMs) {
|
|
1853
|
+
return `sched:${key}:${Math.floor(nowMs / everyMs)}`;
|
|
1854
|
+
}
|
|
1855
|
+
__name(scheduledRunId, "scheduledRunId");
|
|
1856
|
+
var cronParser;
|
|
1857
|
+
function loadCronParser() {
|
|
1858
|
+
if (cronParser !== void 0) return cronParser;
|
|
1859
|
+
try {
|
|
1860
|
+
cronParser = nodeRequire("cron-parser");
|
|
1861
|
+
} catch {
|
|
1862
|
+
throw new Error('cron schedules need the optional peer dependency "cron-parser" \u2014 install it (e.g. `npm i cron-parser`).');
|
|
1863
|
+
}
|
|
1864
|
+
return cronParser;
|
|
1865
|
+
}
|
|
1866
|
+
__name(loadCronParser, "loadCronParser");
|
|
1867
|
+
function prevCronFireMs(expr, nowMs, timezone = "UTC") {
|
|
1868
|
+
const parse = resolveCronParse(loadCronParser());
|
|
1869
|
+
if (!parse) {
|
|
1870
|
+
throw new Error(`the installed "cron-parser" exposes neither v4's parseExpression nor v5's CronExpressionParser.parse \u2014 the supported range is ^4.0.0 || ^5.0.0.`);
|
|
1871
|
+
}
|
|
1872
|
+
return parse(expr, {
|
|
1873
|
+
currentDate: new Date(nowMs + 1),
|
|
1874
|
+
tz: timezone
|
|
1875
|
+
}).prev().toDate().getTime();
|
|
1876
|
+
}
|
|
1877
|
+
__name(prevCronFireMs, "prevCronFireMs");
|
|
1878
|
+
function nextCronFireMs(expr, afterMs, timezone = "UTC") {
|
|
1879
|
+
const parse = resolveCronParse(loadCronParser());
|
|
1880
|
+
if (!parse) {
|
|
1881
|
+
throw new Error(`the installed "cron-parser" exposes neither v4's parseExpression nor v5's CronExpressionParser.parse \u2014 the supported range is ^4.0.0 || ^5.0.0.`);
|
|
1882
|
+
}
|
|
1883
|
+
return parse(expr, {
|
|
1884
|
+
currentDate: new Date(afterMs),
|
|
1885
|
+
tz: timezone
|
|
1886
|
+
}).next().toDate().getTime();
|
|
1887
|
+
}
|
|
1888
|
+
__name(nextCronFireMs, "nextCronFireMs");
|
|
1889
|
+
function scheduleRunIdAt(s, nowMs) {
|
|
1890
|
+
if (s.cron != null) return `sched:${s.key}:${prevCronFireMs(s.cron, nowMs, s.timezone)}`;
|
|
1891
|
+
if (s.everyMs != null) return scheduledRunId(s.key, s.everyMs, nowMs);
|
|
1892
|
+
throw new Error(`schedule "${s.key}" needs either "everyMs" or "cron"`);
|
|
1893
|
+
}
|
|
1894
|
+
__name(scheduleRunIdAt, "scheduleRunIdAt");
|
|
1895
|
+
function scheduleRunIdsAt(s, nowMs) {
|
|
1896
|
+
const current = scheduleRunIdAt(s, nowMs);
|
|
1897
|
+
if (!s.backfill || s.backfill.maxCatchup <= 0) return [
|
|
1898
|
+
current
|
|
1899
|
+
];
|
|
1900
|
+
const ids = [];
|
|
1901
|
+
if (s.everyMs != null) {
|
|
1902
|
+
const bucket = Math.floor(nowMs / s.everyMs);
|
|
1903
|
+
const oldest = Math.max(0, bucket - s.backfill.maxCatchup);
|
|
1904
|
+
for (let b = oldest; b < bucket; b += 1) ids.push(`sched:${s.key}:${b}`);
|
|
1905
|
+
} else if (s.cron != null) {
|
|
1906
|
+
const prior = [];
|
|
1907
|
+
let cursor = prevCronFireMs(s.cron, nowMs, s.timezone);
|
|
1908
|
+
for (let i = 0; i < s.backfill.maxCatchup; i += 1) {
|
|
1909
|
+
const before = prevCronFireMs(s.cron, cursor - 1, s.timezone);
|
|
1910
|
+
prior.push(`sched:${s.key}:${before}`);
|
|
1911
|
+
cursor = before;
|
|
1912
|
+
}
|
|
1913
|
+
ids.push(...prior.reverse());
|
|
1914
|
+
}
|
|
1915
|
+
ids.push(current);
|
|
1916
|
+
return ids;
|
|
1917
|
+
}
|
|
1918
|
+
__name(scheduleRunIdsAt, "scheduleRunIdsAt");
|
|
1919
|
+
async function runSchedules(engine, schedules, nowMs, opts) {
|
|
1920
|
+
const random = opts?.random ?? Math.random;
|
|
1921
|
+
const sleep = opts?.sleep ?? defaultSleep;
|
|
1922
|
+
const ids = [];
|
|
1923
|
+
for (const s of schedules) {
|
|
1924
|
+
if (s.paused) continue;
|
|
1925
|
+
if (s.overlap === "skip" && s.everyMs) {
|
|
1926
|
+
const prevBucket = Math.floor(nowMs / s.everyMs) - 1;
|
|
1927
|
+
if (prevBucket >= 0) {
|
|
1928
|
+
const prev = await engine.getRun(`sched:${s.key}:${prevBucket}`);
|
|
1929
|
+
if (prev && (prev.status === "running" || prev.status === "suspended")) continue;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
if (s.jitter && s.jitter > 0) await sleep(Math.floor(random() * s.jitter));
|
|
1933
|
+
for (const runId of scheduleRunIdsAt(s, nowMs)) {
|
|
1934
|
+
await engine.start(s.workflow, s.input, runId);
|
|
1935
|
+
ids.push(runId);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
return ids;
|
|
1939
|
+
}
|
|
1940
|
+
__name(runSchedules, "runSchedules");
|
|
1941
|
+
|
|
1942
|
+
// src/schedules.ts
|
|
1943
|
+
var ScheduleAlreadyExistsError = class extends Error {
|
|
1944
|
+
static {
|
|
1945
|
+
__name(this, "ScheduleAlreadyExistsError");
|
|
1946
|
+
}
|
|
1947
|
+
scheduleId;
|
|
1948
|
+
constructor(scheduleId) {
|
|
1949
|
+
super(`schedule "${scheduleId}" already exists`), this.scheduleId = scheduleId;
|
|
1950
|
+
this.name = "ScheduleAlreadyExistsError";
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
var ScheduleNotFoundError = class extends Error {
|
|
1954
|
+
static {
|
|
1955
|
+
__name(this, "ScheduleNotFoundError");
|
|
1956
|
+
}
|
|
1957
|
+
scheduleId;
|
|
1958
|
+
constructor(scheduleId) {
|
|
1959
|
+
super(`schedule "${scheduleId}" not found`), this.scheduleId = scheduleId;
|
|
1960
|
+
this.name = "ScheduleNotFoundError";
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
var DEFAULT_NAMESPACE = "default";
|
|
1964
|
+
var ON_TIME_GRACE_MS = 6e4;
|
|
1965
|
+
var TICK_BATCH = 100;
|
|
1966
|
+
var ScheduleClient = class {
|
|
1967
|
+
static {
|
|
1968
|
+
__name(this, "ScheduleClient");
|
|
1969
|
+
}
|
|
1970
|
+
store;
|
|
1971
|
+
host;
|
|
1972
|
+
options;
|
|
1973
|
+
constructor(store, host, options = {}) {
|
|
1974
|
+
this.store = store;
|
|
1975
|
+
this.host = host;
|
|
1976
|
+
this.options = options;
|
|
1977
|
+
}
|
|
1978
|
+
now() {
|
|
1979
|
+
return (this.options.clock ?? Date.now)();
|
|
1980
|
+
}
|
|
1981
|
+
/** Whether the store persists schedules (implements the schedule methods). */
|
|
1982
|
+
get supported() {
|
|
1983
|
+
const s = this.store;
|
|
1984
|
+
return !!(s.saveSchedule && s.getSchedule && s.updateSchedule && s.deleteSchedule && s.listSchedules);
|
|
1985
|
+
}
|
|
1986
|
+
requireStore() {
|
|
1987
|
+
if (!this.supported) {
|
|
1988
|
+
throw new Error("persisted schedules need a store that implements saveSchedule/getSchedule/updateSchedule/deleteSchedule/listSchedules \u2014 every bundled store adapter does; this one does not");
|
|
1989
|
+
}
|
|
1990
|
+
const s = this.store;
|
|
1991
|
+
return {
|
|
1992
|
+
saveSchedule: /* @__PURE__ */ __name((r) => s.saveSchedule.call(s, r), "saveSchedule"),
|
|
1993
|
+
getSchedule: /* @__PURE__ */ __name((id) => s.getSchedule.call(s, id), "getSchedule"),
|
|
1994
|
+
updateSchedule: /* @__PURE__ */ __name((id, patch, expected) => s.updateSchedule.call(s, id, patch, expected), "updateSchedule"),
|
|
1995
|
+
deleteSchedule: /* @__PURE__ */ __name((id) => s.deleteSchedule.call(s, id), "deleteSchedule"),
|
|
1996
|
+
listSchedules: /* @__PURE__ */ __name((q) => s.listSchedules.call(s, q), "listSchedules")
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
/** Create a schedule. Throws {@link ScheduleAlreadyExistsError} if the id is taken. */
|
|
2000
|
+
async create(opts) {
|
|
2001
|
+
const store = this.requireStore();
|
|
2002
|
+
if (await store.getSchedule(opts.id)) throw new ScheduleAlreadyExistsError(opts.id);
|
|
2003
|
+
return this.write(opts, null);
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Create the schedule, or replace the definition of an existing one (its history — last fire, last
|
|
2007
|
+
* run, fire count — is kept). The next fire is recomputed from now under the new definition, so a
|
|
2008
|
+
* window that already fired is never fired again by an upsert.
|
|
2009
|
+
*/
|
|
2010
|
+
async upsert(opts) {
|
|
2011
|
+
const store = this.requireStore();
|
|
2012
|
+
return this.write(opts, await store.getSchedule(opts.id));
|
|
2013
|
+
}
|
|
2014
|
+
/** The schedule `id`, or null. */
|
|
2015
|
+
async get(id) {
|
|
2016
|
+
const record = await this.requireStore().getSchedule(id);
|
|
2017
|
+
return record ? describe(record) : null;
|
|
2018
|
+
}
|
|
2019
|
+
/** Schedules matching `query`, soonest-due first. */
|
|
2020
|
+
async list(query = {}) {
|
|
2021
|
+
return (await this.requireStore().listSchedules(query)).map(describe);
|
|
2022
|
+
}
|
|
2023
|
+
/** Delete the schedule. Runs it already started are left alone. Returns whether it existed. */
|
|
2024
|
+
delete(id) {
|
|
2025
|
+
return this.requireStore().deleteSchedule(id);
|
|
2026
|
+
}
|
|
2027
|
+
/** Stop firing until {@link resume}. `note` records why (shown by `get`/`list`). */
|
|
2028
|
+
async pause(id, note) {
|
|
2029
|
+
const store = this.requireStore();
|
|
2030
|
+
const record = await this.mustGet(id);
|
|
2031
|
+
const spec = {
|
|
2032
|
+
...record.spec
|
|
2033
|
+
};
|
|
2034
|
+
if (note !== void 0) spec.note = note;
|
|
2035
|
+
await store.updateSchedule(id, {
|
|
2036
|
+
paused: true,
|
|
2037
|
+
spec,
|
|
2038
|
+
updatedAt: new Date(this.now())
|
|
2039
|
+
});
|
|
2040
|
+
return describe(await this.mustGet(id));
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Fire again from the next window after now — the windows skipped while paused are NOT caught up.
|
|
2044
|
+
*/
|
|
2045
|
+
async resume(id) {
|
|
2046
|
+
const store = this.requireStore();
|
|
2047
|
+
const record = await this.mustGet(id);
|
|
2048
|
+
const spec = record.spec;
|
|
2049
|
+
const window = nextWindowAfter(spec, this.now());
|
|
2050
|
+
await store.updateSchedule(id, {
|
|
2051
|
+
paused: false,
|
|
2052
|
+
nextFireAt: window === null ? null : window + jitterFor(record.id, window, spec),
|
|
2053
|
+
state: {
|
|
2054
|
+
...record.state,
|
|
2055
|
+
window
|
|
2056
|
+
},
|
|
2057
|
+
updatedAt: new Date(this.now())
|
|
2058
|
+
});
|
|
2059
|
+
return describe(await this.mustGet(id));
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* Start one run of the schedule right now, outside its cadence (paused or not). The run id is
|
|
2063
|
+
* `sched:<id>:manual:<nowMs>`; it doesn't move the schedule's next fire.
|
|
2064
|
+
*/
|
|
2065
|
+
async trigger(id) {
|
|
2066
|
+
const record = await this.mustGet(id);
|
|
2067
|
+
const runId = `sched:${id}:manual:${this.now()}`;
|
|
2068
|
+
return this.host.start(record.workflow, specOf(record).input, runId, startOptions(record));
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Fire every due schedule (in this engine's namespace; all namespaces for an operator engine):
|
|
2072
|
+
* start its window's run and advance it to the next window. Called by the NestJS timer poller on
|
|
2073
|
+
* each tick when `persistedSchedules` is on; call it yourself from any other driver. Returns the
|
|
2074
|
+
* run ids started (or re-found, for an idempotent re-fire).
|
|
2075
|
+
*/
|
|
2076
|
+
async tick(nowMs = this.now()) {
|
|
2077
|
+
const store = this.requireStore();
|
|
2078
|
+
const started = [];
|
|
2079
|
+
for (let round = 0; round < 50; round++) {
|
|
2080
|
+
const due = await store.listSchedules({
|
|
2081
|
+
dueBy: nowMs,
|
|
2082
|
+
namespace: this.options.namespace,
|
|
2083
|
+
limit: TICK_BATCH
|
|
2084
|
+
});
|
|
2085
|
+
for (const record of due) {
|
|
2086
|
+
const runId = await this.fire(record, nowMs);
|
|
2087
|
+
if (runId) started.push(runId);
|
|
2088
|
+
}
|
|
2089
|
+
if (due.length < TICK_BATCH) break;
|
|
2090
|
+
}
|
|
2091
|
+
return started;
|
|
2092
|
+
}
|
|
2093
|
+
/** Fire one due schedule and advance it. Returns the run id started, if any. */
|
|
2094
|
+
async fire(record, nowMs) {
|
|
2095
|
+
const store = this.requireStore();
|
|
2096
|
+
const spec = specOf(record);
|
|
2097
|
+
const state = record.state;
|
|
2098
|
+
let latest;
|
|
2099
|
+
let next;
|
|
2100
|
+
try {
|
|
2101
|
+
latest = latestWindowAtOrBefore(spec, nowMs);
|
|
2102
|
+
next = nextWindowAfter(spec, latest);
|
|
2103
|
+
} catch (error2) {
|
|
2104
|
+
await store.updateSchedule(record.id, {
|
|
2105
|
+
paused: true,
|
|
2106
|
+
nextFireAt: null,
|
|
2107
|
+
state: {
|
|
2108
|
+
...state,
|
|
2109
|
+
lastError: `invalid schedule: ${errorMessage(error2)}`
|
|
2110
|
+
},
|
|
2111
|
+
updatedAt: new Date(nowMs)
|
|
2112
|
+
}, record.nextFireAt);
|
|
2113
|
+
return void 0;
|
|
2114
|
+
}
|
|
2115
|
+
const window = state.window ?? latest;
|
|
2116
|
+
const jitter = spec.jitterMs ?? 0;
|
|
2117
|
+
const onTime = nowMs - latest <= ON_TIME_GRACE_MS + jitter;
|
|
2118
|
+
let runId;
|
|
2119
|
+
let lastError;
|
|
2120
|
+
let fired = false;
|
|
2121
|
+
const skipMissed = spec.catchup === "skip" && !onTime;
|
|
2122
|
+
const beforeWindow = latest < window;
|
|
2123
|
+
if (!skipMissed && !beforeWindow && !await this.overlapping(spec, state)) {
|
|
2124
|
+
runId = `sched:${record.id}:${latest}`;
|
|
2125
|
+
try {
|
|
2126
|
+
await this.host.start(record.workflow, spec.input, runId, startOptions(record));
|
|
2127
|
+
fired = true;
|
|
2128
|
+
} catch (error2) {
|
|
2129
|
+
if (await this.host.getRun(runId)) fired = true;
|
|
2130
|
+
else lastError = errorMessage(error2);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
const nextState = {
|
|
2134
|
+
...state,
|
|
2135
|
+
window: next,
|
|
2136
|
+
...fired ? {
|
|
2137
|
+
lastFireAt: latest,
|
|
2138
|
+
lastRunId: runId,
|
|
2139
|
+
fires: (state.fires ?? 0) + 1
|
|
2140
|
+
} : {},
|
|
2141
|
+
lastError: fired ? void 0 : lastError ?? state.lastError
|
|
2142
|
+
};
|
|
2143
|
+
await store.updateSchedule(record.id, {
|
|
2144
|
+
nextFireAt: next === null ? null : next + jitterFor(record.id, next, spec),
|
|
2145
|
+
state: stripUndefined(nextState),
|
|
2146
|
+
updatedAt: new Date(nowMs)
|
|
2147
|
+
}, record.nextFireAt);
|
|
2148
|
+
return fired ? runId : void 0;
|
|
2149
|
+
}
|
|
2150
|
+
/** `overlap: 'skip'` and the previous run of this schedule is still in flight. */
|
|
2151
|
+
async overlapping(spec, state) {
|
|
2152
|
+
if (spec.overlap !== "skip" || !state.lastRunId) return false;
|
|
2153
|
+
const prev = await this.host.getRun(state.lastRunId);
|
|
2154
|
+
return !!prev && !TERMINAL_RUN_STATUSES.includes(prev.status);
|
|
2155
|
+
}
|
|
2156
|
+
async mustGet(id) {
|
|
2157
|
+
const record = await this.requireStore().getSchedule(id);
|
|
2158
|
+
if (!record) throw new ScheduleNotFoundError(id);
|
|
2159
|
+
return record;
|
|
2160
|
+
}
|
|
2161
|
+
async write(opts, existing) {
|
|
2162
|
+
const store = this.requireStore();
|
|
2163
|
+
const spec = toSpec(opts);
|
|
2164
|
+
const now = this.now();
|
|
2165
|
+
const paused = opts.paused ?? existing?.paused ?? false;
|
|
2166
|
+
const window = nextWindowAfter(spec, now);
|
|
2167
|
+
const record = {
|
|
2168
|
+
id: opts.id,
|
|
2169
|
+
namespace: opts.namespace ?? this.options.namespace ?? DEFAULT_NAMESPACE,
|
|
2170
|
+
workflow: workflowName(opts.workflow),
|
|
2171
|
+
paused,
|
|
2172
|
+
nextFireAt: window === null ? null : window + jitterFor(opts.id, window, spec),
|
|
2173
|
+
tags: opts.tags,
|
|
2174
|
+
spec,
|
|
2175
|
+
state: stripUndefined({
|
|
2176
|
+
...existing?.state ?? {},
|
|
2177
|
+
window
|
|
2178
|
+
}),
|
|
2179
|
+
createdAt: existing?.createdAt ?? new Date(now),
|
|
2180
|
+
updatedAt: new Date(now)
|
|
2181
|
+
};
|
|
2182
|
+
await store.saveSchedule(record);
|
|
2183
|
+
return describe(record);
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
function toSpec(opts) {
|
|
2187
|
+
if (opts.cron == null === (opts.every == null)) {
|
|
2188
|
+
throw new Error(`schedule "${opts.id}" needs exactly one of "cron" or "every"`);
|
|
2189
|
+
}
|
|
2190
|
+
const everyMs = opts.every != null ? parseDuration(opts.every) : void 0;
|
|
2191
|
+
if (everyMs !== void 0 && !(everyMs > 0)) {
|
|
2192
|
+
throw new Error(`schedule "${opts.id}": "every" must be a positive duration`);
|
|
2193
|
+
}
|
|
2194
|
+
const jitterMs = opts.jitter != null ? parseDuration(opts.jitter) : void 0;
|
|
2195
|
+
return stripUndefined({
|
|
2196
|
+
v: 1,
|
|
2197
|
+
input: opts.input,
|
|
2198
|
+
cron: opts.cron,
|
|
2199
|
+
everyMs,
|
|
2200
|
+
timezone: opts.timezone,
|
|
2201
|
+
jitterMs: jitterMs && jitterMs > 0 ? jitterMs : void 0,
|
|
2202
|
+
overlap: opts.overlap ?? "allow",
|
|
2203
|
+
catchup: opts.catchup ?? "latest",
|
|
2204
|
+
note: opts.note,
|
|
2205
|
+
searchAttributes: opts.searchAttributes,
|
|
2206
|
+
priority: opts.priority,
|
|
2207
|
+
concurrency: opts.concurrency
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
__name(toSpec, "toSpec");
|
|
2211
|
+
function specOf(record) {
|
|
2212
|
+
return record.spec;
|
|
2213
|
+
}
|
|
2214
|
+
__name(specOf, "specOf");
|
|
2215
|
+
function latestWindowAtOrBefore(spec, atMs) {
|
|
2216
|
+
if (spec.cron != null) return prevCronFireMs(spec.cron, atMs, spec.timezone);
|
|
2217
|
+
const every = spec.everyMs;
|
|
2218
|
+
return Math.floor(atMs / every) * every;
|
|
2219
|
+
}
|
|
2220
|
+
__name(latestWindowAtOrBefore, "latestWindowAtOrBefore");
|
|
2221
|
+
function nextWindowAfter(spec, afterMs) {
|
|
2222
|
+
if (spec.cron != null) return nextCronFireMs(spec.cron, afterMs, spec.timezone);
|
|
2223
|
+
const every = spec.everyMs;
|
|
2224
|
+
return (Math.floor(afterMs / every) + 1) * every;
|
|
2225
|
+
}
|
|
2226
|
+
__name(nextWindowAfter, "nextWindowAfter");
|
|
2227
|
+
function jitterFor(id, window, spec) {
|
|
2228
|
+
const max = spec.jitterMs ?? 0;
|
|
2229
|
+
if (max <= 0) return 0;
|
|
2230
|
+
let h = 2166136261;
|
|
2231
|
+
for (const ch of `${id}@${window}`) {
|
|
2232
|
+
h ^= ch.charCodeAt(0);
|
|
2233
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
2234
|
+
}
|
|
2235
|
+
return h % max;
|
|
2236
|
+
}
|
|
2237
|
+
__name(jitterFor, "jitterFor");
|
|
2238
|
+
function startOptions(record) {
|
|
2239
|
+
const spec = specOf(record);
|
|
2240
|
+
return stripUndefined({
|
|
2241
|
+
tags: [
|
|
2242
|
+
...record.tags ?? [],
|
|
2243
|
+
`schedule:${record.id}`
|
|
2244
|
+
],
|
|
2245
|
+
namespace: record.namespace,
|
|
2246
|
+
searchAttributes: spec.searchAttributes,
|
|
2247
|
+
priority: spec.priority,
|
|
2248
|
+
concurrency: spec.concurrency
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
__name(startOptions, "startOptions");
|
|
2252
|
+
function describe(record) {
|
|
2253
|
+
const spec = specOf(record);
|
|
2254
|
+
const state = record.state;
|
|
2255
|
+
return stripUndefined({
|
|
2256
|
+
id: record.id,
|
|
2257
|
+
workflow: record.workflow,
|
|
2258
|
+
input: spec.input,
|
|
2259
|
+
namespace: record.namespace,
|
|
2260
|
+
cron: spec.cron,
|
|
2261
|
+
everyMs: spec.everyMs,
|
|
2262
|
+
timezone: spec.timezone,
|
|
2263
|
+
jitterMs: spec.jitterMs,
|
|
2264
|
+
overlap: spec.overlap ?? "allow",
|
|
2265
|
+
catchup: spec.catchup ?? "latest",
|
|
2266
|
+
paused: record.paused,
|
|
2267
|
+
note: spec.note,
|
|
2268
|
+
tags: record.tags,
|
|
2269
|
+
searchAttributes: spec.searchAttributes,
|
|
2270
|
+
priority: spec.priority,
|
|
2271
|
+
concurrency: spec.concurrency,
|
|
2272
|
+
nextFireAt: record.paused || record.nextFireAt == null ? null : new Date(record.nextFireAt),
|
|
2273
|
+
lastFireAt: state.lastFireAt != null ? new Date(state.lastFireAt) : void 0,
|
|
2274
|
+
lastRunId: state.lastRunId,
|
|
2275
|
+
fires: state.fires ?? 0,
|
|
2276
|
+
lastError: state.lastError,
|
|
2277
|
+
createdAt: record.createdAt,
|
|
2278
|
+
updatedAt: record.updatedAt
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
__name(describe, "describe");
|
|
2282
|
+
function stripUndefined(o) {
|
|
2283
|
+
return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
2284
|
+
}
|
|
2285
|
+
__name(stripUndefined, "stripUndefined");
|
|
2286
|
+
function errorMessage(error2) {
|
|
2287
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
2288
|
+
}
|
|
2289
|
+
__name(errorMessage, "errorMessage");
|
|
2290
|
+
|
|
1688
2291
|
// src/singleton-gate.ts
|
|
1689
2292
|
var SINGLETON_RETRY_MS = 1e3;
|
|
1690
2293
|
var SINGLETON_RETRY_JITTER_MS = 250;
|
|
@@ -2039,10 +2642,15 @@ var Position = class Position2 {
|
|
|
2039
2642
|
rewind() {
|
|
2040
2643
|
this.seq -= 1;
|
|
2041
2644
|
}
|
|
2645
|
+
/** The position the NEXT primitive would claim, without claiming it. */
|
|
2646
|
+
peek() {
|
|
2647
|
+
return this.seq + 1;
|
|
2648
|
+
}
|
|
2042
2649
|
};
|
|
2043
2650
|
function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttributesSchema) {
|
|
2044
2651
|
const { store, replay } = host;
|
|
2045
2652
|
const pos = new Position();
|
|
2653
|
+
const scopeOwner = {};
|
|
2046
2654
|
const readCheckpoint = /* @__PURE__ */ __name((seq) => {
|
|
2047
2655
|
const hit = replay?.get(seq);
|
|
2048
2656
|
if (hit !== void 0) return hit;
|
|
@@ -2088,7 +2696,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
2088
2696
|
};
|
|
2089
2697
|
const body = /* @__PURE__ */ __name(() => {
|
|
2090
2698
|
const logger = createStepLogger(events, host.clock);
|
|
2091
|
-
return runInStepLogger(logger, () => fn(logger));
|
|
2699
|
+
return runInStepScope(scopeOwner, name, () => runInStepLogger(logger, () => fn(logger)));
|
|
2092
2700
|
}, "body");
|
|
2093
2701
|
const output = host.interceptStep ? await host.interceptStep(invocation, body) : await body();
|
|
2094
2702
|
await host.completeStep({
|
|
@@ -2141,7 +2749,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
2141
2749
|
}
|
|
2142
2750
|
if (existing && existing.status === "completed") return existing.output;
|
|
2143
2751
|
return store.transaction(async (tx) => {
|
|
2144
|
-
const output = await fn(tx.raw);
|
|
2752
|
+
const output = await runInStepScope(scopeOwner, name, () => fn(tx.raw));
|
|
2145
2753
|
const cp = instantCheckpoint({
|
|
2146
2754
|
runId,
|
|
2147
2755
|
seq: current,
|
|
@@ -2650,30 +3258,49 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
2650
3258
|
});
|
|
2651
3259
|
}
|
|
2652
3260
|
__name(step, "step");
|
|
3261
|
+
const nestedStep = /* @__PURE__ */ __name(() => {
|
|
3262
|
+
const scope = currentStepScope();
|
|
3263
|
+
return scope !== void 0 && scope.owner === scopeOwner ? scope.step : void 0;
|
|
3264
|
+
}, "nestedStep");
|
|
3265
|
+
const guard = /* @__PURE__ */ __name((primitive, fn) => (...args) => {
|
|
3266
|
+
const stepName = nestedStep();
|
|
3267
|
+
if (stepName === void 0) return fn(...args);
|
|
3268
|
+
return (async () => {
|
|
3269
|
+
if (await readCheckpoint(pos.peek())) return fn(...args);
|
|
3270
|
+
throw new NestedWorkflowCallError(runId, primitive, stepName);
|
|
3271
|
+
})();
|
|
3272
|
+
}, "guard");
|
|
3273
|
+
const guardedWebhook = /* @__PURE__ */ __name(() => {
|
|
3274
|
+
const stepName = nestedStep();
|
|
3275
|
+
if (stepName !== void 0 && !replay?.get(pos.peek())) {
|
|
3276
|
+
throw new NestedWorkflowCallError(runId, "webhook", stepName);
|
|
3277
|
+
}
|
|
3278
|
+
return webhook();
|
|
3279
|
+
}, "guardedWebhook");
|
|
2653
3280
|
return {
|
|
2654
3281
|
runId,
|
|
2655
|
-
step,
|
|
2656
|
-
localStep,
|
|
2657
|
-
upsertSearchAttributes,
|
|
2658
|
-
transaction,
|
|
2659
|
-
callEntity,
|
|
2660
|
-
signalEntity,
|
|
2661
|
-
sleep,
|
|
2662
|
-
sleepUntil,
|
|
3282
|
+
step: guard("step", step),
|
|
3283
|
+
localStep: guard("localStep", localStep),
|
|
3284
|
+
upsertSearchAttributes: guard("upsertSearchAttributes", upsertSearchAttributes),
|
|
3285
|
+
transaction: guard("transaction", transaction),
|
|
3286
|
+
callEntity: guard("callEntity", callEntity),
|
|
3287
|
+
signalEntity: guard("signalEntity", signalEntity),
|
|
3288
|
+
sleep: guard("sleep", sleep),
|
|
3289
|
+
sleepUntil: guard("sleepUntil", sleepUntil),
|
|
2663
3290
|
continueAsNew,
|
|
2664
|
-
waitForSignal,
|
|
2665
|
-
waitForEvent,
|
|
2666
|
-
task,
|
|
2667
|
-
child,
|
|
2668
|
-
all,
|
|
2669
|
-
startChild,
|
|
2670
|
-
breakpoint,
|
|
2671
|
-
webhook,
|
|
2672
|
-
setEvent,
|
|
2673
|
-
onUpdate,
|
|
2674
|
-
patched,
|
|
2675
|
-
now,
|
|
2676
|
-
sideEffect
|
|
3291
|
+
waitForSignal: guard("waitForSignal", waitForSignal),
|
|
3292
|
+
waitForEvent: guard("waitForEvent", waitForEvent),
|
|
3293
|
+
task: guard("task", task),
|
|
3294
|
+
child: guard("child", child),
|
|
3295
|
+
all: guard("all", all),
|
|
3296
|
+
startChild: guard("startChild", startChild),
|
|
3297
|
+
breakpoint: guard("breakpoint", breakpoint),
|
|
3298
|
+
webhook: guardedWebhook,
|
|
3299
|
+
setEvent: guard("setEvent", setEvent),
|
|
3300
|
+
onUpdate: guard("onUpdate", onUpdate),
|
|
3301
|
+
patched: guard("patched", patched),
|
|
3302
|
+
now: guard("now", now),
|
|
3303
|
+
sideEffect: guard("sideEffect", sideEffect)
|
|
2677
3304
|
};
|
|
2678
3305
|
}
|
|
2679
3306
|
__name(createWorkflowCtx, "createWorkflowCtx");
|
|
@@ -2753,6 +3380,12 @@ var WorkflowEngine = class {
|
|
|
2753
3380
|
runDispatcher;
|
|
2754
3381
|
/** Per-key serialization for singleton workflows (admission, back-pressure, notify-on-release). */
|
|
2755
3382
|
singletons;
|
|
3383
|
+
/**
|
|
3384
|
+
* Persisted, runtime-managed schedules: create/upsert/pause/resume/delete/list/trigger them, and
|
|
3385
|
+
* `tick()` fires the due ones (the NestJS timer poller does it when `persistedSchedules` is on).
|
|
3386
|
+
* Needs a store that persists schedules — every bundled adapter does. See {@link ScheduleClient}.
|
|
3387
|
+
*/
|
|
3388
|
+
schedules;
|
|
2756
3389
|
/** Every registered workflow, keyed by `name@version` — so old versions stay runnable. */
|
|
2757
3390
|
workflows = /* @__PURE__ */ new Map();
|
|
2758
3391
|
/** The newest registered version per workflow name — used to `start` new runs. */
|
|
@@ -2837,6 +3470,13 @@ var WorkflowEngine = class {
|
|
|
2837
3470
|
}, "dispatch"),
|
|
2838
3471
|
configFor: /* @__PURE__ */ __name((run) => (this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? this.latest.get(run.workflow))?.singleton, "configFor")
|
|
2839
3472
|
});
|
|
3473
|
+
this.schedules = new ScheduleClient(this.store, {
|
|
3474
|
+
start: /* @__PURE__ */ __name((workflow, input, runId, opts) => this.start(workflow, input, runId, opts), "start"),
|
|
3475
|
+
getRun: /* @__PURE__ */ __name((runId) => this.store.getRun(runId), "getRun")
|
|
3476
|
+
}, {
|
|
3477
|
+
namespace: this.namespace,
|
|
3478
|
+
clock: this.clock
|
|
3479
|
+
});
|
|
2840
3480
|
this.pool.bind(
|
|
2841
3481
|
async (result) => {
|
|
2842
3482
|
const waiter = this.pending.get(result.stepId);
|
|
@@ -2972,6 +3612,7 @@ var WorkflowEngine = class {
|
|
|
2972
3612
|
fn,
|
|
2973
3613
|
tags: opts?.tags,
|
|
2974
3614
|
singleton: opts?.singleton,
|
|
3615
|
+
concurrency: opts?.concurrency,
|
|
2975
3616
|
executionTimeoutMs: opts?.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
|
|
2976
3617
|
validateInput: opts?.validateInput,
|
|
2977
3618
|
searchAttributesSchema: opts?.searchAttributesSchema,
|
|
@@ -3034,6 +3675,7 @@ var WorkflowEngine = class {
|
|
|
3034
3675
|
}, "fn"),
|
|
3035
3676
|
tags: opts.tags,
|
|
3036
3677
|
singleton: opts.singleton,
|
|
3678
|
+
concurrency: opts.concurrency,
|
|
3037
3679
|
executionTimeoutMs: opts.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
|
|
3038
3680
|
validateInput: opts.validateInput,
|
|
3039
3681
|
requires: opts.requires,
|
|
@@ -3205,12 +3847,17 @@ var WorkflowEngine = class {
|
|
|
3205
3847
|
error: prior.error
|
|
3206
3848
|
};
|
|
3207
3849
|
}
|
|
3850
|
+
const quota = await resolveConcurrency(opts?.concurrency, registered.concurrency, input);
|
|
3851
|
+
if (quota) await assertConcurrency(this.store, name, quota);
|
|
3208
3852
|
const now = /* @__PURE__ */ new Date();
|
|
3209
3853
|
const stampedTags = [
|
|
3210
3854
|
...opts?.tags ?? [],
|
|
3211
3855
|
...registered.singleton ? [
|
|
3212
3856
|
this.singletons.tag(registered.singleton, input)
|
|
3213
3857
|
] : [],
|
|
3858
|
+
...quota ? [
|
|
3859
|
+
concurrencyTag(quota.key)
|
|
3860
|
+
] : [],
|
|
3214
3861
|
...registered.versionDeclared === false ? [
|
|
3215
3862
|
VERSION_UNDECLARED_TAG
|
|
3216
3863
|
] : []
|
|
@@ -3255,10 +3902,30 @@ var WorkflowEngine = class {
|
|
|
3255
3902
|
getRun(runId) {
|
|
3256
3903
|
return this.store.getRun(runId);
|
|
3257
3904
|
}
|
|
3258
|
-
|
|
3905
|
+
/**
|
|
3906
|
+
* Re-execute a run's body (replaying its checkpoints). This is the EXPLICIT entry point: besides
|
|
3907
|
+
* driving a live run, it re-drives a `failed` run — the manual retry (prefer {@link requeue}, which
|
|
3908
|
+
* also resets the failure state so replay re-attempts the failed parts). Completed / cancelled /
|
|
3909
|
+
* dead runs are never re-executed. The engine's own implicit wake-ups (signals, step results,
|
|
3910
|
+
* timers, redelivered dispatches) go through {@link resumeImplicit}, which treats `failed` as
|
|
3911
|
+
* terminal too.
|
|
3912
|
+
*/
|
|
3913
|
+
resume(runId) {
|
|
3914
|
+
return this.resumeRun(runId, true);
|
|
3915
|
+
}
|
|
3916
|
+
/**
|
|
3917
|
+
* {@link resume} for every IMPLICIT wake-up — a delivered signal/child completion, a late step
|
|
3918
|
+
* result, a due timer, a redelivered dispatch, a queue-slot wake. A `failed` run is terminal to
|
|
3919
|
+
* these: only an explicit retry ({@link requeue} / {@link resume}) may re-drive it. Otherwise a
|
|
3920
|
+
* stale waiter or a late result left behind by a failed run silently re-executes its body.
|
|
3921
|
+
*/
|
|
3922
|
+
resumeImplicit(runId) {
|
|
3923
|
+
return this.resumeRun(runId, false);
|
|
3924
|
+
}
|
|
3925
|
+
async resumeRun(runId, allowFailed) {
|
|
3259
3926
|
const run = await this.store.getRun(runId);
|
|
3260
3927
|
if (!run) throw new Error(`run ${runId} not found`);
|
|
3261
|
-
if (run.status === "cancelled" || run.status === "completed" || run.status === "dead") {
|
|
3928
|
+
if (run.status === "cancelled" || run.status === "completed" || run.status === "dead" || run.status === "failed" && !allowFailed) {
|
|
3262
3929
|
return {
|
|
3263
3930
|
runId,
|
|
3264
3931
|
status: run.status,
|
|
@@ -3707,7 +4374,7 @@ var WorkflowEngine = class {
|
|
|
3707
4374
|
const nowMs = this.clock();
|
|
3708
4375
|
const acquired = await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs);
|
|
3709
4376
|
if (!acquired) return null;
|
|
3710
|
-
return this.
|
|
4377
|
+
return this.resumeImplicit(runId).catch((err) => {
|
|
3711
4378
|
if (err instanceof NamespaceMismatch) return null;
|
|
3712
4379
|
throw err;
|
|
3713
4380
|
});
|
|
@@ -3928,7 +4595,7 @@ var WorkflowEngine = class {
|
|
|
3928
4595
|
if (!acquired) continue;
|
|
3929
4596
|
try {
|
|
3930
4597
|
const settled = onLocked ? await onLocked(run) : void 0;
|
|
3931
|
-
results.push(settled ?? await this.
|
|
4598
|
+
results.push(settled ?? await this.resumeImplicit(run.id));
|
|
3932
4599
|
} catch (error2) {
|
|
3933
4600
|
console.error(`[nestjs-durable] could not resume run ${run.id} (${run.workflow}@${run.workflowVersion}); leaving it for another instance:`, error2);
|
|
3934
4601
|
}
|
|
@@ -4025,7 +4692,7 @@ var WorkflowEngine = class {
|
|
|
4025
4692
|
// for an ordinary (non-fan) signal/child await.
|
|
4026
4693
|
parallelGroup: waiter.parallelGroup
|
|
4027
4694
|
}));
|
|
4028
|
-
return this.
|
|
4695
|
+
return this.resumeImplicit(waiter.runId).catch((err) => {
|
|
4029
4696
|
if (err instanceof NamespaceMismatch) return null;
|
|
4030
4697
|
throw err;
|
|
4031
4698
|
});
|
|
@@ -4141,7 +4808,7 @@ var WorkflowEngine = class {
|
|
|
4141
4808
|
async cancel(runId, opts) {
|
|
4142
4809
|
const run = await this.store.getRun(runId);
|
|
4143
4810
|
if (!run) return null;
|
|
4144
|
-
if (
|
|
4811
|
+
if (TERMINAL_RUN_STATUSES.includes(run.status)) {
|
|
4145
4812
|
return {
|
|
4146
4813
|
runId,
|
|
4147
4814
|
status: run.status,
|
|
@@ -4170,7 +4837,7 @@ var WorkflowEngine = class {
|
|
|
4170
4837
|
}).catch(() => void 0);
|
|
4171
4838
|
}
|
|
4172
4839
|
queueMicrotask(() => {
|
|
4173
|
-
void this.
|
|
4840
|
+
void this.resumeImplicit(runId).then(() => this.notifyCancelled(runId)).catch(() => void 0);
|
|
4174
4841
|
});
|
|
4175
4842
|
await this.cancelChildren(runId, opts);
|
|
4176
4843
|
return {
|
|
@@ -4248,6 +4915,123 @@ var WorkflowEngine = class {
|
|
|
4248
4915
|
return deleted + 1;
|
|
4249
4916
|
}
|
|
4250
4917
|
/**
|
|
4918
|
+
* Hard-delete EVERY run matching `scope` — plus, by default, each one's whole child subtree — in
|
|
4919
|
+
* bounded batches. The "forget this tenant" / "drop this feature's history" operation: where
|
|
4920
|
+
* {@link deleteRun} removes one tree you already know the id of, this drains everything a scope
|
|
4921
|
+
* selects (`{ namespace: 'acme' }`, `{ tag: 'tenant:acme' }`, `{ workflows: [...] }`, attribute
|
|
4922
|
+
* predicates …) through the store API, so no caller has to reach into the `durable_*` tables.
|
|
4923
|
+
*
|
|
4924
|
+
* - **Live runs** (not yet terminal) are {@link cancel cancelled} first (plain cancel, no saga
|
|
4925
|
+
* undo), which tells any worker holding one to stop, and then deleted with the rest. Pass
|
|
4926
|
+
* `cancelLive: false` to leave live runs (and live descendants of finished ones) alone and
|
|
4927
|
+
* delete terminal history only.
|
|
4928
|
+
* - **Children** are found through the same parent→child edges as {@link getRunChildren} and deleted
|
|
4929
|
+
* with their root even when they don't match `scope` themselves (children inherit `namespace`,
|
|
4930
|
+
* but not `tags`). `children: false` deletes exactly the matching runs.
|
|
4931
|
+
* - **Batched**: `batchSize` runs (default 500) per round, each round one bulk
|
|
4932
|
+
* {@link StateStore.deleteRuns} (falling back to per-run {@link StateStore.deleteRun}).
|
|
4933
|
+
*
|
|
4934
|
+
* An EMPTY scope is rejected — it would delete every run in the store. Returns how many runs were
|
|
4935
|
+
* deleted (children included).
|
|
4936
|
+
*/
|
|
4937
|
+
async purgeRuns(scope, opts) {
|
|
4938
|
+
if (!isNonEmptyRunScope(scope)) {
|
|
4939
|
+
throw new Error("purgeRuns: refusing an empty scope (it would delete every run) \u2014 pass at least one predicate, e.g. { namespace }");
|
|
4940
|
+
}
|
|
4941
|
+
const batchSize = Math.max(1, opts?.batchSize ?? 500);
|
|
4942
|
+
const cancelLive = opts?.cancelLive ?? true;
|
|
4943
|
+
const withChildren = opts?.children ?? true;
|
|
4944
|
+
const statuses = cancelLive ? void 0 : [
|
|
4945
|
+
...TERMINAL_RUN_STATUSES
|
|
4946
|
+
];
|
|
4947
|
+
let deleted = 0;
|
|
4948
|
+
let previous = "";
|
|
4949
|
+
for (; ; ) {
|
|
4950
|
+
const batch = await this.store.listRuns({
|
|
4951
|
+
...scope,
|
|
4952
|
+
statuses,
|
|
4953
|
+
limit: batchSize
|
|
4954
|
+
});
|
|
4955
|
+
if (batch.length === 0) break;
|
|
4956
|
+
const fingerprint = batch.map((r) => r.id).join("\0");
|
|
4957
|
+
if (fingerprint === previous) {
|
|
4958
|
+
throw new Error("purgeRuns: the store returned the same runs after deleting them");
|
|
4959
|
+
}
|
|
4960
|
+
previous = fingerprint;
|
|
4961
|
+
const known = new Map(batch.map((r) => [
|
|
4962
|
+
r.id,
|
|
4963
|
+
r.status
|
|
4964
|
+
]));
|
|
4965
|
+
const ids = withChildren ? await this.collectSubtrees(batch.map((r) => r.id)) : batch.map((r) => r.id);
|
|
4966
|
+
const doomed = [];
|
|
4967
|
+
for (const id of ids) {
|
|
4968
|
+
const status = known.has(id) ? known.get(id) : (await this.store.getRun(id))?.status;
|
|
4969
|
+
if (status === void 0) continue;
|
|
4970
|
+
if (!TERMINAL_RUN_STATUSES.includes(status)) {
|
|
4971
|
+
if (!cancelLive) continue;
|
|
4972
|
+
await this.cancel(id).catch(() => void 0);
|
|
4973
|
+
}
|
|
4974
|
+
doomed.push(id);
|
|
4975
|
+
}
|
|
4976
|
+
await this.deleteRunIds(doomed);
|
|
4977
|
+
deleted += doomed.length;
|
|
4978
|
+
if (batch.length < batchSize) break;
|
|
4979
|
+
}
|
|
4980
|
+
return deleted;
|
|
4981
|
+
}
|
|
4982
|
+
/**
|
|
4983
|
+
* {@link purgeRuns} for one namespace — every run (and child) a tenant/partition owns. Children
|
|
4984
|
+
* inherit their parent's namespace, so a namespace purge is complete by construction.
|
|
4985
|
+
*/
|
|
4986
|
+
purgeNamespace(namespace, opts) {
|
|
4987
|
+
return this.purgeRuns({
|
|
4988
|
+
namespace
|
|
4989
|
+
}, opts);
|
|
4990
|
+
}
|
|
4991
|
+
/** The given runs plus every descendant, children before parents. One waiter scan for the batch. */
|
|
4992
|
+
async collectSubtrees(rootIds) {
|
|
4993
|
+
const waitersByParent = /* @__PURE__ */ new Map();
|
|
4994
|
+
for (const w of await this.store.listSignalWaiters("child:")) {
|
|
4995
|
+
const list = waitersByParent.get(w.runId) ?? [];
|
|
4996
|
+
list.push(w.token.slice("child:".length));
|
|
4997
|
+
waitersByParent.set(w.runId, list);
|
|
4998
|
+
}
|
|
4999
|
+
const prefixes = [
|
|
5000
|
+
"signal:child:",
|
|
5001
|
+
"spawn:"
|
|
5002
|
+
];
|
|
5003
|
+
const childrenOf = /* @__PURE__ */ __name(async (parent) => {
|
|
5004
|
+
const ids = new Set(waitersByParent.get(parent) ?? []);
|
|
5005
|
+
const cps = this.store.listCheckpointsByNamePrefix ? await this.store.listCheckpointsByNamePrefix(parent, prefixes) : (await this.store.listCheckpoints(parent)).filter((cp) => prefixes.some((p) => cp.name.startsWith(p)));
|
|
5006
|
+
for (const cp of cps) {
|
|
5007
|
+
if (cp.name.startsWith("signal:child:")) ids.add(cp.name.slice("signal:child:".length));
|
|
5008
|
+
if (cp.name.startsWith("spawn:") && typeof cp.output === "string") ids.add(cp.output);
|
|
5009
|
+
}
|
|
5010
|
+
return [
|
|
5011
|
+
...ids
|
|
5012
|
+
];
|
|
5013
|
+
}, "childrenOf");
|
|
5014
|
+
const ordered = [];
|
|
5015
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5016
|
+
const visit = /* @__PURE__ */ __name(async (id) => {
|
|
5017
|
+
if (seen.has(id)) return;
|
|
5018
|
+
seen.add(id);
|
|
5019
|
+
for (const child of await childrenOf(id)) await visit(child);
|
|
5020
|
+
ordered.push(id);
|
|
5021
|
+
}, "visit");
|
|
5022
|
+
for (const id of rootIds) await visit(id);
|
|
5023
|
+
return ordered;
|
|
5024
|
+
}
|
|
5025
|
+
/** Bulk-delete through the store's `deleteRuns`, or one `deleteRun` per id when it has none. */
|
|
5026
|
+
async deleteRunIds(ids) {
|
|
5027
|
+
if (ids.length === 0) return;
|
|
5028
|
+
if (this.store.deleteRuns) {
|
|
5029
|
+
await this.store.deleteRuns(ids);
|
|
5030
|
+
return;
|
|
5031
|
+
}
|
|
5032
|
+
for (const id of ids) await this.store.deleteRun(id);
|
|
5033
|
+
}
|
|
5034
|
+
/**
|
|
4251
5035
|
* Cascade cancellation to a run's children — both awaited (`ctx.child`, found via its live
|
|
4252
5036
|
* `child:<id>` waiter) and fire-and-forget (`ctx.startChild`, found via its `spawn:<id>`
|
|
4253
5037
|
* checkpoint). Recursive, so a whole subtree is cancelled; the terminal guard in `cancel` stops it
|
|
@@ -4888,7 +5672,7 @@ var WorkflowEngine = class {
|
|
|
4888
5672
|
const checkpoint = await this.store.getCheckpoint(runId, cmd.seq);
|
|
4889
5673
|
if (checkpoint && checkpoint.status !== "pending") {
|
|
4890
5674
|
setTimeout(() => {
|
|
4891
|
-
void this.
|
|
5675
|
+
void this.resumeImplicit(runId).catch(() => void 0);
|
|
4892
5676
|
}, 0).unref?.();
|
|
4893
5677
|
return;
|
|
4894
5678
|
}
|
|
@@ -5230,7 +6014,7 @@ var WorkflowEngine = class {
|
|
|
5230
6014
|
kind: "signal",
|
|
5231
6015
|
output: payload
|
|
5232
6016
|
}));
|
|
5233
|
-
setTimeout(() => void this.
|
|
6017
|
+
setTimeout(() => void this.resumeImplicit(run.id).catch(() => void 0), 0);
|
|
5234
6018
|
}, "deliverBuffered");
|
|
5235
6019
|
const buffered = await this.store.takeBufferedSignal(cmd.signal);
|
|
5236
6020
|
if (buffered) {
|
|
@@ -6044,7 +6828,7 @@ var WorkflowEngine = class {
|
|
|
6044
6828
|
queueMs: startedAt.getTime() - cp.enqueuedAt.getTime(),
|
|
6045
6829
|
durationMs: finishedAt.getTime() - startedAt.getTime()
|
|
6046
6830
|
});
|
|
6047
|
-
await this.
|
|
6831
|
+
await this.resumeImplicit(result.runId).catch((err) => {
|
|
6048
6832
|
if (err instanceof NamespaceMismatch) return null;
|
|
6049
6833
|
throw err;
|
|
6050
6834
|
});
|
|
@@ -6145,7 +6929,7 @@ var WorkflowEngine = class {
|
|
|
6145
6929
|
...waiters
|
|
6146
6930
|
];
|
|
6147
6931
|
waiters.clear();
|
|
6148
|
-
for (const runId of runIds) void this.
|
|
6932
|
+
for (const runId of runIds) void this.resumeImplicit(runId).catch(() => void 0);
|
|
6149
6933
|
}
|
|
6150
6934
|
/** Release the flow-control slot a dispatched step held (if any), by its stepId. */
|
|
6151
6935
|
async releaseQueueSlot(id) {
|
|
@@ -6268,14 +7052,6 @@ var RemoteStepError = class extends Error {
|
|
|
6268
7052
|
}
|
|
6269
7053
|
};
|
|
6270
7054
|
|
|
6271
|
-
// src/interfaces.ts
|
|
6272
|
-
var TERMINAL_RUN_STATUSES = [
|
|
6273
|
-
"completed",
|
|
6274
|
-
"failed",
|
|
6275
|
-
"cancelled",
|
|
6276
|
-
"dead"
|
|
6277
|
-
];
|
|
6278
|
-
|
|
6279
7055
|
// src/run-facets.ts
|
|
6280
7056
|
function facetOrigin(origin) {
|
|
6281
7057
|
return origin?.trim() ? origin : null;
|
|
@@ -6307,7 +7083,8 @@ __name(mergeRunFacetRows, "mergeRunFacetRows");
|
|
|
6307
7083
|
var RUN_VALUE_FACET_LIMIT = 100;
|
|
6308
7084
|
var RUN_VALUE_FACET_SCAN = 5e3;
|
|
6309
7085
|
var ENGINE_MINTED_TAG_PREFIXES = [
|
|
6310
|
-
"singleton:"
|
|
7086
|
+
"singleton:",
|
|
7087
|
+
"concurrency:"
|
|
6311
7088
|
];
|
|
6312
7089
|
function isEngineMintedTag(value) {
|
|
6313
7090
|
return value !== null && ENGINE_MINTED_TAG_PREFIXES.some((prefix) => value.startsWith(prefix));
|
|
@@ -6412,9 +7189,42 @@ var CodecStateStore = class {
|
|
|
6412
7189
|
* Bound the same conditional way as {@link runFacets} so absence is reported, not faked.
|
|
6413
7190
|
*/
|
|
6414
7191
|
runValueFacets;
|
|
7192
|
+
/** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
|
|
7193
|
+
* engine's per-run fallback still kicks in through the wrapper. */
|
|
7194
|
+
deleteRuns;
|
|
7195
|
+
/** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
|
|
7196
|
+
* when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
|
|
7197
|
+
pruneTerminalRuns;
|
|
7198
|
+
/** Forwarded verbatim — a count reads no payload. Bound only when the inner store counts, so the
|
|
7199
|
+
* engine's facet/listing fallback still applies through the wrapper. */
|
|
7200
|
+
countRuns;
|
|
7201
|
+
/** Schedules persist their run `input` inside `spec` — encoded like any other payload. Bound only
|
|
7202
|
+
* when the inner store persists schedules, so `engine.schedules` reports absence faithfully. */
|
|
7203
|
+
saveSchedule;
|
|
7204
|
+
getSchedule;
|
|
7205
|
+
updateSchedule;
|
|
7206
|
+
deleteSchedule;
|
|
7207
|
+
listSchedules;
|
|
6415
7208
|
constructor(inner, codec) {
|
|
6416
7209
|
this.inner = inner;
|
|
6417
7210
|
this.codec = codec;
|
|
7211
|
+
const deleteRuns = inner.deleteRuns;
|
|
7212
|
+
if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
|
|
7213
|
+
const prune = inner.pruneTerminalRuns;
|
|
7214
|
+
if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
|
|
7215
|
+
const count = inner.countRuns;
|
|
7216
|
+
if (count) this.countRuns = (query) => count.call(inner, query);
|
|
7217
|
+
const { saveSchedule, getSchedule, updateSchedule, deleteSchedule, listSchedules } = inner;
|
|
7218
|
+
if (saveSchedule && getSchedule && updateSchedule && deleteSchedule && listSchedules) {
|
|
7219
|
+
this.saveSchedule = (r) => saveSchedule.call(inner, this.encSchedule(r));
|
|
7220
|
+
this.getSchedule = async (id) => this.decSchedule(await getSchedule.call(inner, id));
|
|
7221
|
+
this.updateSchedule = (id, patch, expected) => updateSchedule.call(inner, id, patch.spec ? {
|
|
7222
|
+
...patch,
|
|
7223
|
+
spec: this.encSpec(patch.spec)
|
|
7224
|
+
} : patch, expected);
|
|
7225
|
+
this.deleteSchedule = (id) => deleteSchedule.call(inner, id);
|
|
7226
|
+
this.listSchedules = async (q) => (await listSchedules.call(inner, q)).map((r) => this.decSchedule(r));
|
|
7227
|
+
}
|
|
6418
7228
|
const facets = inner.runFacets;
|
|
6419
7229
|
if (facets) this.runFacets = (query) => facets.call(inner, query);
|
|
6420
7230
|
const valueFacets = inner.runValueFacets;
|
|
@@ -6456,6 +7266,28 @@ var CodecStateStore = class {
|
|
|
6456
7266
|
output: this.dec(cp.output)
|
|
6457
7267
|
};
|
|
6458
7268
|
}
|
|
7269
|
+
encSpec(spec) {
|
|
7270
|
+
return "input" in spec ? {
|
|
7271
|
+
...spec,
|
|
7272
|
+
input: this.enc(spec.input)
|
|
7273
|
+
} : spec;
|
|
7274
|
+
}
|
|
7275
|
+
encSchedule(r) {
|
|
7276
|
+
return {
|
|
7277
|
+
...r,
|
|
7278
|
+
spec: this.encSpec(r.spec)
|
|
7279
|
+
};
|
|
7280
|
+
}
|
|
7281
|
+
decSchedule(r) {
|
|
7282
|
+
if (!r || !("input" in r.spec)) return r;
|
|
7283
|
+
return {
|
|
7284
|
+
...r,
|
|
7285
|
+
spec: {
|
|
7286
|
+
...r.spec,
|
|
7287
|
+
input: this.dec(r.spec.input)
|
|
7288
|
+
}
|
|
7289
|
+
};
|
|
7290
|
+
}
|
|
6459
7291
|
ensureSchema() {
|
|
6460
7292
|
return this.inner.ensureSchema?.() ?? Promise.resolve();
|
|
6461
7293
|
}
|
|
@@ -6654,126 +7486,6 @@ function collectMetrics(engine) {
|
|
|
6654
7486
|
}
|
|
6655
7487
|
__name(collectMetrics, "collectMetrics");
|
|
6656
7488
|
|
|
6657
|
-
// src/scheduler.ts
|
|
6658
|
-
import { createRequire } from "node:module";
|
|
6659
|
-
|
|
6660
|
-
// src/cron-compat.ts
|
|
6661
|
-
function isIndexable(value) {
|
|
6662
|
-
return typeof value === "object" && value !== null || typeof value === "function";
|
|
6663
|
-
}
|
|
6664
|
-
__name(isIndexable, "isIndexable");
|
|
6665
|
-
function unwrapDefault(module_) {
|
|
6666
|
-
if (!isIndexable(module_)) return module_;
|
|
6667
|
-
if (Reflect.get(module_, "parseExpression") !== void 0) return module_;
|
|
6668
|
-
if (Reflect.get(module_, "CronExpressionParser") !== void 0) return module_;
|
|
6669
|
-
const inner = Reflect.get(module_, "default");
|
|
6670
|
-
return inner === void 0 ? module_ : inner;
|
|
6671
|
-
}
|
|
6672
|
-
__name(unwrapDefault, "unwrapDefault");
|
|
6673
|
-
function v4Entry(module_) {
|
|
6674
|
-
if (!isIndexable(module_)) return void 0;
|
|
6675
|
-
const parse = Reflect.get(module_, "parseExpression");
|
|
6676
|
-
if (typeof parse !== "function") return void 0;
|
|
6677
|
-
return (expr, options) => parse.call(module_, expr, options);
|
|
6678
|
-
}
|
|
6679
|
-
__name(v4Entry, "v4Entry");
|
|
6680
|
-
function v5Entry(module_) {
|
|
6681
|
-
if (!isIndexable(module_)) return void 0;
|
|
6682
|
-
const parser = Reflect.get(module_, "CronExpressionParser");
|
|
6683
|
-
if (!isIndexable(parser)) return void 0;
|
|
6684
|
-
const parse = Reflect.get(parser, "parse");
|
|
6685
|
-
if (typeof parse !== "function") return void 0;
|
|
6686
|
-
return (expr, options) => parse.call(parser, expr, options);
|
|
6687
|
-
}
|
|
6688
|
-
__name(v5Entry, "v5Entry");
|
|
6689
|
-
function resolveCronParse(module_) {
|
|
6690
|
-
const resolved = unwrapDefault(module_);
|
|
6691
|
-
return v4Entry(resolved) ?? v5Entry(resolved);
|
|
6692
|
-
}
|
|
6693
|
-
__name(resolveCronParse, "resolveCronParse");
|
|
6694
|
-
|
|
6695
|
-
// src/scheduler.ts
|
|
6696
|
-
var nodeRequire = createRequire(import.meta.url);
|
|
6697
|
-
var defaultSleep = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "defaultSleep");
|
|
6698
|
-
function scheduledRunId(key, everyMs, nowMs) {
|
|
6699
|
-
return `sched:${key}:${Math.floor(nowMs / everyMs)}`;
|
|
6700
|
-
}
|
|
6701
|
-
__name(scheduledRunId, "scheduledRunId");
|
|
6702
|
-
var cronParser;
|
|
6703
|
-
function loadCronParser() {
|
|
6704
|
-
if (cronParser !== void 0) return cronParser;
|
|
6705
|
-
try {
|
|
6706
|
-
cronParser = nodeRequire("cron-parser");
|
|
6707
|
-
} catch {
|
|
6708
|
-
throw new Error('cron schedules need the optional peer dependency "cron-parser" \u2014 install it (e.g. `npm i cron-parser`).');
|
|
6709
|
-
}
|
|
6710
|
-
return cronParser;
|
|
6711
|
-
}
|
|
6712
|
-
__name(loadCronParser, "loadCronParser");
|
|
6713
|
-
function prevCronFireMs(expr, nowMs, timezone = "UTC") {
|
|
6714
|
-
const parse = resolveCronParse(loadCronParser());
|
|
6715
|
-
if (!parse) {
|
|
6716
|
-
throw new Error(`the installed "cron-parser" exposes neither v4's parseExpression nor v5's CronExpressionParser.parse \u2014 the supported range is ^4.0.0 || ^5.0.0.`);
|
|
6717
|
-
}
|
|
6718
|
-
return parse(expr, {
|
|
6719
|
-
currentDate: new Date(nowMs + 1),
|
|
6720
|
-
tz: timezone
|
|
6721
|
-
}).prev().toDate().getTime();
|
|
6722
|
-
}
|
|
6723
|
-
__name(prevCronFireMs, "prevCronFireMs");
|
|
6724
|
-
function scheduleRunIdAt(s, nowMs) {
|
|
6725
|
-
if (s.cron != null) return `sched:${s.key}:${prevCronFireMs(s.cron, nowMs, s.timezone)}`;
|
|
6726
|
-
if (s.everyMs != null) return scheduledRunId(s.key, s.everyMs, nowMs);
|
|
6727
|
-
throw new Error(`schedule "${s.key}" needs either "everyMs" or "cron"`);
|
|
6728
|
-
}
|
|
6729
|
-
__name(scheduleRunIdAt, "scheduleRunIdAt");
|
|
6730
|
-
function scheduleRunIdsAt(s, nowMs) {
|
|
6731
|
-
const current = scheduleRunIdAt(s, nowMs);
|
|
6732
|
-
if (!s.backfill || s.backfill.maxCatchup <= 0) return [
|
|
6733
|
-
current
|
|
6734
|
-
];
|
|
6735
|
-
const ids = [];
|
|
6736
|
-
if (s.everyMs != null) {
|
|
6737
|
-
const bucket = Math.floor(nowMs / s.everyMs);
|
|
6738
|
-
const oldest = Math.max(0, bucket - s.backfill.maxCatchup);
|
|
6739
|
-
for (let b = oldest; b < bucket; b += 1) ids.push(`sched:${s.key}:${b}`);
|
|
6740
|
-
} else if (s.cron != null) {
|
|
6741
|
-
const prior = [];
|
|
6742
|
-
let cursor = prevCronFireMs(s.cron, nowMs, s.timezone);
|
|
6743
|
-
for (let i = 0; i < s.backfill.maxCatchup; i += 1) {
|
|
6744
|
-
const before = prevCronFireMs(s.cron, cursor - 1, s.timezone);
|
|
6745
|
-
prior.push(`sched:${s.key}:${before}`);
|
|
6746
|
-
cursor = before;
|
|
6747
|
-
}
|
|
6748
|
-
ids.push(...prior.reverse());
|
|
6749
|
-
}
|
|
6750
|
-
ids.push(current);
|
|
6751
|
-
return ids;
|
|
6752
|
-
}
|
|
6753
|
-
__name(scheduleRunIdsAt, "scheduleRunIdsAt");
|
|
6754
|
-
async function runSchedules(engine, schedules, nowMs, opts) {
|
|
6755
|
-
const random = opts?.random ?? Math.random;
|
|
6756
|
-
const sleep = opts?.sleep ?? defaultSleep;
|
|
6757
|
-
const ids = [];
|
|
6758
|
-
for (const s of schedules) {
|
|
6759
|
-
if (s.paused) continue;
|
|
6760
|
-
if (s.overlap === "skip" && s.everyMs) {
|
|
6761
|
-
const prevBucket = Math.floor(nowMs / s.everyMs) - 1;
|
|
6762
|
-
if (prevBucket >= 0) {
|
|
6763
|
-
const prev = await engine.getRun(`sched:${s.key}:${prevBucket}`);
|
|
6764
|
-
if (prev && (prev.status === "running" || prev.status === "suspended")) continue;
|
|
6765
|
-
}
|
|
6766
|
-
}
|
|
6767
|
-
if (s.jitter && s.jitter > 0) await sleep(Math.floor(random() * s.jitter));
|
|
6768
|
-
for (const runId of scheduleRunIdsAt(s, nowMs)) {
|
|
6769
|
-
await engine.start(s.workflow, s.input, runId);
|
|
6770
|
-
ids.push(runId);
|
|
6771
|
-
}
|
|
6772
|
-
}
|
|
6773
|
-
return ids;
|
|
6774
|
-
}
|
|
6775
|
-
__name(runSchedules, "runSchedules");
|
|
6776
|
-
|
|
6777
7489
|
// src/search-attributes.ts
|
|
6778
7490
|
function compare(actual, filter) {
|
|
6779
7491
|
if (filter.op === "in") return filter.values.some((v) => actual === v);
|
|
@@ -6981,6 +7693,38 @@ var InMemoryStateStore = class {
|
|
|
6981
7693
|
}
|
|
6982
7694
|
this.reindexAttributes(runId, void 0);
|
|
6983
7695
|
}
|
|
7696
|
+
async deleteRuns(runIds) {
|
|
7697
|
+
for (const runId of runIds) await this.deleteRun(runId);
|
|
7698
|
+
}
|
|
7699
|
+
async pruneTerminalRuns(policy, nowMs, limit) {
|
|
7700
|
+
const statuses = policy.statuses.filter((s) => TERMINAL_RUN_STATUSES.includes(s));
|
|
7701
|
+
if (statuses.length === 0 || limit <= 0) return 0;
|
|
7702
|
+
if (policy.maxAge == null && policy.maxCount == null) return 0;
|
|
7703
|
+
const inScope = this.matching({
|
|
7704
|
+
...policy.scope ?? {},
|
|
7705
|
+
statuses
|
|
7706
|
+
});
|
|
7707
|
+
const ids = /* @__PURE__ */ new Set();
|
|
7708
|
+
if (policy.maxAge != null) {
|
|
7709
|
+
const cutoff = nowMs - parseDuration(policy.maxAge);
|
|
7710
|
+
const expired = inScope.filter((r) => r.updatedAt.getTime() < cutoff).sort((a, b) => a.updatedAt.getTime() - b.updatedAt.getTime()).slice(0, limit);
|
|
7711
|
+
for (const r of expired) ids.add(r.id);
|
|
7712
|
+
}
|
|
7713
|
+
if (policy.maxCount != null && ids.size < limit) {
|
|
7714
|
+
const byRecency = [
|
|
7715
|
+
...inScope
|
|
7716
|
+
].sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime() || b.id.localeCompare(a.id));
|
|
7717
|
+
for (const r of byRecency.slice(policy.maxCount)) {
|
|
7718
|
+
if (ids.size >= limit) break;
|
|
7719
|
+
ids.add(r.id);
|
|
7720
|
+
}
|
|
7721
|
+
}
|
|
7722
|
+
const doomed = [
|
|
7723
|
+
...ids
|
|
7724
|
+
].slice(0, limit);
|
|
7725
|
+
await this.deleteRuns(doomed);
|
|
7726
|
+
return doomed.length;
|
|
7727
|
+
}
|
|
6984
7728
|
async getCheckpoint(runId, seq) {
|
|
6985
7729
|
const cp = this.checkpoints.get(this.key(runId, seq));
|
|
6986
7730
|
return cp ? {
|
|
@@ -7188,6 +7932,9 @@ var InMemoryStateStore = class {
|
|
|
7188
7932
|
...r
|
|
7189
7933
|
}));
|
|
7190
7934
|
}
|
|
7935
|
+
async countRuns(query) {
|
|
7936
|
+
return this.matching(query).length;
|
|
7937
|
+
}
|
|
7191
7938
|
async runFacets(query) {
|
|
7192
7939
|
return mergeRunFacetRows(this.matching(query).map((run) => ({
|
|
7193
7940
|
status: run.status,
|
|
@@ -7205,6 +7952,34 @@ var InMemoryStateStore = class {
|
|
|
7205
7952
|
});
|
|
7206
7953
|
return runValueFacetsFromRuns(runs, axis, opts);
|
|
7207
7954
|
}
|
|
7955
|
+
schedules = /* @__PURE__ */ new Map();
|
|
7956
|
+
async saveSchedule(record) {
|
|
7957
|
+
this.schedules.set(record.id, structuredClone(record));
|
|
7958
|
+
}
|
|
7959
|
+
async getSchedule(id) {
|
|
7960
|
+
const record = this.schedules.get(id);
|
|
7961
|
+
return record ? structuredClone(record) : null;
|
|
7962
|
+
}
|
|
7963
|
+
async updateSchedule(id, patch, expectedNextFireAt) {
|
|
7964
|
+
const record = this.schedules.get(id);
|
|
7965
|
+
if (!record) return false;
|
|
7966
|
+
if (expectedNextFireAt !== void 0 && record.nextFireAt !== expectedNextFireAt) return false;
|
|
7967
|
+
this.schedules.set(id, {
|
|
7968
|
+
...record,
|
|
7969
|
+
...structuredClone(patch)
|
|
7970
|
+
});
|
|
7971
|
+
return true;
|
|
7972
|
+
}
|
|
7973
|
+
async deleteSchedule(id) {
|
|
7974
|
+
return this.schedules.delete(id);
|
|
7975
|
+
}
|
|
7976
|
+
async listSchedules(query) {
|
|
7977
|
+
const rows = [
|
|
7978
|
+
...this.schedules.values()
|
|
7979
|
+
].filter((s) => query.namespace === void 0 || s.namespace === query.namespace).filter((s) => query.workflow === void 0 || s.workflow === query.workflow).filter((s) => query.tag === void 0 || (s.tags ?? []).includes(query.tag)).filter((s) => query.paused === void 0 || s.paused === query.paused).filter((s) => query.dueBy === void 0 || !s.paused && s.nextFireAt !== null && s.nextFireAt <= query.dueBy).sort((a, b) => (a.nextFireAt ?? Number.POSITIVE_INFINITY) - (b.nextFireAt ?? Number.POSITIVE_INFINITY) || a.id.localeCompare(b.id));
|
|
7980
|
+
const offset = query.offset ?? 0;
|
|
7981
|
+
return rows.slice(offset, query.limit != null ? offset + query.limit : void 0).map((s) => structuredClone(s));
|
|
7982
|
+
}
|
|
7208
7983
|
async listCheckpoints(runId) {
|
|
7209
7984
|
const rows = [
|
|
7210
7985
|
...this.checkpoints.values()
|
|
@@ -7285,7 +8060,9 @@ export {
|
|
|
7285
8060
|
CONTROL_FLOW_SIGNAL,
|
|
7286
8061
|
CURRENT_PROTOCOL_VERSION,
|
|
7287
8062
|
CodecStateStore,
|
|
8063
|
+
ConcurrencyLimitError,
|
|
7288
8064
|
ContinueAsNew,
|
|
8065
|
+
DEFAULT_CONCURRENCY_STATUSES,
|
|
7289
8066
|
DEFAULT_PARTITION,
|
|
7290
8067
|
DURABLE_OPTIONS,
|
|
7291
8068
|
DURABLE_OPTIONS_CANONICAL,
|
|
@@ -7304,6 +8081,7 @@ export {
|
|
|
7304
8081
|
InMemoryTransport,
|
|
7305
8082
|
LEGACY_V1_CAPABILITIES,
|
|
7306
8083
|
LEGACY_V1_PROTOCOL,
|
|
8084
|
+
NestedWorkflowCallError,
|
|
7307
8085
|
NonDeterminismError,
|
|
7308
8086
|
QueueController,
|
|
7309
8087
|
RUN_VALUE_FACET_LIMIT,
|
|
@@ -7315,6 +8093,9 @@ export {
|
|
|
7315
8093
|
RunGateway,
|
|
7316
8094
|
STATE_STORE,
|
|
7317
8095
|
STATE_STORE_CANONICAL,
|
|
8096
|
+
ScheduleAlreadyExistsError,
|
|
8097
|
+
ScheduleClient,
|
|
8098
|
+
ScheduleNotFoundError,
|
|
7318
8099
|
SignalTimeoutError,
|
|
7319
8100
|
SingletonQueueFullError,
|
|
7320
8101
|
TERMINAL_RUN_STATUSES,
|
|
@@ -7328,6 +8109,7 @@ export {
|
|
|
7328
8109
|
aggregateAnnouncements,
|
|
7329
8110
|
announcementsOf,
|
|
7330
8111
|
applyAttributeQuery,
|
|
8112
|
+
assertConcurrency,
|
|
7331
8113
|
attributeColumnFor,
|
|
7332
8114
|
attributeOperand,
|
|
7333
8115
|
attributePredicateOperands,
|
|
@@ -7340,10 +8122,13 @@ export {
|
|
|
7340
8122
|
classifyWaiterToken,
|
|
7341
8123
|
clearDurableExecutionWrappers,
|
|
7342
8124
|
collectMetrics,
|
|
8125
|
+
concurrencyTag,
|
|
7343
8126
|
controlPlaneDescriptor,
|
|
8127
|
+
countRuns,
|
|
7344
8128
|
coveredTokens,
|
|
7345
8129
|
createStepLogger,
|
|
7346
8130
|
currentStep,
|
|
8131
|
+
currentStepScope,
|
|
7347
8132
|
currentWorkflowCtx,
|
|
7348
8133
|
debug,
|
|
7349
8134
|
describeWorker,
|
|
@@ -7362,6 +8147,7 @@ export {
|
|
|
7362
8147
|
isEngineMintedTag,
|
|
7363
8148
|
isEventToken,
|
|
7364
8149
|
isLegacyDescriptor,
|
|
8150
|
+
isNonEmptyRunScope,
|
|
7365
8151
|
isWorkflowControlFlowSignal,
|
|
7366
8152
|
log,
|
|
7367
8153
|
matchesAttributes,
|
|
@@ -7369,6 +8155,7 @@ export {
|
|
|
7369
8155
|
mergeRunValueFacetRows,
|
|
7370
8156
|
missingCapabilities,
|
|
7371
8157
|
negotiate,
|
|
8158
|
+
nextCronFireMs,
|
|
7372
8159
|
normalizeAttributeRows,
|
|
7373
8160
|
normalizeDescriptor,
|
|
7374
8161
|
observedAnnouncements,
|
|
@@ -7378,11 +8165,14 @@ export {
|
|
|
7378
8165
|
prevCronFireMs,
|
|
7379
8166
|
readSearchAttributes,
|
|
7380
8167
|
requiredCapabilities,
|
|
8168
|
+
resolveConcurrency,
|
|
7381
8169
|
resolveRouting,
|
|
7382
8170
|
resolveRunWaiting,
|
|
7383
8171
|
runDurableExecution,
|
|
7384
8172
|
runInStepLogger,
|
|
8173
|
+
runInStepScope,
|
|
7385
8174
|
runInWorkflowCtx,
|
|
8175
|
+
runOutsideWorkflowCtx,
|
|
7386
8176
|
runSchedules,
|
|
7387
8177
|
runStepHandler,
|
|
7388
8178
|
runValueFacetsFromRuns,
|