@dudousxd/nestjs-durable-core 0.71.1 → 0.72.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 CHANGED
@@ -25,7 +25,9 @@ __export(index_exports, {
25
25
  CONTROL_FLOW_SIGNAL: () => CONTROL_FLOW_SIGNAL,
26
26
  CURRENT_PROTOCOL_VERSION: () => CURRENT_PROTOCOL_VERSION,
27
27
  CodecStateStore: () => CodecStateStore,
28
+ ConcurrencyLimitError: () => ConcurrencyLimitError,
28
29
  ContinueAsNew: () => ContinueAsNew,
30
+ DEFAULT_CONCURRENCY_STATUSES: () => DEFAULT_CONCURRENCY_STATUSES,
29
31
  DEFAULT_PARTITION: () => DEFAULT_PARTITION,
30
32
  DURABLE_OPTIONS: () => DURABLE_OPTIONS,
31
33
  DURABLE_OPTIONS_CANONICAL: () => DURABLE_OPTIONS_CANONICAL,
@@ -55,6 +57,9 @@ __export(index_exports, {
55
57
  RunGateway: () => RunGateway,
56
58
  STATE_STORE: () => STATE_STORE,
57
59
  STATE_STORE_CANONICAL: () => STATE_STORE_CANONICAL,
60
+ ScheduleAlreadyExistsError: () => ScheduleAlreadyExistsError,
61
+ ScheduleClient: () => ScheduleClient,
62
+ ScheduleNotFoundError: () => ScheduleNotFoundError,
58
63
  SignalTimeoutError: () => SignalTimeoutError,
59
64
  SingletonQueueFullError: () => SingletonQueueFullError,
60
65
  TERMINAL_RUN_STATUSES: () => TERMINAL_RUN_STATUSES,
@@ -68,6 +73,7 @@ __export(index_exports, {
68
73
  aggregateAnnouncements: () => aggregateAnnouncements,
69
74
  announcementsOf: () => announcementsOf,
70
75
  applyAttributeQuery: () => applyAttributeQuery,
76
+ assertConcurrency: () => assertConcurrency,
71
77
  attributeColumnFor: () => attributeColumnFor,
72
78
  attributeOperand: () => attributeOperand,
73
79
  attributePredicateOperands: () => attributePredicateOperands,
@@ -80,7 +86,9 @@ __export(index_exports, {
80
86
  classifyWaiterToken: () => classifyWaiterToken,
81
87
  clearDurableExecutionWrappers: () => clearDurableExecutionWrappers,
82
88
  collectMetrics: () => collectMetrics,
89
+ concurrencyTag: () => concurrencyTag,
83
90
  controlPlaneDescriptor: () => controlPlaneDescriptor,
91
+ countRuns: () => countRuns,
84
92
  coveredTokens: () => coveredTokens,
85
93
  createStepLogger: () => createStepLogger,
86
94
  currentStep: () => currentStep,
@@ -102,6 +110,7 @@ __export(index_exports, {
102
110
  isEngineMintedTag: () => isEngineMintedTag,
103
111
  isEventToken: () => isEventToken,
104
112
  isLegacyDescriptor: () => isLegacyDescriptor,
113
+ isNonEmptyRunScope: () => isNonEmptyRunScope,
105
114
  isWorkflowControlFlowSignal: () => isWorkflowControlFlowSignal,
106
115
  log: () => log,
107
116
  matchesAttributes: () => matchesAttributes,
@@ -109,6 +118,7 @@ __export(index_exports, {
109
118
  mergeRunValueFacetRows: () => mergeRunValueFacetRows,
110
119
  missingCapabilities: () => missingCapabilities,
111
120
  negotiate: () => negotiate,
121
+ nextCronFireMs: () => nextCronFireMs,
112
122
  normalizeAttributeRows: () => normalizeAttributeRows,
113
123
  normalizeDescriptor: () => normalizeDescriptor,
114
124
  observedAnnouncements: () => observedAnnouncements,
@@ -118,6 +128,7 @@ __export(index_exports, {
118
128
  prevCronFireMs: () => prevCronFireMs,
119
129
  readSearchAttributes: () => readSearchAttributes,
120
130
  requiredCapabilities: () => requiredCapabilities,
131
+ resolveConcurrency: () => resolveConcurrency,
121
132
  resolveRouting: () => resolveRouting,
122
133
  resolveRunWaiting: () => resolveRunWaiting,
123
134
  runDurableExecution: () => runDurableExecution,
@@ -391,6 +402,23 @@ var FatalError = class extends Error {
391
402
  this.code = code;
392
403
  }
393
404
  };
405
+ var ConcurrencyLimitError = class extends Error {
406
+ static {
407
+ __name(this, "ConcurrencyLimitError");
408
+ }
409
+ workflow;
410
+ key;
411
+ limit;
412
+ active;
413
+ constructor(workflow, key, limit, active) {
414
+ super(`concurrency limit reached for key "${key}" (${active}/${limit} in flight); ${workflow} was not started \u2014 retry later`);
415
+ this.name = "ConcurrencyLimitError";
416
+ this.workflow = workflow;
417
+ this.key = key;
418
+ this.limit = limit;
419
+ this.active = active;
420
+ }
421
+ };
394
422
  var SingletonQueueFullError = class extends Error {
395
423
  static {
396
424
  __name(this, "SingletonQueueFullError");
@@ -904,6 +932,58 @@ function instantCheckpoint(p) {
904
932
  }
905
933
  __name(instantCheckpoint, "instantCheckpoint");
906
934
 
935
+ // src/concurrency.ts
936
+ var DEFAULT_CONCURRENCY_STATUSES = [
937
+ "pending",
938
+ "running",
939
+ "suspended",
940
+ "cancelling",
941
+ "blocked"
942
+ ];
943
+ function concurrencyTag(key) {
944
+ return `concurrency:${key}`;
945
+ }
946
+ __name(concurrencyTag, "concurrencyTag");
947
+ async function resolveConcurrency(override, config, input) {
948
+ if (override) return override;
949
+ if (!config) return void 0;
950
+ const key = config.key(input);
951
+ if (key === void 0) return void 0;
952
+ const limit = typeof config.limit === "function" ? await config.limit(key) : config.limit;
953
+ return {
954
+ key,
955
+ limit,
956
+ countStatuses: config.countStatuses
957
+ };
958
+ }
959
+ __name(resolveConcurrency, "resolveConcurrency");
960
+ async function countRuns(store, query) {
961
+ if (store.countRuns) return store.countRuns(query);
962
+ if (store.runFacets && !query.status && !query.statuses && query.origin === void 0) {
963
+ const cells = await store.runFacets(query);
964
+ return cells.reduce((sum, c) => sum + c.count, 0);
965
+ }
966
+ if (store.runFacets && query.statuses && !query.status && query.origin === void 0) {
967
+ const { statuses, ...rest } = query;
968
+ const cells = await store.runFacets(rest);
969
+ return cells.filter((c) => statuses.includes(c.status)).reduce((sum, c) => sum + c.count, 0);
970
+ }
971
+ return (await store.listRuns(query)).length;
972
+ }
973
+ __name(countRuns, "countRuns");
974
+ async function assertConcurrency(store, workflow, quota) {
975
+ const active = quota.limit <= 0 ? 0 : await countRuns(store, {
976
+ tag: concurrencyTag(quota.key),
977
+ statuses: [
978
+ ...quota.countStatuses ?? DEFAULT_CONCURRENCY_STATUSES
979
+ ]
980
+ });
981
+ if (quota.limit <= 0 || active >= quota.limit) {
982
+ throw new ConcurrencyLimitError(workflow, quota.key, quota.limit, active);
983
+ }
984
+ }
985
+ __name(assertConcurrency, "assertConcurrency");
986
+
907
987
  // src/entities.ts
908
988
  var ENTITY_WORKFLOW = "__entity";
909
989
  var entityRunId = /* @__PURE__ */ __name((name, key) => `entity:${name}:${key}`, "entityRunId");
@@ -1736,6 +1816,19 @@ function planDispatch(requires, liveDescriptors, cp, token) {
1736
1816
  }
1737
1817
  __name(planDispatch, "planDispatch");
1738
1818
 
1819
+ // src/interfaces.ts
1820
+ var TERMINAL_RUN_STATUSES = [
1821
+ "completed",
1822
+ "failed",
1823
+ "cancelled",
1824
+ "dead"
1825
+ ];
1826
+ function isNonEmptyRunScope(scope) {
1827
+ if (!scope) return false;
1828
+ 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;
1829
+ }
1830
+ __name(isNonEmptyRunScope, "isNonEmptyRunScope");
1831
+
1739
1832
  // src/tenant-group.ts
1740
1833
  function tenantGroup(baseGroup, tenant) {
1741
1834
  return tenant !== void 0 && tenant !== "" && tenant !== "default" ? `${baseGroup}@${tenant}` : baseGroup;
@@ -1826,6 +1919,486 @@ function indexWaitersByRun(waiters) {
1826
1919
  }
1827
1920
  __name(indexWaitersByRun, "indexWaitersByRun");
1828
1921
 
1922
+ // src/scheduler.ts
1923
+ var import_node_module = require("module");
1924
+
1925
+ // src/cron-compat.ts
1926
+ function isIndexable(value) {
1927
+ return typeof value === "object" && value !== null || typeof value === "function";
1928
+ }
1929
+ __name(isIndexable, "isIndexable");
1930
+ function unwrapDefault(module_) {
1931
+ if (!isIndexable(module_)) return module_;
1932
+ if (Reflect.get(module_, "parseExpression") !== void 0) return module_;
1933
+ if (Reflect.get(module_, "CronExpressionParser") !== void 0) return module_;
1934
+ const inner = Reflect.get(module_, "default");
1935
+ return inner === void 0 ? module_ : inner;
1936
+ }
1937
+ __name(unwrapDefault, "unwrapDefault");
1938
+ function v4Entry(module_) {
1939
+ if (!isIndexable(module_)) return void 0;
1940
+ const parse = Reflect.get(module_, "parseExpression");
1941
+ if (typeof parse !== "function") return void 0;
1942
+ return (expr, options) => parse.call(module_, expr, options);
1943
+ }
1944
+ __name(v4Entry, "v4Entry");
1945
+ function v5Entry(module_) {
1946
+ if (!isIndexable(module_)) return void 0;
1947
+ const parser = Reflect.get(module_, "CronExpressionParser");
1948
+ if (!isIndexable(parser)) return void 0;
1949
+ const parse = Reflect.get(parser, "parse");
1950
+ if (typeof parse !== "function") return void 0;
1951
+ return (expr, options) => parse.call(parser, expr, options);
1952
+ }
1953
+ __name(v5Entry, "v5Entry");
1954
+ function resolveCronParse(module_) {
1955
+ const resolved = unwrapDefault(module_);
1956
+ return v4Entry(resolved) ?? v5Entry(resolved);
1957
+ }
1958
+ __name(resolveCronParse, "resolveCronParse");
1959
+
1960
+ // src/scheduler.ts
1961
+ var nodeRequire = (0, import_node_module.createRequire)(__importMetaUrl);
1962
+ var defaultSleep = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "defaultSleep");
1963
+ function scheduledRunId(key, everyMs, nowMs) {
1964
+ return `sched:${key}:${Math.floor(nowMs / everyMs)}`;
1965
+ }
1966
+ __name(scheduledRunId, "scheduledRunId");
1967
+ var cronParser;
1968
+ function loadCronParser() {
1969
+ if (cronParser !== void 0) return cronParser;
1970
+ try {
1971
+ cronParser = nodeRequire("cron-parser");
1972
+ } catch {
1973
+ throw new Error('cron schedules need the optional peer dependency "cron-parser" \u2014 install it (e.g. `npm i cron-parser`).');
1974
+ }
1975
+ return cronParser;
1976
+ }
1977
+ __name(loadCronParser, "loadCronParser");
1978
+ function prevCronFireMs(expr, nowMs, timezone = "UTC") {
1979
+ const parse = resolveCronParse(loadCronParser());
1980
+ if (!parse) {
1981
+ 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.`);
1982
+ }
1983
+ return parse(expr, {
1984
+ currentDate: new Date(nowMs + 1),
1985
+ tz: timezone
1986
+ }).prev().toDate().getTime();
1987
+ }
1988
+ __name(prevCronFireMs, "prevCronFireMs");
1989
+ function nextCronFireMs(expr, afterMs, timezone = "UTC") {
1990
+ const parse = resolveCronParse(loadCronParser());
1991
+ if (!parse) {
1992
+ 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.`);
1993
+ }
1994
+ return parse(expr, {
1995
+ currentDate: new Date(afterMs),
1996
+ tz: timezone
1997
+ }).next().toDate().getTime();
1998
+ }
1999
+ __name(nextCronFireMs, "nextCronFireMs");
2000
+ function scheduleRunIdAt(s, nowMs) {
2001
+ if (s.cron != null) return `sched:${s.key}:${prevCronFireMs(s.cron, nowMs, s.timezone)}`;
2002
+ if (s.everyMs != null) return scheduledRunId(s.key, s.everyMs, nowMs);
2003
+ throw new Error(`schedule "${s.key}" needs either "everyMs" or "cron"`);
2004
+ }
2005
+ __name(scheduleRunIdAt, "scheduleRunIdAt");
2006
+ function scheduleRunIdsAt(s, nowMs) {
2007
+ const current = scheduleRunIdAt(s, nowMs);
2008
+ if (!s.backfill || s.backfill.maxCatchup <= 0) return [
2009
+ current
2010
+ ];
2011
+ const ids = [];
2012
+ if (s.everyMs != null) {
2013
+ const bucket = Math.floor(nowMs / s.everyMs);
2014
+ const oldest = Math.max(0, bucket - s.backfill.maxCatchup);
2015
+ for (let b = oldest; b < bucket; b += 1) ids.push(`sched:${s.key}:${b}`);
2016
+ } else if (s.cron != null) {
2017
+ const prior = [];
2018
+ let cursor = prevCronFireMs(s.cron, nowMs, s.timezone);
2019
+ for (let i = 0; i < s.backfill.maxCatchup; i += 1) {
2020
+ const before = prevCronFireMs(s.cron, cursor - 1, s.timezone);
2021
+ prior.push(`sched:${s.key}:${before}`);
2022
+ cursor = before;
2023
+ }
2024
+ ids.push(...prior.reverse());
2025
+ }
2026
+ ids.push(current);
2027
+ return ids;
2028
+ }
2029
+ __name(scheduleRunIdsAt, "scheduleRunIdsAt");
2030
+ async function runSchedules(engine, schedules, nowMs, opts) {
2031
+ const random = opts?.random ?? Math.random;
2032
+ const sleep = opts?.sleep ?? defaultSleep;
2033
+ const ids = [];
2034
+ for (const s of schedules) {
2035
+ if (s.paused) continue;
2036
+ if (s.overlap === "skip" && s.everyMs) {
2037
+ const prevBucket = Math.floor(nowMs / s.everyMs) - 1;
2038
+ if (prevBucket >= 0) {
2039
+ const prev = await engine.getRun(`sched:${s.key}:${prevBucket}`);
2040
+ if (prev && (prev.status === "running" || prev.status === "suspended")) continue;
2041
+ }
2042
+ }
2043
+ if (s.jitter && s.jitter > 0) await sleep(Math.floor(random() * s.jitter));
2044
+ for (const runId of scheduleRunIdsAt(s, nowMs)) {
2045
+ await engine.start(s.workflow, s.input, runId);
2046
+ ids.push(runId);
2047
+ }
2048
+ }
2049
+ return ids;
2050
+ }
2051
+ __name(runSchedules, "runSchedules");
2052
+
2053
+ // src/schedules.ts
2054
+ var ScheduleAlreadyExistsError = class extends Error {
2055
+ static {
2056
+ __name(this, "ScheduleAlreadyExistsError");
2057
+ }
2058
+ scheduleId;
2059
+ constructor(scheduleId) {
2060
+ super(`schedule "${scheduleId}" already exists`), this.scheduleId = scheduleId;
2061
+ this.name = "ScheduleAlreadyExistsError";
2062
+ }
2063
+ };
2064
+ var ScheduleNotFoundError = class extends Error {
2065
+ static {
2066
+ __name(this, "ScheduleNotFoundError");
2067
+ }
2068
+ scheduleId;
2069
+ constructor(scheduleId) {
2070
+ super(`schedule "${scheduleId}" not found`), this.scheduleId = scheduleId;
2071
+ this.name = "ScheduleNotFoundError";
2072
+ }
2073
+ };
2074
+ var DEFAULT_NAMESPACE = "default";
2075
+ var ON_TIME_GRACE_MS = 6e4;
2076
+ var TICK_BATCH = 100;
2077
+ var ScheduleClient = class {
2078
+ static {
2079
+ __name(this, "ScheduleClient");
2080
+ }
2081
+ store;
2082
+ host;
2083
+ options;
2084
+ constructor(store, host, options = {}) {
2085
+ this.store = store;
2086
+ this.host = host;
2087
+ this.options = options;
2088
+ }
2089
+ now() {
2090
+ return (this.options.clock ?? Date.now)();
2091
+ }
2092
+ /** Whether the store persists schedules (implements the schedule methods). */
2093
+ get supported() {
2094
+ const s = this.store;
2095
+ return !!(s.saveSchedule && s.getSchedule && s.updateSchedule && s.deleteSchedule && s.listSchedules);
2096
+ }
2097
+ requireStore() {
2098
+ if (!this.supported) {
2099
+ 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");
2100
+ }
2101
+ const s = this.store;
2102
+ return {
2103
+ saveSchedule: /* @__PURE__ */ __name((r) => s.saveSchedule.call(s, r), "saveSchedule"),
2104
+ getSchedule: /* @__PURE__ */ __name((id) => s.getSchedule.call(s, id), "getSchedule"),
2105
+ updateSchedule: /* @__PURE__ */ __name((id, patch, expected) => s.updateSchedule.call(s, id, patch, expected), "updateSchedule"),
2106
+ deleteSchedule: /* @__PURE__ */ __name((id) => s.deleteSchedule.call(s, id), "deleteSchedule"),
2107
+ listSchedules: /* @__PURE__ */ __name((q) => s.listSchedules.call(s, q), "listSchedules")
2108
+ };
2109
+ }
2110
+ /** Create a schedule. Throws {@link ScheduleAlreadyExistsError} if the id is taken. */
2111
+ async create(opts) {
2112
+ const store = this.requireStore();
2113
+ if (await store.getSchedule(opts.id)) throw new ScheduleAlreadyExistsError(opts.id);
2114
+ return this.write(opts, null);
2115
+ }
2116
+ /**
2117
+ * Create the schedule, or replace the definition of an existing one (its history — last fire, last
2118
+ * run, fire count — is kept). The next fire is recomputed from now under the new definition, so a
2119
+ * window that already fired is never fired again by an upsert.
2120
+ */
2121
+ async upsert(opts) {
2122
+ const store = this.requireStore();
2123
+ return this.write(opts, await store.getSchedule(opts.id));
2124
+ }
2125
+ /** The schedule `id`, or null. */
2126
+ async get(id) {
2127
+ const record = await this.requireStore().getSchedule(id);
2128
+ return record ? describe(record) : null;
2129
+ }
2130
+ /** Schedules matching `query`, soonest-due first. */
2131
+ async list(query = {}) {
2132
+ return (await this.requireStore().listSchedules(query)).map(describe);
2133
+ }
2134
+ /** Delete the schedule. Runs it already started are left alone. Returns whether it existed. */
2135
+ delete(id) {
2136
+ return this.requireStore().deleteSchedule(id);
2137
+ }
2138
+ /** Stop firing until {@link resume}. `note` records why (shown by `get`/`list`). */
2139
+ async pause(id, note) {
2140
+ const store = this.requireStore();
2141
+ const record = await this.mustGet(id);
2142
+ const spec = {
2143
+ ...record.spec
2144
+ };
2145
+ if (note !== void 0) spec.note = note;
2146
+ await store.updateSchedule(id, {
2147
+ paused: true,
2148
+ spec,
2149
+ updatedAt: new Date(this.now())
2150
+ });
2151
+ return describe(await this.mustGet(id));
2152
+ }
2153
+ /**
2154
+ * Fire again from the next window after now — the windows skipped while paused are NOT caught up.
2155
+ */
2156
+ async resume(id) {
2157
+ const store = this.requireStore();
2158
+ const record = await this.mustGet(id);
2159
+ const spec = record.spec;
2160
+ const window = nextWindowAfter(spec, this.now());
2161
+ await store.updateSchedule(id, {
2162
+ paused: false,
2163
+ nextFireAt: window === null ? null : window + jitterFor(record.id, window, spec),
2164
+ state: {
2165
+ ...record.state,
2166
+ window
2167
+ },
2168
+ updatedAt: new Date(this.now())
2169
+ });
2170
+ return describe(await this.mustGet(id));
2171
+ }
2172
+ /**
2173
+ * Start one run of the schedule right now, outside its cadence (paused or not). The run id is
2174
+ * `sched:<id>:manual:<nowMs>`; it doesn't move the schedule's next fire.
2175
+ */
2176
+ async trigger(id) {
2177
+ const record = await this.mustGet(id);
2178
+ const runId = `sched:${id}:manual:${this.now()}`;
2179
+ return this.host.start(record.workflow, specOf(record).input, runId, startOptions(record));
2180
+ }
2181
+ /**
2182
+ * Fire every due schedule (in this engine's namespace; all namespaces for an operator engine):
2183
+ * start its window's run and advance it to the next window. Called by the NestJS timer poller on
2184
+ * each tick when `persistedSchedules` is on; call it yourself from any other driver. Returns the
2185
+ * run ids started (or re-found, for an idempotent re-fire).
2186
+ */
2187
+ async tick(nowMs = this.now()) {
2188
+ const store = this.requireStore();
2189
+ const started = [];
2190
+ for (let round = 0; round < 50; round++) {
2191
+ const due = await store.listSchedules({
2192
+ dueBy: nowMs,
2193
+ namespace: this.options.namespace,
2194
+ limit: TICK_BATCH
2195
+ });
2196
+ for (const record of due) {
2197
+ const runId = await this.fire(record, nowMs);
2198
+ if (runId) started.push(runId);
2199
+ }
2200
+ if (due.length < TICK_BATCH) break;
2201
+ }
2202
+ return started;
2203
+ }
2204
+ /** Fire one due schedule and advance it. Returns the run id started, if any. */
2205
+ async fire(record, nowMs) {
2206
+ const store = this.requireStore();
2207
+ const spec = specOf(record);
2208
+ const state = record.state;
2209
+ let latest;
2210
+ let next;
2211
+ try {
2212
+ latest = latestWindowAtOrBefore(spec, nowMs);
2213
+ next = nextWindowAfter(spec, latest);
2214
+ } catch (error2) {
2215
+ await store.updateSchedule(record.id, {
2216
+ paused: true,
2217
+ nextFireAt: null,
2218
+ state: {
2219
+ ...state,
2220
+ lastError: `invalid schedule: ${errorMessage(error2)}`
2221
+ },
2222
+ updatedAt: new Date(nowMs)
2223
+ }, record.nextFireAt);
2224
+ return void 0;
2225
+ }
2226
+ const window = state.window ?? latest;
2227
+ const jitter = spec.jitterMs ?? 0;
2228
+ const onTime = nowMs - latest <= ON_TIME_GRACE_MS + jitter;
2229
+ let runId;
2230
+ let lastError;
2231
+ let fired = false;
2232
+ const skipMissed = spec.catchup === "skip" && !onTime;
2233
+ const beforeWindow = latest < window;
2234
+ if (!skipMissed && !beforeWindow && !await this.overlapping(spec, state)) {
2235
+ runId = `sched:${record.id}:${latest}`;
2236
+ try {
2237
+ await this.host.start(record.workflow, spec.input, runId, startOptions(record));
2238
+ fired = true;
2239
+ } catch (error2) {
2240
+ if (await this.host.getRun(runId)) fired = true;
2241
+ else lastError = errorMessage(error2);
2242
+ }
2243
+ }
2244
+ const nextState = {
2245
+ ...state,
2246
+ window: next,
2247
+ ...fired ? {
2248
+ lastFireAt: latest,
2249
+ lastRunId: runId,
2250
+ fires: (state.fires ?? 0) + 1
2251
+ } : {},
2252
+ lastError: fired ? void 0 : lastError ?? state.lastError
2253
+ };
2254
+ await store.updateSchedule(record.id, {
2255
+ nextFireAt: next === null ? null : next + jitterFor(record.id, next, spec),
2256
+ state: stripUndefined(nextState),
2257
+ updatedAt: new Date(nowMs)
2258
+ }, record.nextFireAt);
2259
+ return fired ? runId : void 0;
2260
+ }
2261
+ /** `overlap: 'skip'` and the previous run of this schedule is still in flight. */
2262
+ async overlapping(spec, state) {
2263
+ if (spec.overlap !== "skip" || !state.lastRunId) return false;
2264
+ const prev = await this.host.getRun(state.lastRunId);
2265
+ return !!prev && !TERMINAL_RUN_STATUSES.includes(prev.status);
2266
+ }
2267
+ async mustGet(id) {
2268
+ const record = await this.requireStore().getSchedule(id);
2269
+ if (!record) throw new ScheduleNotFoundError(id);
2270
+ return record;
2271
+ }
2272
+ async write(opts, existing) {
2273
+ const store = this.requireStore();
2274
+ const spec = toSpec(opts);
2275
+ const now = this.now();
2276
+ const paused = opts.paused ?? existing?.paused ?? false;
2277
+ const window = nextWindowAfter(spec, now);
2278
+ const record = {
2279
+ id: opts.id,
2280
+ namespace: opts.namespace ?? this.options.namespace ?? DEFAULT_NAMESPACE,
2281
+ workflow: workflowName(opts.workflow),
2282
+ paused,
2283
+ nextFireAt: window === null ? null : window + jitterFor(opts.id, window, spec),
2284
+ tags: opts.tags,
2285
+ spec,
2286
+ state: stripUndefined({
2287
+ ...existing?.state ?? {},
2288
+ window
2289
+ }),
2290
+ createdAt: existing?.createdAt ?? new Date(now),
2291
+ updatedAt: new Date(now)
2292
+ };
2293
+ await store.saveSchedule(record);
2294
+ return describe(record);
2295
+ }
2296
+ };
2297
+ function toSpec(opts) {
2298
+ if (opts.cron == null === (opts.every == null)) {
2299
+ throw new Error(`schedule "${opts.id}" needs exactly one of "cron" or "every"`);
2300
+ }
2301
+ const everyMs = opts.every != null ? parseDuration(opts.every) : void 0;
2302
+ if (everyMs !== void 0 && !(everyMs > 0)) {
2303
+ throw new Error(`schedule "${opts.id}": "every" must be a positive duration`);
2304
+ }
2305
+ const jitterMs = opts.jitter != null ? parseDuration(opts.jitter) : void 0;
2306
+ return stripUndefined({
2307
+ v: 1,
2308
+ input: opts.input,
2309
+ cron: opts.cron,
2310
+ everyMs,
2311
+ timezone: opts.timezone,
2312
+ jitterMs: jitterMs && jitterMs > 0 ? jitterMs : void 0,
2313
+ overlap: opts.overlap ?? "allow",
2314
+ catchup: opts.catchup ?? "latest",
2315
+ note: opts.note,
2316
+ searchAttributes: opts.searchAttributes,
2317
+ priority: opts.priority,
2318
+ concurrency: opts.concurrency
2319
+ });
2320
+ }
2321
+ __name(toSpec, "toSpec");
2322
+ function specOf(record) {
2323
+ return record.spec;
2324
+ }
2325
+ __name(specOf, "specOf");
2326
+ function latestWindowAtOrBefore(spec, atMs) {
2327
+ if (spec.cron != null) return prevCronFireMs(spec.cron, atMs, spec.timezone);
2328
+ const every = spec.everyMs;
2329
+ return Math.floor(atMs / every) * every;
2330
+ }
2331
+ __name(latestWindowAtOrBefore, "latestWindowAtOrBefore");
2332
+ function nextWindowAfter(spec, afterMs) {
2333
+ if (spec.cron != null) return nextCronFireMs(spec.cron, afterMs, spec.timezone);
2334
+ const every = spec.everyMs;
2335
+ return (Math.floor(afterMs / every) + 1) * every;
2336
+ }
2337
+ __name(nextWindowAfter, "nextWindowAfter");
2338
+ function jitterFor(id, window, spec) {
2339
+ const max = spec.jitterMs ?? 0;
2340
+ if (max <= 0) return 0;
2341
+ let h = 2166136261;
2342
+ for (const ch of `${id}@${window}`) {
2343
+ h ^= ch.charCodeAt(0);
2344
+ h = Math.imul(h, 16777619) >>> 0;
2345
+ }
2346
+ return h % max;
2347
+ }
2348
+ __name(jitterFor, "jitterFor");
2349
+ function startOptions(record) {
2350
+ const spec = specOf(record);
2351
+ return stripUndefined({
2352
+ tags: [
2353
+ ...record.tags ?? [],
2354
+ `schedule:${record.id}`
2355
+ ],
2356
+ namespace: record.namespace,
2357
+ searchAttributes: spec.searchAttributes,
2358
+ priority: spec.priority,
2359
+ concurrency: spec.concurrency
2360
+ });
2361
+ }
2362
+ __name(startOptions, "startOptions");
2363
+ function describe(record) {
2364
+ const spec = specOf(record);
2365
+ const state = record.state;
2366
+ return stripUndefined({
2367
+ id: record.id,
2368
+ workflow: record.workflow,
2369
+ input: spec.input,
2370
+ namespace: record.namespace,
2371
+ cron: spec.cron,
2372
+ everyMs: spec.everyMs,
2373
+ timezone: spec.timezone,
2374
+ jitterMs: spec.jitterMs,
2375
+ overlap: spec.overlap ?? "allow",
2376
+ catchup: spec.catchup ?? "latest",
2377
+ paused: record.paused,
2378
+ note: spec.note,
2379
+ tags: record.tags,
2380
+ searchAttributes: spec.searchAttributes,
2381
+ priority: spec.priority,
2382
+ concurrency: spec.concurrency,
2383
+ nextFireAt: record.paused || record.nextFireAt == null ? null : new Date(record.nextFireAt),
2384
+ lastFireAt: state.lastFireAt != null ? new Date(state.lastFireAt) : void 0,
2385
+ lastRunId: state.lastRunId,
2386
+ fires: state.fires ?? 0,
2387
+ lastError: state.lastError,
2388
+ createdAt: record.createdAt,
2389
+ updatedAt: record.updatedAt
2390
+ });
2391
+ }
2392
+ __name(describe, "describe");
2393
+ function stripUndefined(o) {
2394
+ return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
2395
+ }
2396
+ __name(stripUndefined, "stripUndefined");
2397
+ function errorMessage(error2) {
2398
+ return error2 instanceof Error ? error2.message : String(error2);
2399
+ }
2400
+ __name(errorMessage, "errorMessage");
2401
+
1829
2402
  // src/singleton-gate.ts
1830
2403
  var SINGLETON_RETRY_MS = 1e3;
1831
2404
  var SINGLETON_RETRY_JITTER_MS = 250;
@@ -2894,6 +3467,12 @@ var WorkflowEngine = class {
2894
3467
  runDispatcher;
2895
3468
  /** Per-key serialization for singleton workflows (admission, back-pressure, notify-on-release). */
2896
3469
  singletons;
3470
+ /**
3471
+ * Persisted, runtime-managed schedules: create/upsert/pause/resume/delete/list/trigger them, and
3472
+ * `tick()` fires the due ones (the NestJS timer poller does it when `persistedSchedules` is on).
3473
+ * Needs a store that persists schedules — every bundled adapter does. See {@link ScheduleClient}.
3474
+ */
3475
+ schedules;
2897
3476
  /** Every registered workflow, keyed by `name@version` — so old versions stay runnable. */
2898
3477
  workflows = /* @__PURE__ */ new Map();
2899
3478
  /** The newest registered version per workflow name — used to `start` new runs. */
@@ -2978,6 +3557,13 @@ var WorkflowEngine = class {
2978
3557
  }, "dispatch"),
2979
3558
  configFor: /* @__PURE__ */ __name((run) => (this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? this.latest.get(run.workflow))?.singleton, "configFor")
2980
3559
  });
3560
+ this.schedules = new ScheduleClient(this.store, {
3561
+ start: /* @__PURE__ */ __name((workflow, input, runId, opts) => this.start(workflow, input, runId, opts), "start"),
3562
+ getRun: /* @__PURE__ */ __name((runId) => this.store.getRun(runId), "getRun")
3563
+ }, {
3564
+ namespace: this.namespace,
3565
+ clock: this.clock
3566
+ });
2981
3567
  this.pool.bind(
2982
3568
  async (result) => {
2983
3569
  const waiter = this.pending.get(result.stepId);
@@ -3113,6 +3699,7 @@ var WorkflowEngine = class {
3113
3699
  fn,
3114
3700
  tags: opts?.tags,
3115
3701
  singleton: opts?.singleton,
3702
+ concurrency: opts?.concurrency,
3116
3703
  executionTimeoutMs: opts?.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
3117
3704
  validateInput: opts?.validateInput,
3118
3705
  searchAttributesSchema: opts?.searchAttributesSchema,
@@ -3175,6 +3762,7 @@ var WorkflowEngine = class {
3175
3762
  }, "fn"),
3176
3763
  tags: opts.tags,
3177
3764
  singleton: opts.singleton,
3765
+ concurrency: opts.concurrency,
3178
3766
  executionTimeoutMs: opts.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
3179
3767
  validateInput: opts.validateInput,
3180
3768
  requires: opts.requires,
@@ -3346,12 +3934,17 @@ var WorkflowEngine = class {
3346
3934
  error: prior.error
3347
3935
  };
3348
3936
  }
3937
+ const quota = await resolveConcurrency(opts?.concurrency, registered.concurrency, input);
3938
+ if (quota) await assertConcurrency(this.store, name, quota);
3349
3939
  const now = /* @__PURE__ */ new Date();
3350
3940
  const stampedTags = [
3351
3941
  ...opts?.tags ?? [],
3352
3942
  ...registered.singleton ? [
3353
3943
  this.singletons.tag(registered.singleton, input)
3354
3944
  ] : [],
3945
+ ...quota ? [
3946
+ concurrencyTag(quota.key)
3947
+ ] : [],
3355
3948
  ...registered.versionDeclared === false ? [
3356
3949
  VERSION_UNDECLARED_TAG
3357
3950
  ] : []
@@ -4389,6 +4982,123 @@ var WorkflowEngine = class {
4389
4982
  return deleted + 1;
4390
4983
  }
4391
4984
  /**
4985
+ * Hard-delete EVERY run matching `scope` — plus, by default, each one's whole child subtree — in
4986
+ * bounded batches. The "forget this tenant" / "drop this feature's history" operation: where
4987
+ * {@link deleteRun} removes one tree you already know the id of, this drains everything a scope
4988
+ * selects (`{ namespace: 'acme' }`, `{ tag: 'tenant:acme' }`, `{ workflows: [...] }`, attribute
4989
+ * predicates …) through the store API, so no caller has to reach into the `durable_*` tables.
4990
+ *
4991
+ * - **Live runs** (not yet terminal) are {@link cancel cancelled} first (plain cancel, no saga
4992
+ * undo), which tells any worker holding one to stop, and then deleted with the rest. Pass
4993
+ * `cancelLive: false` to leave live runs (and live descendants of finished ones) alone and
4994
+ * delete terminal history only.
4995
+ * - **Children** are found through the same parent→child edges as {@link getRunChildren} and deleted
4996
+ * with their root even when they don't match `scope` themselves (children inherit `namespace`,
4997
+ * but not `tags`). `children: false` deletes exactly the matching runs.
4998
+ * - **Batched**: `batchSize` runs (default 500) per round, each round one bulk
4999
+ * {@link StateStore.deleteRuns} (falling back to per-run {@link StateStore.deleteRun}).
5000
+ *
5001
+ * An EMPTY scope is rejected — it would delete every run in the store. Returns how many runs were
5002
+ * deleted (children included).
5003
+ */
5004
+ async purgeRuns(scope, opts) {
5005
+ if (!isNonEmptyRunScope(scope)) {
5006
+ throw new Error("purgeRuns: refusing an empty scope (it would delete every run) \u2014 pass at least one predicate, e.g. { namespace }");
5007
+ }
5008
+ const batchSize = Math.max(1, opts?.batchSize ?? 500);
5009
+ const cancelLive = opts?.cancelLive ?? true;
5010
+ const withChildren = opts?.children ?? true;
5011
+ const statuses = cancelLive ? void 0 : [
5012
+ ...TERMINAL_RUN_STATUSES
5013
+ ];
5014
+ let deleted = 0;
5015
+ let previous = "";
5016
+ for (; ; ) {
5017
+ const batch = await this.store.listRuns({
5018
+ ...scope,
5019
+ statuses,
5020
+ limit: batchSize
5021
+ });
5022
+ if (batch.length === 0) break;
5023
+ const fingerprint = batch.map((r) => r.id).join("\0");
5024
+ if (fingerprint === previous) {
5025
+ throw new Error("purgeRuns: the store returned the same runs after deleting them");
5026
+ }
5027
+ previous = fingerprint;
5028
+ const known = new Map(batch.map((r) => [
5029
+ r.id,
5030
+ r.status
5031
+ ]));
5032
+ const ids = withChildren ? await this.collectSubtrees(batch.map((r) => r.id)) : batch.map((r) => r.id);
5033
+ const doomed = [];
5034
+ for (const id of ids) {
5035
+ const status = known.has(id) ? known.get(id) : (await this.store.getRun(id))?.status;
5036
+ if (status === void 0) continue;
5037
+ if (!TERMINAL_RUN_STATUSES.includes(status)) {
5038
+ if (!cancelLive) continue;
5039
+ await this.cancel(id).catch(() => void 0);
5040
+ }
5041
+ doomed.push(id);
5042
+ }
5043
+ await this.deleteRunIds(doomed);
5044
+ deleted += doomed.length;
5045
+ if (batch.length < batchSize) break;
5046
+ }
5047
+ return deleted;
5048
+ }
5049
+ /**
5050
+ * {@link purgeRuns} for one namespace — every run (and child) a tenant/partition owns. Children
5051
+ * inherit their parent's namespace, so a namespace purge is complete by construction.
5052
+ */
5053
+ purgeNamespace(namespace, opts) {
5054
+ return this.purgeRuns({
5055
+ namespace
5056
+ }, opts);
5057
+ }
5058
+ /** The given runs plus every descendant, children before parents. One waiter scan for the batch. */
5059
+ async collectSubtrees(rootIds) {
5060
+ const waitersByParent = /* @__PURE__ */ new Map();
5061
+ for (const w of await this.store.listSignalWaiters("child:")) {
5062
+ const list = waitersByParent.get(w.runId) ?? [];
5063
+ list.push(w.token.slice("child:".length));
5064
+ waitersByParent.set(w.runId, list);
5065
+ }
5066
+ const prefixes = [
5067
+ "signal:child:",
5068
+ "spawn:"
5069
+ ];
5070
+ const childrenOf = /* @__PURE__ */ __name(async (parent) => {
5071
+ const ids = new Set(waitersByParent.get(parent) ?? []);
5072
+ 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)));
5073
+ for (const cp of cps) {
5074
+ if (cp.name.startsWith("signal:child:")) ids.add(cp.name.slice("signal:child:".length));
5075
+ if (cp.name.startsWith("spawn:") && typeof cp.output === "string") ids.add(cp.output);
5076
+ }
5077
+ return [
5078
+ ...ids
5079
+ ];
5080
+ }, "childrenOf");
5081
+ const ordered = [];
5082
+ const seen = /* @__PURE__ */ new Set();
5083
+ const visit = /* @__PURE__ */ __name(async (id) => {
5084
+ if (seen.has(id)) return;
5085
+ seen.add(id);
5086
+ for (const child of await childrenOf(id)) await visit(child);
5087
+ ordered.push(id);
5088
+ }, "visit");
5089
+ for (const id of rootIds) await visit(id);
5090
+ return ordered;
5091
+ }
5092
+ /** Bulk-delete through the store's `deleteRuns`, or one `deleteRun` per id when it has none. */
5093
+ async deleteRunIds(ids) {
5094
+ if (ids.length === 0) return;
5095
+ if (this.store.deleteRuns) {
5096
+ await this.store.deleteRuns(ids);
5097
+ return;
5098
+ }
5099
+ for (const id of ids) await this.store.deleteRun(id);
5100
+ }
5101
+ /**
4392
5102
  * Cascade cancellation to a run's children — both awaited (`ctx.child`, found via its live
4393
5103
  * `child:<id>` waiter) and fire-and-forget (`ctx.startChild`, found via its `spawn:<id>`
4394
5104
  * checkpoint). Recursive, so a whole subtree is cancelled; the terminal guard in `cancel` stops it
@@ -6409,14 +7119,6 @@ var RemoteStepError = class extends Error {
6409
7119
  }
6410
7120
  };
6411
7121
 
6412
- // src/interfaces.ts
6413
- var TERMINAL_RUN_STATUSES = [
6414
- "completed",
6415
- "failed",
6416
- "cancelled",
6417
- "dead"
6418
- ];
6419
-
6420
7122
  // src/run-facets.ts
6421
7123
  function facetOrigin(origin) {
6422
7124
  return origin?.trim() ? origin : null;
@@ -6448,7 +7150,8 @@ __name(mergeRunFacetRows, "mergeRunFacetRows");
6448
7150
  var RUN_VALUE_FACET_LIMIT = 100;
6449
7151
  var RUN_VALUE_FACET_SCAN = 5e3;
6450
7152
  var ENGINE_MINTED_TAG_PREFIXES = [
6451
- "singleton:"
7153
+ "singleton:",
7154
+ "concurrency:"
6452
7155
  ];
6453
7156
  function isEngineMintedTag(value) {
6454
7157
  return value !== null && ENGINE_MINTED_TAG_PREFIXES.some((prefix) => value.startsWith(prefix));
@@ -6553,9 +7256,42 @@ var CodecStateStore = class {
6553
7256
  * Bound the same conditional way as {@link runFacets} so absence is reported, not faked.
6554
7257
  */
6555
7258
  runValueFacets;
7259
+ /** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
7260
+ * engine's per-run fallback still kicks in through the wrapper. */
7261
+ deleteRuns;
7262
+ /** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
7263
+ * when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
7264
+ pruneTerminalRuns;
7265
+ /** Forwarded verbatim — a count reads no payload. Bound only when the inner store counts, so the
7266
+ * engine's facet/listing fallback still applies through the wrapper. */
7267
+ countRuns;
7268
+ /** Schedules persist their run `input` inside `spec` — encoded like any other payload. Bound only
7269
+ * when the inner store persists schedules, so `engine.schedules` reports absence faithfully. */
7270
+ saveSchedule;
7271
+ getSchedule;
7272
+ updateSchedule;
7273
+ deleteSchedule;
7274
+ listSchedules;
6556
7275
  constructor(inner, codec) {
6557
7276
  this.inner = inner;
6558
7277
  this.codec = codec;
7278
+ const deleteRuns = inner.deleteRuns;
7279
+ if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
7280
+ const prune = inner.pruneTerminalRuns;
7281
+ if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
7282
+ const count = inner.countRuns;
7283
+ if (count) this.countRuns = (query) => count.call(inner, query);
7284
+ const { saveSchedule, getSchedule, updateSchedule, deleteSchedule, listSchedules } = inner;
7285
+ if (saveSchedule && getSchedule && updateSchedule && deleteSchedule && listSchedules) {
7286
+ this.saveSchedule = (r) => saveSchedule.call(inner, this.encSchedule(r));
7287
+ this.getSchedule = async (id) => this.decSchedule(await getSchedule.call(inner, id));
7288
+ this.updateSchedule = (id, patch, expected) => updateSchedule.call(inner, id, patch.spec ? {
7289
+ ...patch,
7290
+ spec: this.encSpec(patch.spec)
7291
+ } : patch, expected);
7292
+ this.deleteSchedule = (id) => deleteSchedule.call(inner, id);
7293
+ this.listSchedules = async (q) => (await listSchedules.call(inner, q)).map((r) => this.decSchedule(r));
7294
+ }
6559
7295
  const facets = inner.runFacets;
6560
7296
  if (facets) this.runFacets = (query) => facets.call(inner, query);
6561
7297
  const valueFacets = inner.runValueFacets;
@@ -6597,6 +7333,28 @@ var CodecStateStore = class {
6597
7333
  output: this.dec(cp.output)
6598
7334
  };
6599
7335
  }
7336
+ encSpec(spec) {
7337
+ return "input" in spec ? {
7338
+ ...spec,
7339
+ input: this.enc(spec.input)
7340
+ } : spec;
7341
+ }
7342
+ encSchedule(r) {
7343
+ return {
7344
+ ...r,
7345
+ spec: this.encSpec(r.spec)
7346
+ };
7347
+ }
7348
+ decSchedule(r) {
7349
+ if (!r || !("input" in r.spec)) return r;
7350
+ return {
7351
+ ...r,
7352
+ spec: {
7353
+ ...r.spec,
7354
+ input: this.dec(r.spec.input)
7355
+ }
7356
+ };
7357
+ }
6600
7358
  ensureSchema() {
6601
7359
  return this.inner.ensureSchema?.() ?? Promise.resolve();
6602
7360
  }
@@ -6795,126 +7553,6 @@ function collectMetrics(engine) {
6795
7553
  }
6796
7554
  __name(collectMetrics, "collectMetrics");
6797
7555
 
6798
- // src/scheduler.ts
6799
- var import_node_module = require("module");
6800
-
6801
- // src/cron-compat.ts
6802
- function isIndexable(value) {
6803
- return typeof value === "object" && value !== null || typeof value === "function";
6804
- }
6805
- __name(isIndexable, "isIndexable");
6806
- function unwrapDefault(module_) {
6807
- if (!isIndexable(module_)) return module_;
6808
- if (Reflect.get(module_, "parseExpression") !== void 0) return module_;
6809
- if (Reflect.get(module_, "CronExpressionParser") !== void 0) return module_;
6810
- const inner = Reflect.get(module_, "default");
6811
- return inner === void 0 ? module_ : inner;
6812
- }
6813
- __name(unwrapDefault, "unwrapDefault");
6814
- function v4Entry(module_) {
6815
- if (!isIndexable(module_)) return void 0;
6816
- const parse = Reflect.get(module_, "parseExpression");
6817
- if (typeof parse !== "function") return void 0;
6818
- return (expr, options) => parse.call(module_, expr, options);
6819
- }
6820
- __name(v4Entry, "v4Entry");
6821
- function v5Entry(module_) {
6822
- if (!isIndexable(module_)) return void 0;
6823
- const parser = Reflect.get(module_, "CronExpressionParser");
6824
- if (!isIndexable(parser)) return void 0;
6825
- const parse = Reflect.get(parser, "parse");
6826
- if (typeof parse !== "function") return void 0;
6827
- return (expr, options) => parse.call(parser, expr, options);
6828
- }
6829
- __name(v5Entry, "v5Entry");
6830
- function resolveCronParse(module_) {
6831
- const resolved = unwrapDefault(module_);
6832
- return v4Entry(resolved) ?? v5Entry(resolved);
6833
- }
6834
- __name(resolveCronParse, "resolveCronParse");
6835
-
6836
- // src/scheduler.ts
6837
- var nodeRequire = (0, import_node_module.createRequire)(__importMetaUrl);
6838
- var defaultSleep = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "defaultSleep");
6839
- function scheduledRunId(key, everyMs, nowMs) {
6840
- return `sched:${key}:${Math.floor(nowMs / everyMs)}`;
6841
- }
6842
- __name(scheduledRunId, "scheduledRunId");
6843
- var cronParser;
6844
- function loadCronParser() {
6845
- if (cronParser !== void 0) return cronParser;
6846
- try {
6847
- cronParser = nodeRequire("cron-parser");
6848
- } catch {
6849
- throw new Error('cron schedules need the optional peer dependency "cron-parser" \u2014 install it (e.g. `npm i cron-parser`).');
6850
- }
6851
- return cronParser;
6852
- }
6853
- __name(loadCronParser, "loadCronParser");
6854
- function prevCronFireMs(expr, nowMs, timezone = "UTC") {
6855
- const parse = resolveCronParse(loadCronParser());
6856
- if (!parse) {
6857
- 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.`);
6858
- }
6859
- return parse(expr, {
6860
- currentDate: new Date(nowMs + 1),
6861
- tz: timezone
6862
- }).prev().toDate().getTime();
6863
- }
6864
- __name(prevCronFireMs, "prevCronFireMs");
6865
- function scheduleRunIdAt(s, nowMs) {
6866
- if (s.cron != null) return `sched:${s.key}:${prevCronFireMs(s.cron, nowMs, s.timezone)}`;
6867
- if (s.everyMs != null) return scheduledRunId(s.key, s.everyMs, nowMs);
6868
- throw new Error(`schedule "${s.key}" needs either "everyMs" or "cron"`);
6869
- }
6870
- __name(scheduleRunIdAt, "scheduleRunIdAt");
6871
- function scheduleRunIdsAt(s, nowMs) {
6872
- const current = scheduleRunIdAt(s, nowMs);
6873
- if (!s.backfill || s.backfill.maxCatchup <= 0) return [
6874
- current
6875
- ];
6876
- const ids = [];
6877
- if (s.everyMs != null) {
6878
- const bucket = Math.floor(nowMs / s.everyMs);
6879
- const oldest = Math.max(0, bucket - s.backfill.maxCatchup);
6880
- for (let b = oldest; b < bucket; b += 1) ids.push(`sched:${s.key}:${b}`);
6881
- } else if (s.cron != null) {
6882
- const prior = [];
6883
- let cursor = prevCronFireMs(s.cron, nowMs, s.timezone);
6884
- for (let i = 0; i < s.backfill.maxCatchup; i += 1) {
6885
- const before = prevCronFireMs(s.cron, cursor - 1, s.timezone);
6886
- prior.push(`sched:${s.key}:${before}`);
6887
- cursor = before;
6888
- }
6889
- ids.push(...prior.reverse());
6890
- }
6891
- ids.push(current);
6892
- return ids;
6893
- }
6894
- __name(scheduleRunIdsAt, "scheduleRunIdsAt");
6895
- async function runSchedules(engine, schedules, nowMs, opts) {
6896
- const random = opts?.random ?? Math.random;
6897
- const sleep = opts?.sleep ?? defaultSleep;
6898
- const ids = [];
6899
- for (const s of schedules) {
6900
- if (s.paused) continue;
6901
- if (s.overlap === "skip" && s.everyMs) {
6902
- const prevBucket = Math.floor(nowMs / s.everyMs) - 1;
6903
- if (prevBucket >= 0) {
6904
- const prev = await engine.getRun(`sched:${s.key}:${prevBucket}`);
6905
- if (prev && (prev.status === "running" || prev.status === "suspended")) continue;
6906
- }
6907
- }
6908
- if (s.jitter && s.jitter > 0) await sleep(Math.floor(random() * s.jitter));
6909
- for (const runId of scheduleRunIdsAt(s, nowMs)) {
6910
- await engine.start(s.workflow, s.input, runId);
6911
- ids.push(runId);
6912
- }
6913
- }
6914
- return ids;
6915
- }
6916
- __name(runSchedules, "runSchedules");
6917
-
6918
7556
  // src/search-attributes.ts
6919
7557
  function compare(actual, filter) {
6920
7558
  if (filter.op === "in") return filter.values.some((v) => actual === v);
@@ -7122,6 +7760,38 @@ var InMemoryStateStore = class {
7122
7760
  }
7123
7761
  this.reindexAttributes(runId, void 0);
7124
7762
  }
7763
+ async deleteRuns(runIds) {
7764
+ for (const runId of runIds) await this.deleteRun(runId);
7765
+ }
7766
+ async pruneTerminalRuns(policy, nowMs, limit) {
7767
+ const statuses = policy.statuses.filter((s) => TERMINAL_RUN_STATUSES.includes(s));
7768
+ if (statuses.length === 0 || limit <= 0) return 0;
7769
+ if (policy.maxAge == null && policy.maxCount == null) return 0;
7770
+ const inScope = this.matching({
7771
+ ...policy.scope ?? {},
7772
+ statuses
7773
+ });
7774
+ const ids = /* @__PURE__ */ new Set();
7775
+ if (policy.maxAge != null) {
7776
+ const cutoff = nowMs - parseDuration(policy.maxAge);
7777
+ const expired = inScope.filter((r) => r.updatedAt.getTime() < cutoff).sort((a, b) => a.updatedAt.getTime() - b.updatedAt.getTime()).slice(0, limit);
7778
+ for (const r of expired) ids.add(r.id);
7779
+ }
7780
+ if (policy.maxCount != null && ids.size < limit) {
7781
+ const byRecency = [
7782
+ ...inScope
7783
+ ].sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime() || b.id.localeCompare(a.id));
7784
+ for (const r of byRecency.slice(policy.maxCount)) {
7785
+ if (ids.size >= limit) break;
7786
+ ids.add(r.id);
7787
+ }
7788
+ }
7789
+ const doomed = [
7790
+ ...ids
7791
+ ].slice(0, limit);
7792
+ await this.deleteRuns(doomed);
7793
+ return doomed.length;
7794
+ }
7125
7795
  async getCheckpoint(runId, seq) {
7126
7796
  const cp = this.checkpoints.get(this.key(runId, seq));
7127
7797
  return cp ? {
@@ -7329,6 +7999,9 @@ var InMemoryStateStore = class {
7329
7999
  ...r
7330
8000
  }));
7331
8001
  }
8002
+ async countRuns(query) {
8003
+ return this.matching(query).length;
8004
+ }
7332
8005
  async runFacets(query) {
7333
8006
  return mergeRunFacetRows(this.matching(query).map((run) => ({
7334
8007
  status: run.status,
@@ -7346,6 +8019,34 @@ var InMemoryStateStore = class {
7346
8019
  });
7347
8020
  return runValueFacetsFromRuns(runs, axis, opts);
7348
8021
  }
8022
+ schedules = /* @__PURE__ */ new Map();
8023
+ async saveSchedule(record) {
8024
+ this.schedules.set(record.id, structuredClone(record));
8025
+ }
8026
+ async getSchedule(id) {
8027
+ const record = this.schedules.get(id);
8028
+ return record ? structuredClone(record) : null;
8029
+ }
8030
+ async updateSchedule(id, patch, expectedNextFireAt) {
8031
+ const record = this.schedules.get(id);
8032
+ if (!record) return false;
8033
+ if (expectedNextFireAt !== void 0 && record.nextFireAt !== expectedNextFireAt) return false;
8034
+ this.schedules.set(id, {
8035
+ ...record,
8036
+ ...structuredClone(patch)
8037
+ });
8038
+ return true;
8039
+ }
8040
+ async deleteSchedule(id) {
8041
+ return this.schedules.delete(id);
8042
+ }
8043
+ async listSchedules(query) {
8044
+ const rows = [
8045
+ ...this.schedules.values()
8046
+ ].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));
8047
+ const offset = query.offset ?? 0;
8048
+ return rows.slice(offset, query.limit != null ? offset + query.limit : void 0).map((s) => structuredClone(s));
8049
+ }
7349
8050
  async listCheckpoints(runId) {
7350
8051
  const rows = [
7351
8052
  ...this.checkpoints.values()
@@ -7427,7 +8128,9 @@ var InMemoryTransport = class {
7427
8128
  CONTROL_FLOW_SIGNAL,
7428
8129
  CURRENT_PROTOCOL_VERSION,
7429
8130
  CodecStateStore,
8131
+ ConcurrencyLimitError,
7430
8132
  ContinueAsNew,
8133
+ DEFAULT_CONCURRENCY_STATUSES,
7431
8134
  DEFAULT_PARTITION,
7432
8135
  DURABLE_OPTIONS,
7433
8136
  DURABLE_OPTIONS_CANONICAL,
@@ -7457,6 +8160,9 @@ var InMemoryTransport = class {
7457
8160
  RunGateway,
7458
8161
  STATE_STORE,
7459
8162
  STATE_STORE_CANONICAL,
8163
+ ScheduleAlreadyExistsError,
8164
+ ScheduleClient,
8165
+ ScheduleNotFoundError,
7460
8166
  SignalTimeoutError,
7461
8167
  SingletonQueueFullError,
7462
8168
  TERMINAL_RUN_STATUSES,
@@ -7470,6 +8176,7 @@ var InMemoryTransport = class {
7470
8176
  aggregateAnnouncements,
7471
8177
  announcementsOf,
7472
8178
  applyAttributeQuery,
8179
+ assertConcurrency,
7473
8180
  attributeColumnFor,
7474
8181
  attributeOperand,
7475
8182
  attributePredicateOperands,
@@ -7482,7 +8189,9 @@ var InMemoryTransport = class {
7482
8189
  classifyWaiterToken,
7483
8190
  clearDurableExecutionWrappers,
7484
8191
  collectMetrics,
8192
+ concurrencyTag,
7485
8193
  controlPlaneDescriptor,
8194
+ countRuns,
7486
8195
  coveredTokens,
7487
8196
  createStepLogger,
7488
8197
  currentStep,
@@ -7504,6 +8213,7 @@ var InMemoryTransport = class {
7504
8213
  isEngineMintedTag,
7505
8214
  isEventToken,
7506
8215
  isLegacyDescriptor,
8216
+ isNonEmptyRunScope,
7507
8217
  isWorkflowControlFlowSignal,
7508
8218
  log,
7509
8219
  matchesAttributes,
@@ -7511,6 +8221,7 @@ var InMemoryTransport = class {
7511
8221
  mergeRunValueFacetRows,
7512
8222
  missingCapabilities,
7513
8223
  negotiate,
8224
+ nextCronFireMs,
7514
8225
  normalizeAttributeRows,
7515
8226
  normalizeDescriptor,
7516
8227
  observedAnnouncements,
@@ -7520,6 +8231,7 @@ var InMemoryTransport = class {
7520
8231
  prevCronFireMs,
7521
8232
  readSearchAttributes,
7522
8233
  requiredCapabilities,
8234
+ resolveConcurrency,
7523
8235
  resolveRouting,
7524
8236
  resolveRunWaiting,
7525
8237
  runDurableExecution,