@dudousxd/nestjs-durable-core 0.71.0 → 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
  ] : []
@@ -3657,9 +4250,14 @@ var WorkflowEngine = class {
3657
4250
  async recoverIncomplete(nowMs = this.clock()) {
3658
4251
  if (this.draining) return [];
3659
4252
  const results = [];
3660
- for (const run of await this.store.listIncompleteRuns(this.namespace)) {
3661
- const acquired = await this.store.tryLockRun(run.id, this.instanceId, nowMs + this.leaseMs, nowMs);
4253
+ for (const listed of await this.store.listIncompleteRuns(this.namespace)) {
4254
+ const acquired = await this.store.tryLockRun(listed.id, this.instanceId, nowMs + this.leaseMs, nowMs);
3662
4255
  if (!acquired) continue;
4256
+ const run = await this.store.getRun(listed.id);
4257
+ if (!run || run.status !== "running" && run.status !== "cancelling") {
4258
+ await this.store.releaseRunLock(listed.id);
4259
+ continue;
4260
+ }
3663
4261
  if (run.status === "running") {
3664
4262
  const dispatched = await this.inFlightRemoteStep(run.id);
3665
4263
  if (dispatched) {
@@ -4384,6 +4982,123 @@ var WorkflowEngine = class {
4384
4982
  return deleted + 1;
4385
4983
  }
4386
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
+ /**
4387
5102
  * Cascade cancellation to a run's children — both awaited (`ctx.child`, found via its live
4388
5103
  * `child:<id>` waiter) and fire-and-forget (`ctx.startChild`, found via its `spawn:<id>`
4389
5104
  * checkpoint). Recursive, so a whole subtree is cancelled; the terminal guard in `cancel` stops it
@@ -5519,16 +6234,16 @@ var WorkflowEngine = class {
5519
6234
  try {
5520
6235
  const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
5521
6236
  if (this.cancelRequested.has(run.id)) {
5522
- return this.compensateAndCancel(run, compensations, replay);
6237
+ return await this.compensateAndCancel(run, compensations, replay);
5523
6238
  }
5524
- return this.settleRun(run, {
6239
+ return await this.settleRun(run, {
5525
6240
  kind: "completed",
5526
6241
  output
5527
6242
  });
5528
6243
  } catch (err) {
5529
6244
  if (err instanceof ContinueAsNew) {
5530
6245
  if (this.cancelRequested.has(run.id)) {
5531
- return this.compensateAndCancel(run, compensations, replay);
6246
+ return await this.compensateAndCancel(run, compensations, replay);
5532
6247
  }
5533
6248
  await this.store.updateRun(run.id, {
5534
6249
  status: "completed",
@@ -5555,35 +6270,35 @@ var WorkflowEngine = class {
5555
6270
  }
5556
6271
  if (err instanceof WorkflowBlocked) {
5557
6272
  if (this.cancelRequested.has(run.id)) {
5558
- return this.compensateAndCancel(run, compensations, replay);
6273
+ return await this.compensateAndCancel(run, compensations, replay);
5559
6274
  }
5560
- return this.settleRun(run, {
6275
+ return await this.settleRun(run, {
5561
6276
  kind: "blocked",
5562
6277
  blocked: err.plan
5563
6278
  });
5564
6279
  }
5565
6280
  if (err instanceof WorkflowSuspended) {
5566
6281
  if (this.cancelRequested.has(run.id)) {
5567
- return this.compensateAndCancel(run, compensations, replay);
6282
+ return await this.compensateAndCancel(run, compensations, replay);
5568
6283
  }
5569
- return this.settleRun(run, {
6284
+ return await this.settleRun(run, {
5570
6285
  kind: "suspended",
5571
6286
  wakeAt: err.wakeAt
5572
6287
  });
5573
6288
  }
5574
6289
  if (this.cancelRequested.has(run.id)) {
5575
- return this.compensateAndCancel(run, compensations, replay);
6290
+ return await this.compensateAndCancel(run, compensations, replay);
5576
6291
  }
5577
6292
  const error2 = {
5578
6293
  message: err instanceof Error ? err.message : String(err),
5579
6294
  stack: err instanceof Error ? err.stack : void 0
5580
6295
  };
5581
6296
  const pending = await this.unwindCompensations(run, compensations, replay);
5582
- if (pending) return this.settleRun(run, {
6297
+ if (pending) return await this.settleRun(run, {
5583
6298
  kind: "suspended",
5584
6299
  wakeAt: pending.wakeAt
5585
6300
  });
5586
- return this.settleRun(run, {
6301
+ return await this.settleRun(run, {
5587
6302
  kind: "failed",
5588
6303
  error: error2
5589
6304
  });
@@ -5997,7 +6712,10 @@ var WorkflowEngine = class {
5997
6712
  }
5998
6713
  async callRemote(runId, seq, step, input, queue, transport, replay, admission, routing) {
5999
6714
  const group = stepGroup(step, routing?.namespace);
6000
- const existing = replay?.get(seq) ?? await this.store.getCheckpoint(runId, seq);
6715
+ let existing = replay?.get(seq) ?? await this.store.getCheckpoint(runId, seq);
6716
+ if (existing?.status === "pending" && replay?.has(seq)) {
6717
+ existing = await this.store.getCheckpoint(runId, seq) ?? existing;
6718
+ }
6001
6719
  if (existing && existing.name !== step.name) {
6002
6720
  throw new NonDeterminismError(runId, seq, step.name, existing.name);
6003
6721
  }
@@ -6401,14 +7119,6 @@ var RemoteStepError = class extends Error {
6401
7119
  }
6402
7120
  };
6403
7121
 
6404
- // src/interfaces.ts
6405
- var TERMINAL_RUN_STATUSES = [
6406
- "completed",
6407
- "failed",
6408
- "cancelled",
6409
- "dead"
6410
- ];
6411
-
6412
7122
  // src/run-facets.ts
6413
7123
  function facetOrigin(origin) {
6414
7124
  return origin?.trim() ? origin : null;
@@ -6440,7 +7150,8 @@ __name(mergeRunFacetRows, "mergeRunFacetRows");
6440
7150
  var RUN_VALUE_FACET_LIMIT = 100;
6441
7151
  var RUN_VALUE_FACET_SCAN = 5e3;
6442
7152
  var ENGINE_MINTED_TAG_PREFIXES = [
6443
- "singleton:"
7153
+ "singleton:",
7154
+ "concurrency:"
6444
7155
  ];
6445
7156
  function isEngineMintedTag(value) {
6446
7157
  return value !== null && ENGINE_MINTED_TAG_PREFIXES.some((prefix) => value.startsWith(prefix));
@@ -6545,9 +7256,42 @@ var CodecStateStore = class {
6545
7256
  * Bound the same conditional way as {@link runFacets} so absence is reported, not faked.
6546
7257
  */
6547
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;
6548
7275
  constructor(inner, codec) {
6549
7276
  this.inner = inner;
6550
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
+ }
6551
7295
  const facets = inner.runFacets;
6552
7296
  if (facets) this.runFacets = (query) => facets.call(inner, query);
6553
7297
  const valueFacets = inner.runValueFacets;
@@ -6589,6 +7333,28 @@ var CodecStateStore = class {
6589
7333
  output: this.dec(cp.output)
6590
7334
  };
6591
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
+ }
6592
7358
  ensureSchema() {
6593
7359
  return this.inner.ensureSchema?.() ?? Promise.resolve();
6594
7360
  }
@@ -6787,126 +7553,6 @@ function collectMetrics(engine) {
6787
7553
  }
6788
7554
  __name(collectMetrics, "collectMetrics");
6789
7555
 
6790
- // src/scheduler.ts
6791
- var import_node_module = require("module");
6792
-
6793
- // src/cron-compat.ts
6794
- function isIndexable(value) {
6795
- return typeof value === "object" && value !== null || typeof value === "function";
6796
- }
6797
- __name(isIndexable, "isIndexable");
6798
- function unwrapDefault(module_) {
6799
- if (!isIndexable(module_)) return module_;
6800
- if (Reflect.get(module_, "parseExpression") !== void 0) return module_;
6801
- if (Reflect.get(module_, "CronExpressionParser") !== void 0) return module_;
6802
- const inner = Reflect.get(module_, "default");
6803
- return inner === void 0 ? module_ : inner;
6804
- }
6805
- __name(unwrapDefault, "unwrapDefault");
6806
- function v4Entry(module_) {
6807
- if (!isIndexable(module_)) return void 0;
6808
- const parse = Reflect.get(module_, "parseExpression");
6809
- if (typeof parse !== "function") return void 0;
6810
- return (expr, options) => parse.call(module_, expr, options);
6811
- }
6812
- __name(v4Entry, "v4Entry");
6813
- function v5Entry(module_) {
6814
- if (!isIndexable(module_)) return void 0;
6815
- const parser = Reflect.get(module_, "CronExpressionParser");
6816
- if (!isIndexable(parser)) return void 0;
6817
- const parse = Reflect.get(parser, "parse");
6818
- if (typeof parse !== "function") return void 0;
6819
- return (expr, options) => parse.call(parser, expr, options);
6820
- }
6821
- __name(v5Entry, "v5Entry");
6822
- function resolveCronParse(module_) {
6823
- const resolved = unwrapDefault(module_);
6824
- return v4Entry(resolved) ?? v5Entry(resolved);
6825
- }
6826
- __name(resolveCronParse, "resolveCronParse");
6827
-
6828
- // src/scheduler.ts
6829
- var nodeRequire = (0, import_node_module.createRequire)(__importMetaUrl);
6830
- var defaultSleep = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "defaultSleep");
6831
- function scheduledRunId(key, everyMs, nowMs) {
6832
- return `sched:${key}:${Math.floor(nowMs / everyMs)}`;
6833
- }
6834
- __name(scheduledRunId, "scheduledRunId");
6835
- var cronParser;
6836
- function loadCronParser() {
6837
- if (cronParser !== void 0) return cronParser;
6838
- try {
6839
- cronParser = nodeRequire("cron-parser");
6840
- } catch {
6841
- throw new Error('cron schedules need the optional peer dependency "cron-parser" \u2014 install it (e.g. `npm i cron-parser`).');
6842
- }
6843
- return cronParser;
6844
- }
6845
- __name(loadCronParser, "loadCronParser");
6846
- function prevCronFireMs(expr, nowMs, timezone = "UTC") {
6847
- const parse = resolveCronParse(loadCronParser());
6848
- if (!parse) {
6849
- 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.`);
6850
- }
6851
- return parse(expr, {
6852
- currentDate: new Date(nowMs + 1),
6853
- tz: timezone
6854
- }).prev().toDate().getTime();
6855
- }
6856
- __name(prevCronFireMs, "prevCronFireMs");
6857
- function scheduleRunIdAt(s, nowMs) {
6858
- if (s.cron != null) return `sched:${s.key}:${prevCronFireMs(s.cron, nowMs, s.timezone)}`;
6859
- if (s.everyMs != null) return scheduledRunId(s.key, s.everyMs, nowMs);
6860
- throw new Error(`schedule "${s.key}" needs either "everyMs" or "cron"`);
6861
- }
6862
- __name(scheduleRunIdAt, "scheduleRunIdAt");
6863
- function scheduleRunIdsAt(s, nowMs) {
6864
- const current = scheduleRunIdAt(s, nowMs);
6865
- if (!s.backfill || s.backfill.maxCatchup <= 0) return [
6866
- current
6867
- ];
6868
- const ids = [];
6869
- if (s.everyMs != null) {
6870
- const bucket = Math.floor(nowMs / s.everyMs);
6871
- const oldest = Math.max(0, bucket - s.backfill.maxCatchup);
6872
- for (let b = oldest; b < bucket; b += 1) ids.push(`sched:${s.key}:${b}`);
6873
- } else if (s.cron != null) {
6874
- const prior = [];
6875
- let cursor = prevCronFireMs(s.cron, nowMs, s.timezone);
6876
- for (let i = 0; i < s.backfill.maxCatchup; i += 1) {
6877
- const before = prevCronFireMs(s.cron, cursor - 1, s.timezone);
6878
- prior.push(`sched:${s.key}:${before}`);
6879
- cursor = before;
6880
- }
6881
- ids.push(...prior.reverse());
6882
- }
6883
- ids.push(current);
6884
- return ids;
6885
- }
6886
- __name(scheduleRunIdsAt, "scheduleRunIdsAt");
6887
- async function runSchedules(engine, schedules, nowMs, opts) {
6888
- const random = opts?.random ?? Math.random;
6889
- const sleep = opts?.sleep ?? defaultSleep;
6890
- const ids = [];
6891
- for (const s of schedules) {
6892
- if (s.paused) continue;
6893
- if (s.overlap === "skip" && s.everyMs) {
6894
- const prevBucket = Math.floor(nowMs / s.everyMs) - 1;
6895
- if (prevBucket >= 0) {
6896
- const prev = await engine.getRun(`sched:${s.key}:${prevBucket}`);
6897
- if (prev && (prev.status === "running" || prev.status === "suspended")) continue;
6898
- }
6899
- }
6900
- if (s.jitter && s.jitter > 0) await sleep(Math.floor(random() * s.jitter));
6901
- for (const runId of scheduleRunIdsAt(s, nowMs)) {
6902
- await engine.start(s.workflow, s.input, runId);
6903
- ids.push(runId);
6904
- }
6905
- }
6906
- return ids;
6907
- }
6908
- __name(runSchedules, "runSchedules");
6909
-
6910
7556
  // src/search-attributes.ts
6911
7557
  function compare(actual, filter) {
6912
7558
  if (filter.op === "in") return filter.values.some((v) => actual === v);
@@ -7114,6 +7760,38 @@ var InMemoryStateStore = class {
7114
7760
  }
7115
7761
  this.reindexAttributes(runId, void 0);
7116
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
+ }
7117
7795
  async getCheckpoint(runId, seq) {
7118
7796
  const cp = this.checkpoints.get(this.key(runId, seq));
7119
7797
  return cp ? {
@@ -7321,6 +7999,9 @@ var InMemoryStateStore = class {
7321
7999
  ...r
7322
8000
  }));
7323
8001
  }
8002
+ async countRuns(query) {
8003
+ return this.matching(query).length;
8004
+ }
7324
8005
  async runFacets(query) {
7325
8006
  return mergeRunFacetRows(this.matching(query).map((run) => ({
7326
8007
  status: run.status,
@@ -7338,6 +8019,34 @@ var InMemoryStateStore = class {
7338
8019
  });
7339
8020
  return runValueFacetsFromRuns(runs, axis, opts);
7340
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
+ }
7341
8050
  async listCheckpoints(runId) {
7342
8051
  const rows = [
7343
8052
  ...this.checkpoints.values()
@@ -7419,7 +8128,9 @@ var InMemoryTransport = class {
7419
8128
  CONTROL_FLOW_SIGNAL,
7420
8129
  CURRENT_PROTOCOL_VERSION,
7421
8130
  CodecStateStore,
8131
+ ConcurrencyLimitError,
7422
8132
  ContinueAsNew,
8133
+ DEFAULT_CONCURRENCY_STATUSES,
7423
8134
  DEFAULT_PARTITION,
7424
8135
  DURABLE_OPTIONS,
7425
8136
  DURABLE_OPTIONS_CANONICAL,
@@ -7449,6 +8160,9 @@ var InMemoryTransport = class {
7449
8160
  RunGateway,
7450
8161
  STATE_STORE,
7451
8162
  STATE_STORE_CANONICAL,
8163
+ ScheduleAlreadyExistsError,
8164
+ ScheduleClient,
8165
+ ScheduleNotFoundError,
7452
8166
  SignalTimeoutError,
7453
8167
  SingletonQueueFullError,
7454
8168
  TERMINAL_RUN_STATUSES,
@@ -7462,6 +8176,7 @@ var InMemoryTransport = class {
7462
8176
  aggregateAnnouncements,
7463
8177
  announcementsOf,
7464
8178
  applyAttributeQuery,
8179
+ assertConcurrency,
7465
8180
  attributeColumnFor,
7466
8181
  attributeOperand,
7467
8182
  attributePredicateOperands,
@@ -7474,7 +8189,9 @@ var InMemoryTransport = class {
7474
8189
  classifyWaiterToken,
7475
8190
  clearDurableExecutionWrappers,
7476
8191
  collectMetrics,
8192
+ concurrencyTag,
7477
8193
  controlPlaneDescriptor,
8194
+ countRuns,
7478
8195
  coveredTokens,
7479
8196
  createStepLogger,
7480
8197
  currentStep,
@@ -7496,6 +8213,7 @@ var InMemoryTransport = class {
7496
8213
  isEngineMintedTag,
7497
8214
  isEventToken,
7498
8215
  isLegacyDescriptor,
8216
+ isNonEmptyRunScope,
7499
8217
  isWorkflowControlFlowSignal,
7500
8218
  log,
7501
8219
  matchesAttributes,
@@ -7503,6 +8221,7 @@ var InMemoryTransport = class {
7503
8221
  mergeRunValueFacetRows,
7504
8222
  missingCapabilities,
7505
8223
  negotiate,
8224
+ nextCronFireMs,
7506
8225
  normalizeAttributeRows,
7507
8226
  normalizeDescriptor,
7508
8227
  observedAnnouncements,
@@ -7512,6 +8231,7 @@ var InMemoryTransport = class {
7512
8231
  prevCronFireMs,
7513
8232
  readSearchAttributes,
7514
8233
  requiredCapabilities,
8234
+ resolveConcurrency,
7515
8235
  resolveRouting,
7516
8236
  resolveRunWaiting,
7517
8237
  runDurableExecution,