@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.js CHANGED
@@ -250,6 +250,23 @@ var FatalError = class extends Error {
250
250
  this.code = code;
251
251
  }
252
252
  };
253
+ var ConcurrencyLimitError = class extends Error {
254
+ static {
255
+ __name(this, "ConcurrencyLimitError");
256
+ }
257
+ workflow;
258
+ key;
259
+ limit;
260
+ active;
261
+ constructor(workflow, key, limit, active) {
262
+ super(`concurrency limit reached for key "${key}" (${active}/${limit} in flight); ${workflow} was not started \u2014 retry later`);
263
+ this.name = "ConcurrencyLimitError";
264
+ this.workflow = workflow;
265
+ this.key = key;
266
+ this.limit = limit;
267
+ this.active = active;
268
+ }
269
+ };
253
270
  var SingletonQueueFullError = class extends Error {
254
271
  static {
255
272
  __name(this, "SingletonQueueFullError");
@@ -763,6 +780,58 @@ function instantCheckpoint(p) {
763
780
  }
764
781
  __name(instantCheckpoint, "instantCheckpoint");
765
782
 
783
+ // src/concurrency.ts
784
+ var DEFAULT_CONCURRENCY_STATUSES = [
785
+ "pending",
786
+ "running",
787
+ "suspended",
788
+ "cancelling",
789
+ "blocked"
790
+ ];
791
+ function concurrencyTag(key) {
792
+ return `concurrency:${key}`;
793
+ }
794
+ __name(concurrencyTag, "concurrencyTag");
795
+ async function resolveConcurrency(override, config, input) {
796
+ if (override) return override;
797
+ if (!config) return void 0;
798
+ const key = config.key(input);
799
+ if (key === void 0) return void 0;
800
+ const limit = typeof config.limit === "function" ? await config.limit(key) : config.limit;
801
+ return {
802
+ key,
803
+ limit,
804
+ countStatuses: config.countStatuses
805
+ };
806
+ }
807
+ __name(resolveConcurrency, "resolveConcurrency");
808
+ async function countRuns(store, query) {
809
+ if (store.countRuns) return store.countRuns(query);
810
+ if (store.runFacets && !query.status && !query.statuses && query.origin === void 0) {
811
+ const cells = await store.runFacets(query);
812
+ return cells.reduce((sum, c) => sum + c.count, 0);
813
+ }
814
+ if (store.runFacets && query.statuses && !query.status && query.origin === void 0) {
815
+ const { statuses, ...rest } = query;
816
+ const cells = await store.runFacets(rest);
817
+ return cells.filter((c) => statuses.includes(c.status)).reduce((sum, c) => sum + c.count, 0);
818
+ }
819
+ return (await store.listRuns(query)).length;
820
+ }
821
+ __name(countRuns, "countRuns");
822
+ async function assertConcurrency(store, workflow, quota) {
823
+ const active = quota.limit <= 0 ? 0 : await countRuns(store, {
824
+ tag: concurrencyTag(quota.key),
825
+ statuses: [
826
+ ...quota.countStatuses ?? DEFAULT_CONCURRENCY_STATUSES
827
+ ]
828
+ });
829
+ if (quota.limit <= 0 || active >= quota.limit) {
830
+ throw new ConcurrencyLimitError(workflow, quota.key, quota.limit, active);
831
+ }
832
+ }
833
+ __name(assertConcurrency, "assertConcurrency");
834
+
766
835
  // src/entities.ts
767
836
  var ENTITY_WORKFLOW = "__entity";
768
837
  var entityRunId = /* @__PURE__ */ __name((name, key) => `entity:${name}:${key}`, "entityRunId");
@@ -1595,6 +1664,19 @@ function planDispatch(requires, liveDescriptors, cp, token) {
1595
1664
  }
1596
1665
  __name(planDispatch, "planDispatch");
1597
1666
 
1667
+ // src/interfaces.ts
1668
+ var TERMINAL_RUN_STATUSES = [
1669
+ "completed",
1670
+ "failed",
1671
+ "cancelled",
1672
+ "dead"
1673
+ ];
1674
+ function isNonEmptyRunScope(scope) {
1675
+ if (!scope) return false;
1676
+ 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;
1677
+ }
1678
+ __name(isNonEmptyRunScope, "isNonEmptyRunScope");
1679
+
1598
1680
  // src/tenant-group.ts
1599
1681
  function tenantGroup(baseGroup, tenant) {
1600
1682
  return tenant !== void 0 && tenant !== "" && tenant !== "default" ? `${baseGroup}@${tenant}` : baseGroup;
@@ -1685,6 +1767,486 @@ function indexWaitersByRun(waiters) {
1685
1767
  }
1686
1768
  __name(indexWaitersByRun, "indexWaitersByRun");
1687
1769
 
1770
+ // src/scheduler.ts
1771
+ import { createRequire } from "node:module";
1772
+
1773
+ // src/cron-compat.ts
1774
+ function isIndexable(value) {
1775
+ return typeof value === "object" && value !== null || typeof value === "function";
1776
+ }
1777
+ __name(isIndexable, "isIndexable");
1778
+ function unwrapDefault(module_) {
1779
+ if (!isIndexable(module_)) return module_;
1780
+ if (Reflect.get(module_, "parseExpression") !== void 0) return module_;
1781
+ if (Reflect.get(module_, "CronExpressionParser") !== void 0) return module_;
1782
+ const inner = Reflect.get(module_, "default");
1783
+ return inner === void 0 ? module_ : inner;
1784
+ }
1785
+ __name(unwrapDefault, "unwrapDefault");
1786
+ function v4Entry(module_) {
1787
+ if (!isIndexable(module_)) return void 0;
1788
+ const parse = Reflect.get(module_, "parseExpression");
1789
+ if (typeof parse !== "function") return void 0;
1790
+ return (expr, options) => parse.call(module_, expr, options);
1791
+ }
1792
+ __name(v4Entry, "v4Entry");
1793
+ function v5Entry(module_) {
1794
+ if (!isIndexable(module_)) return void 0;
1795
+ const parser = Reflect.get(module_, "CronExpressionParser");
1796
+ if (!isIndexable(parser)) return void 0;
1797
+ const parse = Reflect.get(parser, "parse");
1798
+ if (typeof parse !== "function") return void 0;
1799
+ return (expr, options) => parse.call(parser, expr, options);
1800
+ }
1801
+ __name(v5Entry, "v5Entry");
1802
+ function resolveCronParse(module_) {
1803
+ const resolved = unwrapDefault(module_);
1804
+ return v4Entry(resolved) ?? v5Entry(resolved);
1805
+ }
1806
+ __name(resolveCronParse, "resolveCronParse");
1807
+
1808
+ // src/scheduler.ts
1809
+ var nodeRequire = createRequire(import.meta.url);
1810
+ var defaultSleep = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "defaultSleep");
1811
+ function scheduledRunId(key, everyMs, nowMs) {
1812
+ return `sched:${key}:${Math.floor(nowMs / everyMs)}`;
1813
+ }
1814
+ __name(scheduledRunId, "scheduledRunId");
1815
+ var cronParser;
1816
+ function loadCronParser() {
1817
+ if (cronParser !== void 0) return cronParser;
1818
+ try {
1819
+ cronParser = nodeRequire("cron-parser");
1820
+ } catch {
1821
+ throw new Error('cron schedules need the optional peer dependency "cron-parser" \u2014 install it (e.g. `npm i cron-parser`).');
1822
+ }
1823
+ return cronParser;
1824
+ }
1825
+ __name(loadCronParser, "loadCronParser");
1826
+ function prevCronFireMs(expr, nowMs, timezone = "UTC") {
1827
+ const parse = resolveCronParse(loadCronParser());
1828
+ if (!parse) {
1829
+ 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.`);
1830
+ }
1831
+ return parse(expr, {
1832
+ currentDate: new Date(nowMs + 1),
1833
+ tz: timezone
1834
+ }).prev().toDate().getTime();
1835
+ }
1836
+ __name(prevCronFireMs, "prevCronFireMs");
1837
+ function nextCronFireMs(expr, afterMs, timezone = "UTC") {
1838
+ const parse = resolveCronParse(loadCronParser());
1839
+ if (!parse) {
1840
+ 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.`);
1841
+ }
1842
+ return parse(expr, {
1843
+ currentDate: new Date(afterMs),
1844
+ tz: timezone
1845
+ }).next().toDate().getTime();
1846
+ }
1847
+ __name(nextCronFireMs, "nextCronFireMs");
1848
+ function scheduleRunIdAt(s, nowMs) {
1849
+ if (s.cron != null) return `sched:${s.key}:${prevCronFireMs(s.cron, nowMs, s.timezone)}`;
1850
+ if (s.everyMs != null) return scheduledRunId(s.key, s.everyMs, nowMs);
1851
+ throw new Error(`schedule "${s.key}" needs either "everyMs" or "cron"`);
1852
+ }
1853
+ __name(scheduleRunIdAt, "scheduleRunIdAt");
1854
+ function scheduleRunIdsAt(s, nowMs) {
1855
+ const current = scheduleRunIdAt(s, nowMs);
1856
+ if (!s.backfill || s.backfill.maxCatchup <= 0) return [
1857
+ current
1858
+ ];
1859
+ const ids = [];
1860
+ if (s.everyMs != null) {
1861
+ const bucket = Math.floor(nowMs / s.everyMs);
1862
+ const oldest = Math.max(0, bucket - s.backfill.maxCatchup);
1863
+ for (let b = oldest; b < bucket; b += 1) ids.push(`sched:${s.key}:${b}`);
1864
+ } else if (s.cron != null) {
1865
+ const prior = [];
1866
+ let cursor = prevCronFireMs(s.cron, nowMs, s.timezone);
1867
+ for (let i = 0; i < s.backfill.maxCatchup; i += 1) {
1868
+ const before = prevCronFireMs(s.cron, cursor - 1, s.timezone);
1869
+ prior.push(`sched:${s.key}:${before}`);
1870
+ cursor = before;
1871
+ }
1872
+ ids.push(...prior.reverse());
1873
+ }
1874
+ ids.push(current);
1875
+ return ids;
1876
+ }
1877
+ __name(scheduleRunIdsAt, "scheduleRunIdsAt");
1878
+ async function runSchedules(engine, schedules, nowMs, opts) {
1879
+ const random = opts?.random ?? Math.random;
1880
+ const sleep = opts?.sleep ?? defaultSleep;
1881
+ const ids = [];
1882
+ for (const s of schedules) {
1883
+ if (s.paused) continue;
1884
+ if (s.overlap === "skip" && s.everyMs) {
1885
+ const prevBucket = Math.floor(nowMs / s.everyMs) - 1;
1886
+ if (prevBucket >= 0) {
1887
+ const prev = await engine.getRun(`sched:${s.key}:${prevBucket}`);
1888
+ if (prev && (prev.status === "running" || prev.status === "suspended")) continue;
1889
+ }
1890
+ }
1891
+ if (s.jitter && s.jitter > 0) await sleep(Math.floor(random() * s.jitter));
1892
+ for (const runId of scheduleRunIdsAt(s, nowMs)) {
1893
+ await engine.start(s.workflow, s.input, runId);
1894
+ ids.push(runId);
1895
+ }
1896
+ }
1897
+ return ids;
1898
+ }
1899
+ __name(runSchedules, "runSchedules");
1900
+
1901
+ // src/schedules.ts
1902
+ var ScheduleAlreadyExistsError = class extends Error {
1903
+ static {
1904
+ __name(this, "ScheduleAlreadyExistsError");
1905
+ }
1906
+ scheduleId;
1907
+ constructor(scheduleId) {
1908
+ super(`schedule "${scheduleId}" already exists`), this.scheduleId = scheduleId;
1909
+ this.name = "ScheduleAlreadyExistsError";
1910
+ }
1911
+ };
1912
+ var ScheduleNotFoundError = class extends Error {
1913
+ static {
1914
+ __name(this, "ScheduleNotFoundError");
1915
+ }
1916
+ scheduleId;
1917
+ constructor(scheduleId) {
1918
+ super(`schedule "${scheduleId}" not found`), this.scheduleId = scheduleId;
1919
+ this.name = "ScheduleNotFoundError";
1920
+ }
1921
+ };
1922
+ var DEFAULT_NAMESPACE = "default";
1923
+ var ON_TIME_GRACE_MS = 6e4;
1924
+ var TICK_BATCH = 100;
1925
+ var ScheduleClient = class {
1926
+ static {
1927
+ __name(this, "ScheduleClient");
1928
+ }
1929
+ store;
1930
+ host;
1931
+ options;
1932
+ constructor(store, host, options = {}) {
1933
+ this.store = store;
1934
+ this.host = host;
1935
+ this.options = options;
1936
+ }
1937
+ now() {
1938
+ return (this.options.clock ?? Date.now)();
1939
+ }
1940
+ /** Whether the store persists schedules (implements the schedule methods). */
1941
+ get supported() {
1942
+ const s = this.store;
1943
+ return !!(s.saveSchedule && s.getSchedule && s.updateSchedule && s.deleteSchedule && s.listSchedules);
1944
+ }
1945
+ requireStore() {
1946
+ if (!this.supported) {
1947
+ 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");
1948
+ }
1949
+ const s = this.store;
1950
+ return {
1951
+ saveSchedule: /* @__PURE__ */ __name((r) => s.saveSchedule.call(s, r), "saveSchedule"),
1952
+ getSchedule: /* @__PURE__ */ __name((id) => s.getSchedule.call(s, id), "getSchedule"),
1953
+ updateSchedule: /* @__PURE__ */ __name((id, patch, expected) => s.updateSchedule.call(s, id, patch, expected), "updateSchedule"),
1954
+ deleteSchedule: /* @__PURE__ */ __name((id) => s.deleteSchedule.call(s, id), "deleteSchedule"),
1955
+ listSchedules: /* @__PURE__ */ __name((q) => s.listSchedules.call(s, q), "listSchedules")
1956
+ };
1957
+ }
1958
+ /** Create a schedule. Throws {@link ScheduleAlreadyExistsError} if the id is taken. */
1959
+ async create(opts) {
1960
+ const store = this.requireStore();
1961
+ if (await store.getSchedule(opts.id)) throw new ScheduleAlreadyExistsError(opts.id);
1962
+ return this.write(opts, null);
1963
+ }
1964
+ /**
1965
+ * Create the schedule, or replace the definition of an existing one (its history — last fire, last
1966
+ * run, fire count — is kept). The next fire is recomputed from now under the new definition, so a
1967
+ * window that already fired is never fired again by an upsert.
1968
+ */
1969
+ async upsert(opts) {
1970
+ const store = this.requireStore();
1971
+ return this.write(opts, await store.getSchedule(opts.id));
1972
+ }
1973
+ /** The schedule `id`, or null. */
1974
+ async get(id) {
1975
+ const record = await this.requireStore().getSchedule(id);
1976
+ return record ? describe(record) : null;
1977
+ }
1978
+ /** Schedules matching `query`, soonest-due first. */
1979
+ async list(query = {}) {
1980
+ return (await this.requireStore().listSchedules(query)).map(describe);
1981
+ }
1982
+ /** Delete the schedule. Runs it already started are left alone. Returns whether it existed. */
1983
+ delete(id) {
1984
+ return this.requireStore().deleteSchedule(id);
1985
+ }
1986
+ /** Stop firing until {@link resume}. `note` records why (shown by `get`/`list`). */
1987
+ async pause(id, note) {
1988
+ const store = this.requireStore();
1989
+ const record = await this.mustGet(id);
1990
+ const spec = {
1991
+ ...record.spec
1992
+ };
1993
+ if (note !== void 0) spec.note = note;
1994
+ await store.updateSchedule(id, {
1995
+ paused: true,
1996
+ spec,
1997
+ updatedAt: new Date(this.now())
1998
+ });
1999
+ return describe(await this.mustGet(id));
2000
+ }
2001
+ /**
2002
+ * Fire again from the next window after now — the windows skipped while paused are NOT caught up.
2003
+ */
2004
+ async resume(id) {
2005
+ const store = this.requireStore();
2006
+ const record = await this.mustGet(id);
2007
+ const spec = record.spec;
2008
+ const window = nextWindowAfter(spec, this.now());
2009
+ await store.updateSchedule(id, {
2010
+ paused: false,
2011
+ nextFireAt: window === null ? null : window + jitterFor(record.id, window, spec),
2012
+ state: {
2013
+ ...record.state,
2014
+ window
2015
+ },
2016
+ updatedAt: new Date(this.now())
2017
+ });
2018
+ return describe(await this.mustGet(id));
2019
+ }
2020
+ /**
2021
+ * Start one run of the schedule right now, outside its cadence (paused or not). The run id is
2022
+ * `sched:<id>:manual:<nowMs>`; it doesn't move the schedule's next fire.
2023
+ */
2024
+ async trigger(id) {
2025
+ const record = await this.mustGet(id);
2026
+ const runId = `sched:${id}:manual:${this.now()}`;
2027
+ return this.host.start(record.workflow, specOf(record).input, runId, startOptions(record));
2028
+ }
2029
+ /**
2030
+ * Fire every due schedule (in this engine's namespace; all namespaces for an operator engine):
2031
+ * start its window's run and advance it to the next window. Called by the NestJS timer poller on
2032
+ * each tick when `persistedSchedules` is on; call it yourself from any other driver. Returns the
2033
+ * run ids started (or re-found, for an idempotent re-fire).
2034
+ */
2035
+ async tick(nowMs = this.now()) {
2036
+ const store = this.requireStore();
2037
+ const started = [];
2038
+ for (let round = 0; round < 50; round++) {
2039
+ const due = await store.listSchedules({
2040
+ dueBy: nowMs,
2041
+ namespace: this.options.namespace,
2042
+ limit: TICK_BATCH
2043
+ });
2044
+ for (const record of due) {
2045
+ const runId = await this.fire(record, nowMs);
2046
+ if (runId) started.push(runId);
2047
+ }
2048
+ if (due.length < TICK_BATCH) break;
2049
+ }
2050
+ return started;
2051
+ }
2052
+ /** Fire one due schedule and advance it. Returns the run id started, if any. */
2053
+ async fire(record, nowMs) {
2054
+ const store = this.requireStore();
2055
+ const spec = specOf(record);
2056
+ const state = record.state;
2057
+ let latest;
2058
+ let next;
2059
+ try {
2060
+ latest = latestWindowAtOrBefore(spec, nowMs);
2061
+ next = nextWindowAfter(spec, latest);
2062
+ } catch (error2) {
2063
+ await store.updateSchedule(record.id, {
2064
+ paused: true,
2065
+ nextFireAt: null,
2066
+ state: {
2067
+ ...state,
2068
+ lastError: `invalid schedule: ${errorMessage(error2)}`
2069
+ },
2070
+ updatedAt: new Date(nowMs)
2071
+ }, record.nextFireAt);
2072
+ return void 0;
2073
+ }
2074
+ const window = state.window ?? latest;
2075
+ const jitter = spec.jitterMs ?? 0;
2076
+ const onTime = nowMs - latest <= ON_TIME_GRACE_MS + jitter;
2077
+ let runId;
2078
+ let lastError;
2079
+ let fired = false;
2080
+ const skipMissed = spec.catchup === "skip" && !onTime;
2081
+ const beforeWindow = latest < window;
2082
+ if (!skipMissed && !beforeWindow && !await this.overlapping(spec, state)) {
2083
+ runId = `sched:${record.id}:${latest}`;
2084
+ try {
2085
+ await this.host.start(record.workflow, spec.input, runId, startOptions(record));
2086
+ fired = true;
2087
+ } catch (error2) {
2088
+ if (await this.host.getRun(runId)) fired = true;
2089
+ else lastError = errorMessage(error2);
2090
+ }
2091
+ }
2092
+ const nextState = {
2093
+ ...state,
2094
+ window: next,
2095
+ ...fired ? {
2096
+ lastFireAt: latest,
2097
+ lastRunId: runId,
2098
+ fires: (state.fires ?? 0) + 1
2099
+ } : {},
2100
+ lastError: fired ? void 0 : lastError ?? state.lastError
2101
+ };
2102
+ await store.updateSchedule(record.id, {
2103
+ nextFireAt: next === null ? null : next + jitterFor(record.id, next, spec),
2104
+ state: stripUndefined(nextState),
2105
+ updatedAt: new Date(nowMs)
2106
+ }, record.nextFireAt);
2107
+ return fired ? runId : void 0;
2108
+ }
2109
+ /** `overlap: 'skip'` and the previous run of this schedule is still in flight. */
2110
+ async overlapping(spec, state) {
2111
+ if (spec.overlap !== "skip" || !state.lastRunId) return false;
2112
+ const prev = await this.host.getRun(state.lastRunId);
2113
+ return !!prev && !TERMINAL_RUN_STATUSES.includes(prev.status);
2114
+ }
2115
+ async mustGet(id) {
2116
+ const record = await this.requireStore().getSchedule(id);
2117
+ if (!record) throw new ScheduleNotFoundError(id);
2118
+ return record;
2119
+ }
2120
+ async write(opts, existing) {
2121
+ const store = this.requireStore();
2122
+ const spec = toSpec(opts);
2123
+ const now = this.now();
2124
+ const paused = opts.paused ?? existing?.paused ?? false;
2125
+ const window = nextWindowAfter(spec, now);
2126
+ const record = {
2127
+ id: opts.id,
2128
+ namespace: opts.namespace ?? this.options.namespace ?? DEFAULT_NAMESPACE,
2129
+ workflow: workflowName(opts.workflow),
2130
+ paused,
2131
+ nextFireAt: window === null ? null : window + jitterFor(opts.id, window, spec),
2132
+ tags: opts.tags,
2133
+ spec,
2134
+ state: stripUndefined({
2135
+ ...existing?.state ?? {},
2136
+ window
2137
+ }),
2138
+ createdAt: existing?.createdAt ?? new Date(now),
2139
+ updatedAt: new Date(now)
2140
+ };
2141
+ await store.saveSchedule(record);
2142
+ return describe(record);
2143
+ }
2144
+ };
2145
+ function toSpec(opts) {
2146
+ if (opts.cron == null === (opts.every == null)) {
2147
+ throw new Error(`schedule "${opts.id}" needs exactly one of "cron" or "every"`);
2148
+ }
2149
+ const everyMs = opts.every != null ? parseDuration(opts.every) : void 0;
2150
+ if (everyMs !== void 0 && !(everyMs > 0)) {
2151
+ throw new Error(`schedule "${opts.id}": "every" must be a positive duration`);
2152
+ }
2153
+ const jitterMs = opts.jitter != null ? parseDuration(opts.jitter) : void 0;
2154
+ return stripUndefined({
2155
+ v: 1,
2156
+ input: opts.input,
2157
+ cron: opts.cron,
2158
+ everyMs,
2159
+ timezone: opts.timezone,
2160
+ jitterMs: jitterMs && jitterMs > 0 ? jitterMs : void 0,
2161
+ overlap: opts.overlap ?? "allow",
2162
+ catchup: opts.catchup ?? "latest",
2163
+ note: opts.note,
2164
+ searchAttributes: opts.searchAttributes,
2165
+ priority: opts.priority,
2166
+ concurrency: opts.concurrency
2167
+ });
2168
+ }
2169
+ __name(toSpec, "toSpec");
2170
+ function specOf(record) {
2171
+ return record.spec;
2172
+ }
2173
+ __name(specOf, "specOf");
2174
+ function latestWindowAtOrBefore(spec, atMs) {
2175
+ if (spec.cron != null) return prevCronFireMs(spec.cron, atMs, spec.timezone);
2176
+ const every = spec.everyMs;
2177
+ return Math.floor(atMs / every) * every;
2178
+ }
2179
+ __name(latestWindowAtOrBefore, "latestWindowAtOrBefore");
2180
+ function nextWindowAfter(spec, afterMs) {
2181
+ if (spec.cron != null) return nextCronFireMs(spec.cron, afterMs, spec.timezone);
2182
+ const every = spec.everyMs;
2183
+ return (Math.floor(afterMs / every) + 1) * every;
2184
+ }
2185
+ __name(nextWindowAfter, "nextWindowAfter");
2186
+ function jitterFor(id, window, spec) {
2187
+ const max = spec.jitterMs ?? 0;
2188
+ if (max <= 0) return 0;
2189
+ let h = 2166136261;
2190
+ for (const ch of `${id}@${window}`) {
2191
+ h ^= ch.charCodeAt(0);
2192
+ h = Math.imul(h, 16777619) >>> 0;
2193
+ }
2194
+ return h % max;
2195
+ }
2196
+ __name(jitterFor, "jitterFor");
2197
+ function startOptions(record) {
2198
+ const spec = specOf(record);
2199
+ return stripUndefined({
2200
+ tags: [
2201
+ ...record.tags ?? [],
2202
+ `schedule:${record.id}`
2203
+ ],
2204
+ namespace: record.namespace,
2205
+ searchAttributes: spec.searchAttributes,
2206
+ priority: spec.priority,
2207
+ concurrency: spec.concurrency
2208
+ });
2209
+ }
2210
+ __name(startOptions, "startOptions");
2211
+ function describe(record) {
2212
+ const spec = specOf(record);
2213
+ const state = record.state;
2214
+ return stripUndefined({
2215
+ id: record.id,
2216
+ workflow: record.workflow,
2217
+ input: spec.input,
2218
+ namespace: record.namespace,
2219
+ cron: spec.cron,
2220
+ everyMs: spec.everyMs,
2221
+ timezone: spec.timezone,
2222
+ jitterMs: spec.jitterMs,
2223
+ overlap: spec.overlap ?? "allow",
2224
+ catchup: spec.catchup ?? "latest",
2225
+ paused: record.paused,
2226
+ note: spec.note,
2227
+ tags: record.tags,
2228
+ searchAttributes: spec.searchAttributes,
2229
+ priority: spec.priority,
2230
+ concurrency: spec.concurrency,
2231
+ nextFireAt: record.paused || record.nextFireAt == null ? null : new Date(record.nextFireAt),
2232
+ lastFireAt: state.lastFireAt != null ? new Date(state.lastFireAt) : void 0,
2233
+ lastRunId: state.lastRunId,
2234
+ fires: state.fires ?? 0,
2235
+ lastError: state.lastError,
2236
+ createdAt: record.createdAt,
2237
+ updatedAt: record.updatedAt
2238
+ });
2239
+ }
2240
+ __name(describe, "describe");
2241
+ function stripUndefined(o) {
2242
+ return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
2243
+ }
2244
+ __name(stripUndefined, "stripUndefined");
2245
+ function errorMessage(error2) {
2246
+ return error2 instanceof Error ? error2.message : String(error2);
2247
+ }
2248
+ __name(errorMessage, "errorMessage");
2249
+
1688
2250
  // src/singleton-gate.ts
1689
2251
  var SINGLETON_RETRY_MS = 1e3;
1690
2252
  var SINGLETON_RETRY_JITTER_MS = 250;
@@ -2753,6 +3315,12 @@ var WorkflowEngine = class {
2753
3315
  runDispatcher;
2754
3316
  /** Per-key serialization for singleton workflows (admission, back-pressure, notify-on-release). */
2755
3317
  singletons;
3318
+ /**
3319
+ * Persisted, runtime-managed schedules: create/upsert/pause/resume/delete/list/trigger them, and
3320
+ * `tick()` fires the due ones (the NestJS timer poller does it when `persistedSchedules` is on).
3321
+ * Needs a store that persists schedules — every bundled adapter does. See {@link ScheduleClient}.
3322
+ */
3323
+ schedules;
2756
3324
  /** Every registered workflow, keyed by `name@version` — so old versions stay runnable. */
2757
3325
  workflows = /* @__PURE__ */ new Map();
2758
3326
  /** The newest registered version per workflow name — used to `start` new runs. */
@@ -2837,6 +3405,13 @@ var WorkflowEngine = class {
2837
3405
  }, "dispatch"),
2838
3406
  configFor: /* @__PURE__ */ __name((run) => (this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? this.latest.get(run.workflow))?.singleton, "configFor")
2839
3407
  });
3408
+ this.schedules = new ScheduleClient(this.store, {
3409
+ start: /* @__PURE__ */ __name((workflow, input, runId, opts) => this.start(workflow, input, runId, opts), "start"),
3410
+ getRun: /* @__PURE__ */ __name((runId) => this.store.getRun(runId), "getRun")
3411
+ }, {
3412
+ namespace: this.namespace,
3413
+ clock: this.clock
3414
+ });
2840
3415
  this.pool.bind(
2841
3416
  async (result) => {
2842
3417
  const waiter = this.pending.get(result.stepId);
@@ -2972,6 +3547,7 @@ var WorkflowEngine = class {
2972
3547
  fn,
2973
3548
  tags: opts?.tags,
2974
3549
  singleton: opts?.singleton,
3550
+ concurrency: opts?.concurrency,
2975
3551
  executionTimeoutMs: opts?.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
2976
3552
  validateInput: opts?.validateInput,
2977
3553
  searchAttributesSchema: opts?.searchAttributesSchema,
@@ -3034,6 +3610,7 @@ var WorkflowEngine = class {
3034
3610
  }, "fn"),
3035
3611
  tags: opts.tags,
3036
3612
  singleton: opts.singleton,
3613
+ concurrency: opts.concurrency,
3037
3614
  executionTimeoutMs: opts.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
3038
3615
  validateInput: opts.validateInput,
3039
3616
  requires: opts.requires,
@@ -3205,12 +3782,17 @@ var WorkflowEngine = class {
3205
3782
  error: prior.error
3206
3783
  };
3207
3784
  }
3785
+ const quota = await resolveConcurrency(opts?.concurrency, registered.concurrency, input);
3786
+ if (quota) await assertConcurrency(this.store, name, quota);
3208
3787
  const now = /* @__PURE__ */ new Date();
3209
3788
  const stampedTags = [
3210
3789
  ...opts?.tags ?? [],
3211
3790
  ...registered.singleton ? [
3212
3791
  this.singletons.tag(registered.singleton, input)
3213
3792
  ] : [],
3793
+ ...quota ? [
3794
+ concurrencyTag(quota.key)
3795
+ ] : [],
3214
3796
  ...registered.versionDeclared === false ? [
3215
3797
  VERSION_UNDECLARED_TAG
3216
3798
  ] : []
@@ -4248,6 +4830,123 @@ var WorkflowEngine = class {
4248
4830
  return deleted + 1;
4249
4831
  }
4250
4832
  /**
4833
+ * Hard-delete EVERY run matching `scope` — plus, by default, each one's whole child subtree — in
4834
+ * bounded batches. The "forget this tenant" / "drop this feature's history" operation: where
4835
+ * {@link deleteRun} removes one tree you already know the id of, this drains everything a scope
4836
+ * selects (`{ namespace: 'acme' }`, `{ tag: 'tenant:acme' }`, `{ workflows: [...] }`, attribute
4837
+ * predicates …) through the store API, so no caller has to reach into the `durable_*` tables.
4838
+ *
4839
+ * - **Live runs** (not yet terminal) are {@link cancel cancelled} first (plain cancel, no saga
4840
+ * undo), which tells any worker holding one to stop, and then deleted with the rest. Pass
4841
+ * `cancelLive: false` to leave live runs (and live descendants of finished ones) alone and
4842
+ * delete terminal history only.
4843
+ * - **Children** are found through the same parent→child edges as {@link getRunChildren} and deleted
4844
+ * with their root even when they don't match `scope` themselves (children inherit `namespace`,
4845
+ * but not `tags`). `children: false` deletes exactly the matching runs.
4846
+ * - **Batched**: `batchSize` runs (default 500) per round, each round one bulk
4847
+ * {@link StateStore.deleteRuns} (falling back to per-run {@link StateStore.deleteRun}).
4848
+ *
4849
+ * An EMPTY scope is rejected — it would delete every run in the store. Returns how many runs were
4850
+ * deleted (children included).
4851
+ */
4852
+ async purgeRuns(scope, opts) {
4853
+ if (!isNonEmptyRunScope(scope)) {
4854
+ throw new Error("purgeRuns: refusing an empty scope (it would delete every run) \u2014 pass at least one predicate, e.g. { namespace }");
4855
+ }
4856
+ const batchSize = Math.max(1, opts?.batchSize ?? 500);
4857
+ const cancelLive = opts?.cancelLive ?? true;
4858
+ const withChildren = opts?.children ?? true;
4859
+ const statuses = cancelLive ? void 0 : [
4860
+ ...TERMINAL_RUN_STATUSES
4861
+ ];
4862
+ let deleted = 0;
4863
+ let previous = "";
4864
+ for (; ; ) {
4865
+ const batch = await this.store.listRuns({
4866
+ ...scope,
4867
+ statuses,
4868
+ limit: batchSize
4869
+ });
4870
+ if (batch.length === 0) break;
4871
+ const fingerprint = batch.map((r) => r.id).join("\0");
4872
+ if (fingerprint === previous) {
4873
+ throw new Error("purgeRuns: the store returned the same runs after deleting them");
4874
+ }
4875
+ previous = fingerprint;
4876
+ const known = new Map(batch.map((r) => [
4877
+ r.id,
4878
+ r.status
4879
+ ]));
4880
+ const ids = withChildren ? await this.collectSubtrees(batch.map((r) => r.id)) : batch.map((r) => r.id);
4881
+ const doomed = [];
4882
+ for (const id of ids) {
4883
+ const status = known.has(id) ? known.get(id) : (await this.store.getRun(id))?.status;
4884
+ if (status === void 0) continue;
4885
+ if (!TERMINAL_RUN_STATUSES.includes(status)) {
4886
+ if (!cancelLive) continue;
4887
+ await this.cancel(id).catch(() => void 0);
4888
+ }
4889
+ doomed.push(id);
4890
+ }
4891
+ await this.deleteRunIds(doomed);
4892
+ deleted += doomed.length;
4893
+ if (batch.length < batchSize) break;
4894
+ }
4895
+ return deleted;
4896
+ }
4897
+ /**
4898
+ * {@link purgeRuns} for one namespace — every run (and child) a tenant/partition owns. Children
4899
+ * inherit their parent's namespace, so a namespace purge is complete by construction.
4900
+ */
4901
+ purgeNamespace(namespace, opts) {
4902
+ return this.purgeRuns({
4903
+ namespace
4904
+ }, opts);
4905
+ }
4906
+ /** The given runs plus every descendant, children before parents. One waiter scan for the batch. */
4907
+ async collectSubtrees(rootIds) {
4908
+ const waitersByParent = /* @__PURE__ */ new Map();
4909
+ for (const w of await this.store.listSignalWaiters("child:")) {
4910
+ const list = waitersByParent.get(w.runId) ?? [];
4911
+ list.push(w.token.slice("child:".length));
4912
+ waitersByParent.set(w.runId, list);
4913
+ }
4914
+ const prefixes = [
4915
+ "signal:child:",
4916
+ "spawn:"
4917
+ ];
4918
+ const childrenOf = /* @__PURE__ */ __name(async (parent) => {
4919
+ const ids = new Set(waitersByParent.get(parent) ?? []);
4920
+ 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)));
4921
+ for (const cp of cps) {
4922
+ if (cp.name.startsWith("signal:child:")) ids.add(cp.name.slice("signal:child:".length));
4923
+ if (cp.name.startsWith("spawn:") && typeof cp.output === "string") ids.add(cp.output);
4924
+ }
4925
+ return [
4926
+ ...ids
4927
+ ];
4928
+ }, "childrenOf");
4929
+ const ordered = [];
4930
+ const seen = /* @__PURE__ */ new Set();
4931
+ const visit = /* @__PURE__ */ __name(async (id) => {
4932
+ if (seen.has(id)) return;
4933
+ seen.add(id);
4934
+ for (const child of await childrenOf(id)) await visit(child);
4935
+ ordered.push(id);
4936
+ }, "visit");
4937
+ for (const id of rootIds) await visit(id);
4938
+ return ordered;
4939
+ }
4940
+ /** Bulk-delete through the store's `deleteRuns`, or one `deleteRun` per id when it has none. */
4941
+ async deleteRunIds(ids) {
4942
+ if (ids.length === 0) return;
4943
+ if (this.store.deleteRuns) {
4944
+ await this.store.deleteRuns(ids);
4945
+ return;
4946
+ }
4947
+ for (const id of ids) await this.store.deleteRun(id);
4948
+ }
4949
+ /**
4251
4950
  * Cascade cancellation to a run's children — both awaited (`ctx.child`, found via its live
4252
4951
  * `child:<id>` waiter) and fire-and-forget (`ctx.startChild`, found via its `spawn:<id>`
4253
4952
  * checkpoint). Recursive, so a whole subtree is cancelled; the terminal guard in `cancel` stops it
@@ -6268,14 +6967,6 @@ var RemoteStepError = class extends Error {
6268
6967
  }
6269
6968
  };
6270
6969
 
6271
- // src/interfaces.ts
6272
- var TERMINAL_RUN_STATUSES = [
6273
- "completed",
6274
- "failed",
6275
- "cancelled",
6276
- "dead"
6277
- ];
6278
-
6279
6970
  // src/run-facets.ts
6280
6971
  function facetOrigin(origin) {
6281
6972
  return origin?.trim() ? origin : null;
@@ -6307,7 +6998,8 @@ __name(mergeRunFacetRows, "mergeRunFacetRows");
6307
6998
  var RUN_VALUE_FACET_LIMIT = 100;
6308
6999
  var RUN_VALUE_FACET_SCAN = 5e3;
6309
7000
  var ENGINE_MINTED_TAG_PREFIXES = [
6310
- "singleton:"
7001
+ "singleton:",
7002
+ "concurrency:"
6311
7003
  ];
6312
7004
  function isEngineMintedTag(value) {
6313
7005
  return value !== null && ENGINE_MINTED_TAG_PREFIXES.some((prefix) => value.startsWith(prefix));
@@ -6412,9 +7104,42 @@ var CodecStateStore = class {
6412
7104
  * Bound the same conditional way as {@link runFacets} so absence is reported, not faked.
6413
7105
  */
6414
7106
  runValueFacets;
7107
+ /** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
7108
+ * engine's per-run fallback still kicks in through the wrapper. */
7109
+ deleteRuns;
7110
+ /** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
7111
+ * when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
7112
+ pruneTerminalRuns;
7113
+ /** Forwarded verbatim — a count reads no payload. Bound only when the inner store counts, so the
7114
+ * engine's facet/listing fallback still applies through the wrapper. */
7115
+ countRuns;
7116
+ /** Schedules persist their run `input` inside `spec` — encoded like any other payload. Bound only
7117
+ * when the inner store persists schedules, so `engine.schedules` reports absence faithfully. */
7118
+ saveSchedule;
7119
+ getSchedule;
7120
+ updateSchedule;
7121
+ deleteSchedule;
7122
+ listSchedules;
6415
7123
  constructor(inner, codec) {
6416
7124
  this.inner = inner;
6417
7125
  this.codec = codec;
7126
+ const deleteRuns = inner.deleteRuns;
7127
+ if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
7128
+ const prune = inner.pruneTerminalRuns;
7129
+ if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
7130
+ const count = inner.countRuns;
7131
+ if (count) this.countRuns = (query) => count.call(inner, query);
7132
+ const { saveSchedule, getSchedule, updateSchedule, deleteSchedule, listSchedules } = inner;
7133
+ if (saveSchedule && getSchedule && updateSchedule && deleteSchedule && listSchedules) {
7134
+ this.saveSchedule = (r) => saveSchedule.call(inner, this.encSchedule(r));
7135
+ this.getSchedule = async (id) => this.decSchedule(await getSchedule.call(inner, id));
7136
+ this.updateSchedule = (id, patch, expected) => updateSchedule.call(inner, id, patch.spec ? {
7137
+ ...patch,
7138
+ spec: this.encSpec(patch.spec)
7139
+ } : patch, expected);
7140
+ this.deleteSchedule = (id) => deleteSchedule.call(inner, id);
7141
+ this.listSchedules = async (q) => (await listSchedules.call(inner, q)).map((r) => this.decSchedule(r));
7142
+ }
6418
7143
  const facets = inner.runFacets;
6419
7144
  if (facets) this.runFacets = (query) => facets.call(inner, query);
6420
7145
  const valueFacets = inner.runValueFacets;
@@ -6456,6 +7181,28 @@ var CodecStateStore = class {
6456
7181
  output: this.dec(cp.output)
6457
7182
  };
6458
7183
  }
7184
+ encSpec(spec) {
7185
+ return "input" in spec ? {
7186
+ ...spec,
7187
+ input: this.enc(spec.input)
7188
+ } : spec;
7189
+ }
7190
+ encSchedule(r) {
7191
+ return {
7192
+ ...r,
7193
+ spec: this.encSpec(r.spec)
7194
+ };
7195
+ }
7196
+ decSchedule(r) {
7197
+ if (!r || !("input" in r.spec)) return r;
7198
+ return {
7199
+ ...r,
7200
+ spec: {
7201
+ ...r.spec,
7202
+ input: this.dec(r.spec.input)
7203
+ }
7204
+ };
7205
+ }
6459
7206
  ensureSchema() {
6460
7207
  return this.inner.ensureSchema?.() ?? Promise.resolve();
6461
7208
  }
@@ -6654,126 +7401,6 @@ function collectMetrics(engine) {
6654
7401
  }
6655
7402
  __name(collectMetrics, "collectMetrics");
6656
7403
 
6657
- // src/scheduler.ts
6658
- import { createRequire } from "node:module";
6659
-
6660
- // src/cron-compat.ts
6661
- function isIndexable(value) {
6662
- return typeof value === "object" && value !== null || typeof value === "function";
6663
- }
6664
- __name(isIndexable, "isIndexable");
6665
- function unwrapDefault(module_) {
6666
- if (!isIndexable(module_)) return module_;
6667
- if (Reflect.get(module_, "parseExpression") !== void 0) return module_;
6668
- if (Reflect.get(module_, "CronExpressionParser") !== void 0) return module_;
6669
- const inner = Reflect.get(module_, "default");
6670
- return inner === void 0 ? module_ : inner;
6671
- }
6672
- __name(unwrapDefault, "unwrapDefault");
6673
- function v4Entry(module_) {
6674
- if (!isIndexable(module_)) return void 0;
6675
- const parse = Reflect.get(module_, "parseExpression");
6676
- if (typeof parse !== "function") return void 0;
6677
- return (expr, options) => parse.call(module_, expr, options);
6678
- }
6679
- __name(v4Entry, "v4Entry");
6680
- function v5Entry(module_) {
6681
- if (!isIndexable(module_)) return void 0;
6682
- const parser = Reflect.get(module_, "CronExpressionParser");
6683
- if (!isIndexable(parser)) return void 0;
6684
- const parse = Reflect.get(parser, "parse");
6685
- if (typeof parse !== "function") return void 0;
6686
- return (expr, options) => parse.call(parser, expr, options);
6687
- }
6688
- __name(v5Entry, "v5Entry");
6689
- function resolveCronParse(module_) {
6690
- const resolved = unwrapDefault(module_);
6691
- return v4Entry(resolved) ?? v5Entry(resolved);
6692
- }
6693
- __name(resolveCronParse, "resolveCronParse");
6694
-
6695
- // src/scheduler.ts
6696
- var nodeRequire = createRequire(import.meta.url);
6697
- var defaultSleep = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "defaultSleep");
6698
- function scheduledRunId(key, everyMs, nowMs) {
6699
- return `sched:${key}:${Math.floor(nowMs / everyMs)}`;
6700
- }
6701
- __name(scheduledRunId, "scheduledRunId");
6702
- var cronParser;
6703
- function loadCronParser() {
6704
- if (cronParser !== void 0) return cronParser;
6705
- try {
6706
- cronParser = nodeRequire("cron-parser");
6707
- } catch {
6708
- throw new Error('cron schedules need the optional peer dependency "cron-parser" \u2014 install it (e.g. `npm i cron-parser`).');
6709
- }
6710
- return cronParser;
6711
- }
6712
- __name(loadCronParser, "loadCronParser");
6713
- function prevCronFireMs(expr, nowMs, timezone = "UTC") {
6714
- const parse = resolveCronParse(loadCronParser());
6715
- if (!parse) {
6716
- throw new Error(`the installed "cron-parser" exposes neither v4's parseExpression nor v5's CronExpressionParser.parse \u2014 the supported range is ^4.0.0 || ^5.0.0.`);
6717
- }
6718
- return parse(expr, {
6719
- currentDate: new Date(nowMs + 1),
6720
- tz: timezone
6721
- }).prev().toDate().getTime();
6722
- }
6723
- __name(prevCronFireMs, "prevCronFireMs");
6724
- function scheduleRunIdAt(s, nowMs) {
6725
- if (s.cron != null) return `sched:${s.key}:${prevCronFireMs(s.cron, nowMs, s.timezone)}`;
6726
- if (s.everyMs != null) return scheduledRunId(s.key, s.everyMs, nowMs);
6727
- throw new Error(`schedule "${s.key}" needs either "everyMs" or "cron"`);
6728
- }
6729
- __name(scheduleRunIdAt, "scheduleRunIdAt");
6730
- function scheduleRunIdsAt(s, nowMs) {
6731
- const current = scheduleRunIdAt(s, nowMs);
6732
- if (!s.backfill || s.backfill.maxCatchup <= 0) return [
6733
- current
6734
- ];
6735
- const ids = [];
6736
- if (s.everyMs != null) {
6737
- const bucket = Math.floor(nowMs / s.everyMs);
6738
- const oldest = Math.max(0, bucket - s.backfill.maxCatchup);
6739
- for (let b = oldest; b < bucket; b += 1) ids.push(`sched:${s.key}:${b}`);
6740
- } else if (s.cron != null) {
6741
- const prior = [];
6742
- let cursor = prevCronFireMs(s.cron, nowMs, s.timezone);
6743
- for (let i = 0; i < s.backfill.maxCatchup; i += 1) {
6744
- const before = prevCronFireMs(s.cron, cursor - 1, s.timezone);
6745
- prior.push(`sched:${s.key}:${before}`);
6746
- cursor = before;
6747
- }
6748
- ids.push(...prior.reverse());
6749
- }
6750
- ids.push(current);
6751
- return ids;
6752
- }
6753
- __name(scheduleRunIdsAt, "scheduleRunIdsAt");
6754
- async function runSchedules(engine, schedules, nowMs, opts) {
6755
- const random = opts?.random ?? Math.random;
6756
- const sleep = opts?.sleep ?? defaultSleep;
6757
- const ids = [];
6758
- for (const s of schedules) {
6759
- if (s.paused) continue;
6760
- if (s.overlap === "skip" && s.everyMs) {
6761
- const prevBucket = Math.floor(nowMs / s.everyMs) - 1;
6762
- if (prevBucket >= 0) {
6763
- const prev = await engine.getRun(`sched:${s.key}:${prevBucket}`);
6764
- if (prev && (prev.status === "running" || prev.status === "suspended")) continue;
6765
- }
6766
- }
6767
- if (s.jitter && s.jitter > 0) await sleep(Math.floor(random() * s.jitter));
6768
- for (const runId of scheduleRunIdsAt(s, nowMs)) {
6769
- await engine.start(s.workflow, s.input, runId);
6770
- ids.push(runId);
6771
- }
6772
- }
6773
- return ids;
6774
- }
6775
- __name(runSchedules, "runSchedules");
6776
-
6777
7404
  // src/search-attributes.ts
6778
7405
  function compare(actual, filter) {
6779
7406
  if (filter.op === "in") return filter.values.some((v) => actual === v);
@@ -6981,6 +7608,38 @@ var InMemoryStateStore = class {
6981
7608
  }
6982
7609
  this.reindexAttributes(runId, void 0);
6983
7610
  }
7611
+ async deleteRuns(runIds) {
7612
+ for (const runId of runIds) await this.deleteRun(runId);
7613
+ }
7614
+ async pruneTerminalRuns(policy, nowMs, limit) {
7615
+ const statuses = policy.statuses.filter((s) => TERMINAL_RUN_STATUSES.includes(s));
7616
+ if (statuses.length === 0 || limit <= 0) return 0;
7617
+ if (policy.maxAge == null && policy.maxCount == null) return 0;
7618
+ const inScope = this.matching({
7619
+ ...policy.scope ?? {},
7620
+ statuses
7621
+ });
7622
+ const ids = /* @__PURE__ */ new Set();
7623
+ if (policy.maxAge != null) {
7624
+ const cutoff = nowMs - parseDuration(policy.maxAge);
7625
+ const expired = inScope.filter((r) => r.updatedAt.getTime() < cutoff).sort((a, b) => a.updatedAt.getTime() - b.updatedAt.getTime()).slice(0, limit);
7626
+ for (const r of expired) ids.add(r.id);
7627
+ }
7628
+ if (policy.maxCount != null && ids.size < limit) {
7629
+ const byRecency = [
7630
+ ...inScope
7631
+ ].sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime() || b.id.localeCompare(a.id));
7632
+ for (const r of byRecency.slice(policy.maxCount)) {
7633
+ if (ids.size >= limit) break;
7634
+ ids.add(r.id);
7635
+ }
7636
+ }
7637
+ const doomed = [
7638
+ ...ids
7639
+ ].slice(0, limit);
7640
+ await this.deleteRuns(doomed);
7641
+ return doomed.length;
7642
+ }
6984
7643
  async getCheckpoint(runId, seq) {
6985
7644
  const cp = this.checkpoints.get(this.key(runId, seq));
6986
7645
  return cp ? {
@@ -7188,6 +7847,9 @@ var InMemoryStateStore = class {
7188
7847
  ...r
7189
7848
  }));
7190
7849
  }
7850
+ async countRuns(query) {
7851
+ return this.matching(query).length;
7852
+ }
7191
7853
  async runFacets(query) {
7192
7854
  return mergeRunFacetRows(this.matching(query).map((run) => ({
7193
7855
  status: run.status,
@@ -7205,6 +7867,34 @@ var InMemoryStateStore = class {
7205
7867
  });
7206
7868
  return runValueFacetsFromRuns(runs, axis, opts);
7207
7869
  }
7870
+ schedules = /* @__PURE__ */ new Map();
7871
+ async saveSchedule(record) {
7872
+ this.schedules.set(record.id, structuredClone(record));
7873
+ }
7874
+ async getSchedule(id) {
7875
+ const record = this.schedules.get(id);
7876
+ return record ? structuredClone(record) : null;
7877
+ }
7878
+ async updateSchedule(id, patch, expectedNextFireAt) {
7879
+ const record = this.schedules.get(id);
7880
+ if (!record) return false;
7881
+ if (expectedNextFireAt !== void 0 && record.nextFireAt !== expectedNextFireAt) return false;
7882
+ this.schedules.set(id, {
7883
+ ...record,
7884
+ ...structuredClone(patch)
7885
+ });
7886
+ return true;
7887
+ }
7888
+ async deleteSchedule(id) {
7889
+ return this.schedules.delete(id);
7890
+ }
7891
+ async listSchedules(query) {
7892
+ const rows = [
7893
+ ...this.schedules.values()
7894
+ ].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));
7895
+ const offset = query.offset ?? 0;
7896
+ return rows.slice(offset, query.limit != null ? offset + query.limit : void 0).map((s) => structuredClone(s));
7897
+ }
7208
7898
  async listCheckpoints(runId) {
7209
7899
  const rows = [
7210
7900
  ...this.checkpoints.values()
@@ -7285,7 +7975,9 @@ export {
7285
7975
  CONTROL_FLOW_SIGNAL,
7286
7976
  CURRENT_PROTOCOL_VERSION,
7287
7977
  CodecStateStore,
7978
+ ConcurrencyLimitError,
7288
7979
  ContinueAsNew,
7980
+ DEFAULT_CONCURRENCY_STATUSES,
7289
7981
  DEFAULT_PARTITION,
7290
7982
  DURABLE_OPTIONS,
7291
7983
  DURABLE_OPTIONS_CANONICAL,
@@ -7315,6 +8007,9 @@ export {
7315
8007
  RunGateway,
7316
8008
  STATE_STORE,
7317
8009
  STATE_STORE_CANONICAL,
8010
+ ScheduleAlreadyExistsError,
8011
+ ScheduleClient,
8012
+ ScheduleNotFoundError,
7318
8013
  SignalTimeoutError,
7319
8014
  SingletonQueueFullError,
7320
8015
  TERMINAL_RUN_STATUSES,
@@ -7328,6 +8023,7 @@ export {
7328
8023
  aggregateAnnouncements,
7329
8024
  announcementsOf,
7330
8025
  applyAttributeQuery,
8026
+ assertConcurrency,
7331
8027
  attributeColumnFor,
7332
8028
  attributeOperand,
7333
8029
  attributePredicateOperands,
@@ -7340,7 +8036,9 @@ export {
7340
8036
  classifyWaiterToken,
7341
8037
  clearDurableExecutionWrappers,
7342
8038
  collectMetrics,
8039
+ concurrencyTag,
7343
8040
  controlPlaneDescriptor,
8041
+ countRuns,
7344
8042
  coveredTokens,
7345
8043
  createStepLogger,
7346
8044
  currentStep,
@@ -7362,6 +8060,7 @@ export {
7362
8060
  isEngineMintedTag,
7363
8061
  isEventToken,
7364
8062
  isLegacyDescriptor,
8063
+ isNonEmptyRunScope,
7365
8064
  isWorkflowControlFlowSignal,
7366
8065
  log,
7367
8066
  matchesAttributes,
@@ -7369,6 +8068,7 @@ export {
7369
8068
  mergeRunValueFacetRows,
7370
8069
  missingCapabilities,
7371
8070
  negotiate,
8071
+ nextCronFireMs,
7372
8072
  normalizeAttributeRows,
7373
8073
  normalizeDescriptor,
7374
8074
  observedAnnouncements,
@@ -7378,6 +8078,7 @@ export {
7378
8078
  prevCronFireMs,
7379
8079
  readSearchAttributes,
7380
8080
  requiredCapabilities,
8081
+ resolveConcurrency,
7381
8082
  resolveRouting,
7382
8083
  resolveRunWaiting,
7383
8084
  runDurableExecution,