@ontrails/core 1.0.0-beta.14 → 1.0.0-beta.16

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.
Files changed (314) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +73 -24
  3. package/package.json +11 -4
  4. package/src/activation-provenance.ts +111 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-projection.ts +221 -0
  7. package/src/activation-source.ts +91 -0
  8. package/src/blob-ref.ts +51 -0
  9. package/src/branded.ts +1 -1
  10. package/src/context.ts +28 -4
  11. package/src/contour.ts +344 -0
  12. package/src/cross-batch.ts +69 -0
  13. package/src/cross-schema.ts +36 -0
  14. package/src/detours.ts +8 -0
  15. package/src/draft.ts +60 -52
  16. package/src/error-projection.ts +51 -0
  17. package/src/errors.ts +245 -48
  18. package/src/execute.ts +1250 -40
  19. package/src/fire.ts +1169 -0
  20. package/src/index.ts +383 -49
  21. package/src/internal/fork-ctx.ts +69 -0
  22. package/src/layer-projection.ts +192 -0
  23. package/src/layer.ts +81 -0
  24. package/src/observe.ts +361 -0
  25. package/src/path-security.ts +1 -1
  26. package/src/permits.ts +12 -0
  27. package/src/redaction/patterns.ts +6 -3
  28. package/src/resilience.ts +11 -11
  29. package/src/resource-config.ts +792 -0
  30. package/src/resource.ts +175 -0
  31. package/src/result.ts +18 -0
  32. package/src/run.ts +7 -6
  33. package/src/schedule-runtime.ts +689 -0
  34. package/src/schedule.ts +324 -0
  35. package/src/serialization.ts +90 -55
  36. package/src/signal-diagnostics.ts +633 -0
  37. package/src/signal-ref.ts +87 -0
  38. package/src/signal.ts +37 -0
  39. package/src/store/accessor-protocol.ts +56 -0
  40. package/src/store/index.ts +4 -0
  41. package/src/structured-examples.ts +245 -0
  42. package/src/surface-derivation.ts +91 -0
  43. package/src/surface-filter.ts +176 -0
  44. package/src/topo.ts +710 -59
  45. package/src/tracing.ts +345 -0
  46. package/src/trail.ts +347 -33
  47. package/src/trails/derive-trail.ts +842 -0
  48. package/src/trails/index.ts +9 -0
  49. package/src/trails/ingest.ts +138 -0
  50. package/src/{internal/trails-db.ts → trails-db.ts} +50 -31
  51. package/src/transport-error-map.ts +174 -0
  52. package/src/type-utils.ts +17 -2
  53. package/src/types.ts +208 -17
  54. package/src/validate-established-topo.ts +8 -2
  55. package/src/validate-topo.ts +244 -13
  56. package/src/validation.ts +58 -1
  57. package/src/webhook.ts +276 -0
  58. package/src/workspace.ts +1 -1
  59. package/src/zod-wrappers.ts +72 -0
  60. package/.turbo/turbo-build.log +0 -1
  61. package/.turbo/turbo-lint.log +0 -3
  62. package/.turbo/turbo-typecheck.log +0 -1
  63. package/dist/adapters.d.ts +0 -39
  64. package/dist/adapters.d.ts.map +0 -1
  65. package/dist/adapters.js +0 -2
  66. package/dist/adapters.js.map +0 -1
  67. package/dist/blob-ref.d.ts +0 -20
  68. package/dist/blob-ref.d.ts.map +0 -1
  69. package/dist/blob-ref.js +0 -22
  70. package/dist/blob-ref.js.map +0 -1
  71. package/dist/branded.d.ts +0 -36
  72. package/dist/branded.d.ts.map +0 -1
  73. package/dist/branded.js +0 -89
  74. package/dist/branded.js.map +0 -1
  75. package/dist/collections.d.ts +0 -31
  76. package/dist/collections.d.ts.map +0 -1
  77. package/dist/collections.js +0 -60
  78. package/dist/collections.js.map +0 -1
  79. package/dist/context.d.ts +0 -10
  80. package/dist/context.d.ts.map +0 -1
  81. package/dist/context.js +0 -21
  82. package/dist/context.js.map +0 -1
  83. package/dist/derive.d.ts +0 -44
  84. package/dist/derive.d.ts.map +0 -1
  85. package/dist/derive.js +0 -149
  86. package/dist/derive.js.map +0 -1
  87. package/dist/dispatch.d.ts +0 -27
  88. package/dist/dispatch.d.ts.map +0 -1
  89. package/dist/dispatch.js +0 -34
  90. package/dist/dispatch.js.map +0 -1
  91. package/dist/draft.d.ts +0 -28
  92. package/dist/draft.d.ts.map +0 -1
  93. package/dist/draft.js +0 -156
  94. package/dist/draft.js.map +0 -1
  95. package/dist/errors.d.ts +0 -83
  96. package/dist/errors.d.ts.map +0 -1
  97. package/dist/errors.js +0 -142
  98. package/dist/errors.js.map +0 -1
  99. package/dist/event.d.ts +0 -8
  100. package/dist/event.d.ts.map +0 -1
  101. package/dist/event.js +0 -7
  102. package/dist/event.js.map +0 -1
  103. package/dist/execute.d.ts +0 -35
  104. package/dist/execute.d.ts.map +0 -1
  105. package/dist/execute.js +0 -90
  106. package/dist/execute.js.map +0 -1
  107. package/dist/fetch.d.ts +0 -15
  108. package/dist/fetch.d.ts.map +0 -1
  109. package/dist/fetch.js +0 -102
  110. package/dist/fetch.js.map +0 -1
  111. package/dist/gate.d.ts +0 -17
  112. package/dist/gate.d.ts.map +0 -1
  113. package/dist/gate.js +0 -21
  114. package/dist/gate.js.map +0 -1
  115. package/dist/guards.d.ts +0 -17
  116. package/dist/guards.d.ts.map +0 -1
  117. package/dist/guards.js +0 -25
  118. package/dist/guards.js.map +0 -1
  119. package/dist/health.d.ts +0 -18
  120. package/dist/health.d.ts.map +0 -1
  121. package/dist/health.js +0 -5
  122. package/dist/health.js.map +0 -1
  123. package/dist/index.d.ts +0 -48
  124. package/dist/index.d.ts.map +0 -1
  125. package/dist/index.js +0 -51
  126. package/dist/index.js.map +0 -1
  127. package/dist/internal/topo-saves.d.ts +0 -47
  128. package/dist/internal/topo-saves.d.ts.map +0 -1
  129. package/dist/internal/topo-saves.js +0 -310
  130. package/dist/internal/topo-saves.js.map +0 -1
  131. package/dist/internal/topo-store-read.d.ts +0 -67
  132. package/dist/internal/topo-store-read.d.ts.map +0 -1
  133. package/dist/internal/topo-store-read.js +0 -222
  134. package/dist/internal/topo-store-read.js.map +0 -1
  135. package/dist/internal/topo-store.d.ts +0 -12
  136. package/dist/internal/topo-store.d.ts.map +0 -1
  137. package/dist/internal/topo-store.js +0 -571
  138. package/dist/internal/topo-store.js.map +0 -1
  139. package/dist/internal/trails-db.d.ts +0 -16
  140. package/dist/internal/trails-db.d.ts.map +0 -1
  141. package/dist/internal/trails-db.js +0 -118
  142. package/dist/internal/trails-db.js.map +0 -1
  143. package/dist/job.d.ts +0 -24
  144. package/dist/job.d.ts.map +0 -1
  145. package/dist/job.js +0 -17
  146. package/dist/job.js.map +0 -1
  147. package/dist/layer.d.ts +0 -6
  148. package/dist/layer.d.ts.map +0 -1
  149. package/dist/layer.js +0 -4
  150. package/dist/layer.js.map +0 -1
  151. package/dist/path-security.d.ts +0 -28
  152. package/dist/path-security.d.ts.map +0 -1
  153. package/dist/path-security.js +0 -63
  154. package/dist/path-security.js.map +0 -1
  155. package/dist/patterns/bulk.d.ts +0 -15
  156. package/dist/patterns/bulk.d.ts.map +0 -1
  157. package/dist/patterns/bulk.js +0 -14
  158. package/dist/patterns/bulk.js.map +0 -1
  159. package/dist/patterns/change.d.ts +0 -10
  160. package/dist/patterns/change.d.ts.map +0 -1
  161. package/dist/patterns/change.js +0 -10
  162. package/dist/patterns/change.js.map +0 -1
  163. package/dist/patterns/date-range.d.ts +0 -10
  164. package/dist/patterns/date-range.d.ts.map +0 -1
  165. package/dist/patterns/date-range.js +0 -10
  166. package/dist/patterns/date-range.js.map +0 -1
  167. package/dist/patterns/index.d.ts +0 -9
  168. package/dist/patterns/index.d.ts.map +0 -1
  169. package/dist/patterns/index.js +0 -9
  170. package/dist/patterns/index.js.map +0 -1
  171. package/dist/patterns/pagination.d.ts +0 -18
  172. package/dist/patterns/pagination.d.ts.map +0 -1
  173. package/dist/patterns/pagination.js +0 -18
  174. package/dist/patterns/pagination.js.map +0 -1
  175. package/dist/patterns/progress.d.ts +0 -11
  176. package/dist/patterns/progress.d.ts.map +0 -1
  177. package/dist/patterns/progress.js +0 -11
  178. package/dist/patterns/progress.js.map +0 -1
  179. package/dist/patterns/sorting.d.ts +0 -13
  180. package/dist/patterns/sorting.d.ts.map +0 -1
  181. package/dist/patterns/sorting.js +0 -10
  182. package/dist/patterns/sorting.js.map +0 -1
  183. package/dist/patterns/status.d.ts +0 -15
  184. package/dist/patterns/status.d.ts.map +0 -1
  185. package/dist/patterns/status.js +0 -9
  186. package/dist/patterns/status.js.map +0 -1
  187. package/dist/patterns/timestamps.d.ts +0 -10
  188. package/dist/patterns/timestamps.d.ts.map +0 -1
  189. package/dist/patterns/timestamps.js +0 -10
  190. package/dist/patterns/timestamps.js.map +0 -1
  191. package/dist/provision-config.d.ts +0 -22
  192. package/dist/provision-config.d.ts.map +0 -1
  193. package/dist/provision-config.js +0 -210
  194. package/dist/provision-config.js.map +0 -1
  195. package/dist/provision.d.ts +0 -71
  196. package/dist/provision.d.ts.map +0 -1
  197. package/dist/provision.js +0 -56
  198. package/dist/provision.js.map +0 -1
  199. package/dist/redaction/index.d.ts +0 -4
  200. package/dist/redaction/index.d.ts.map +0 -1
  201. package/dist/redaction/index.js +0 -3
  202. package/dist/redaction/index.js.map +0 -1
  203. package/dist/redaction/patterns.d.ts +0 -9
  204. package/dist/redaction/patterns.d.ts.map +0 -1
  205. package/dist/redaction/patterns.js +0 -39
  206. package/dist/redaction/patterns.js.map +0 -1
  207. package/dist/redaction/redactor.d.ts +0 -27
  208. package/dist/redaction/redactor.d.ts.map +0 -1
  209. package/dist/redaction/redactor.js +0 -89
  210. package/dist/redaction/redactor.js.map +0 -1
  211. package/dist/resilience.d.ts +0 -34
  212. package/dist/resilience.d.ts.map +0 -1
  213. package/dist/resilience.js +0 -164
  214. package/dist/resilience.js.map +0 -1
  215. package/dist/result.d.ts +0 -57
  216. package/dist/result.d.ts.map +0 -1
  217. package/dist/result.js +0 -156
  218. package/dist/result.js.map +0 -1
  219. package/dist/run.d.ts +0 -27
  220. package/dist/run.d.ts.map +0 -1
  221. package/dist/run.js +0 -34
  222. package/dist/run.js.map +0 -1
  223. package/dist/serialization.d.ts +0 -27
  224. package/dist/serialization.d.ts.map +0 -1
  225. package/dist/serialization.js +0 -153
  226. package/dist/serialization.js.map +0 -1
  227. package/dist/service-config.d.ts +0 -22
  228. package/dist/service-config.d.ts.map +0 -1
  229. package/dist/service-config.js +0 -210
  230. package/dist/service-config.js.map +0 -1
  231. package/dist/service.d.ts +0 -71
  232. package/dist/service.d.ts.map +0 -1
  233. package/dist/service.js +0 -56
  234. package/dist/service.js.map +0 -1
  235. package/dist/signal.d.ts +0 -33
  236. package/dist/signal.d.ts.map +0 -1
  237. package/dist/signal.js +0 -17
  238. package/dist/signal.js.map +0 -1
  239. package/dist/topo-store.d.ts +0 -48
  240. package/dist/topo-store.d.ts.map +0 -1
  241. package/dist/topo-store.js +0 -175
  242. package/dist/topo-store.js.map +0 -1
  243. package/dist/topo.d.ts +0 -25
  244. package/dist/topo.d.ts.map +0 -1
  245. package/dist/topo.js +0 -112
  246. package/dist/topo.js.map +0 -1
  247. package/dist/trail.d.ts +0 -96
  248. package/dist/trail.d.ts.map +0 -1
  249. package/dist/trail.js +0 -20
  250. package/dist/trail.js.map +0 -1
  251. package/dist/type-utils.d.ts +0 -24
  252. package/dist/type-utils.d.ts.map +0 -1
  253. package/dist/type-utils.js +0 -12
  254. package/dist/type-utils.js.map +0 -1
  255. package/dist/types.d.ts +0 -71
  256. package/dist/types.d.ts.map +0 -1
  257. package/dist/types.js +0 -3
  258. package/dist/types.js.map +0 -1
  259. package/dist/validate-established-topo.d.ts +0 -76
  260. package/dist/validate-established-topo.d.ts.map +0 -1
  261. package/dist/validate-established-topo.js +0 -43
  262. package/dist/validate-established-topo.js.map +0 -1
  263. package/dist/validate-topo.d.ts +0 -24
  264. package/dist/validate-topo.d.ts.map +0 -1
  265. package/dist/validate-topo.js +0 -176
  266. package/dist/validate-topo.js.map +0 -1
  267. package/dist/validation.d.ts +0 -27
  268. package/dist/validation.d.ts.map +0 -1
  269. package/dist/validation.js +0 -165
  270. package/dist/validation.js.map +0 -1
  271. package/dist/workspace.d.ts +0 -25
  272. package/dist/workspace.d.ts.map +0 -1
  273. package/dist/workspace.js +0 -57
  274. package/dist/workspace.js.map +0 -1
  275. package/src/__tests__/blob-ref.test.ts +0 -103
  276. package/src/__tests__/branded.test.ts +0 -148
  277. package/src/__tests__/collections.test.ts +0 -126
  278. package/src/__tests__/context.test.ts +0 -77
  279. package/src/__tests__/derive.test.ts +0 -260
  280. package/src/__tests__/errors.test.ts +0 -309
  281. package/src/__tests__/execute.test.ts +0 -525
  282. package/src/__tests__/fetch.test.ts +0 -217
  283. package/src/__tests__/gate.test.ts +0 -125
  284. package/src/__tests__/guards.test.ts +0 -102
  285. package/src/__tests__/path-security.test.ts +0 -114
  286. package/src/__tests__/patterns.test.ts +0 -273
  287. package/src/__tests__/redaction.test.ts +0 -244
  288. package/src/__tests__/resilience.test.ts +0 -246
  289. package/src/__tests__/result.test.ts +0 -155
  290. package/src/__tests__/run.test.ts +0 -181
  291. package/src/__tests__/serialization.test.ts +0 -401
  292. package/src/__tests__/service-config.test.ts +0 -228
  293. package/src/__tests__/service.test.ts +0 -197
  294. package/src/__tests__/signal.test.ts +0 -82
  295. package/src/__tests__/topo-store-read.test.ts +0 -251
  296. package/src/__tests__/topo-store.test.ts +0 -469
  297. package/src/__tests__/topo.test.ts +0 -277
  298. package/src/__tests__/trail-permit.test.ts +0 -60
  299. package/src/__tests__/trail.test.ts +0 -261
  300. package/src/__tests__/trails-db.test.ts +0 -191
  301. package/src/__tests__/type-utils.test.ts +0 -90
  302. package/src/__tests__/validate-topo.test.ts +0 -489
  303. package/src/__tests__/validation.test.ts +0 -336
  304. package/src/__tests__/workspace.test.ts +0 -183
  305. package/src/event.ts +0 -15
  306. package/src/gate.ts +0 -44
  307. package/src/internal/topo-saves.ts +0 -429
  308. package/src/internal/topo-store-read.ts +0 -473
  309. package/src/internal/topo-store.ts +0 -1087
  310. package/src/provision-config.ts +0 -373
  311. package/src/provision.ts +0 -148
  312. package/src/topo-store.ts +0 -301
  313. package/tsconfig.json +0 -9
  314. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,689 @@
1
+ import type { ActivationEntry } from './activation-source.js';
2
+ import { activationSourceKey } from './activation-source-projection.js';
3
+ import {
4
+ buildActivationProvenanceTraceAttrs,
5
+ withActivationProvenance,
6
+ } from './activation-provenance.js';
7
+ import type { ActivationProvenance } from './activation-provenance.js';
8
+ import { getActivationWherePredicate } from './activation-source.js';
9
+ import { createTrailContext } from './context.js';
10
+ import type { ExecuteTrailOptions } from './execute.js';
11
+ import { ConflictError, InternalError } from './errors.js';
12
+ import {
13
+ getTraceSink,
14
+ TRACE_CONTEXT_KEY,
15
+ traceContextFromRecord,
16
+ writeActivationTraceRecord,
17
+ } from './tracing.js';
18
+ import type { TraceContext } from './tracing.js';
19
+ import { Result } from './result.js';
20
+ import { drainResources } from './resource-config.js';
21
+ import type { ResourceDrainReport } from './resource-config.js';
22
+ import { run } from './run.js';
23
+ import type { Topo } from './topo.js';
24
+ import type { AnyTrail } from './trail.js';
25
+ import type { Logger, TrailContext, TrailContextInit } from './types.js';
26
+ import { validateTopo } from './validate-topo.js';
27
+
28
+ type ScheduleInputDefault = Record<string, never>;
29
+
30
+ export type ScheduleRuntimeState = 'idle' | 'running' | 'stopped' | 'stopping';
31
+
32
+ export type ScheduleCronHandler = () => Promise<void> | void;
33
+
34
+ export interface ScheduleCronJob {
35
+ readonly cron?: string | undefined;
36
+ ref?: (() => unknown) | undefined;
37
+ stop(): unknown;
38
+ unref?: (() => unknown) | undefined;
39
+ }
40
+
41
+ export type ScheduleCronFactory = (
42
+ cron: string,
43
+ handler: ScheduleCronHandler
44
+ ) => ScheduleCronJob;
45
+
46
+ export type ScheduleRuntimeLogger = Pick<Logger, 'debug' | 'error' | 'warn'>;
47
+
48
+ export type ScheduleRuntimeSkipReason =
49
+ | 'stopped'
50
+ | 'where_error'
51
+ | 'where_false';
52
+
53
+ export type ScheduleRuntimeRunStatus = 'err' | 'ok' | 'skipped';
54
+
55
+ export interface ScheduleRuntimeRunRecord {
56
+ readonly activation?: ActivationProvenance | undefined;
57
+ readonly error?: Error | undefined;
58
+ readonly result?: Result<unknown, Error> | undefined;
59
+ readonly skipReason?: ScheduleRuntimeSkipReason | undefined;
60
+ readonly sourceId: string;
61
+ readonly status: ScheduleRuntimeRunStatus;
62
+ readonly trailId: string;
63
+ }
64
+
65
+ export interface ScheduleRuntimeOptions extends Pick<
66
+ ExecuteTrailOptions,
67
+ | 'abortSignal'
68
+ | 'configValues'
69
+ | 'createContext'
70
+ | 'ctx'
71
+ | 'layers'
72
+ | 'resources'
73
+ > {
74
+ readonly cron?: ScheduleCronFactory | undefined;
75
+ readonly logger?: ScheduleRuntimeLogger | undefined;
76
+ readonly onRun?:
77
+ | ((record: ScheduleRuntimeRunRecord) => Promise<void> | void)
78
+ | undefined;
79
+ readonly unref?: boolean | undefined;
80
+ }
81
+
82
+ export interface ScheduleRuntimeRegistrationReport {
83
+ readonly cron: string;
84
+ readonly sourceId: string;
85
+ readonly timezone?: string | undefined;
86
+ readonly trailId: string;
87
+ }
88
+
89
+ export type ScheduleRuntimeWarningCode = 'schedule_timezone_metadata_only';
90
+
91
+ export interface ScheduleRuntimeWarning {
92
+ readonly code: ScheduleRuntimeWarningCode;
93
+ readonly message: string;
94
+ readonly sourceId: string;
95
+ readonly timezone: string;
96
+ readonly trailId: string;
97
+ }
98
+
99
+ export interface ScheduleRuntimeStartReport {
100
+ readonly registered: readonly ScheduleRuntimeRegistrationReport[];
101
+ readonly warnings: readonly ScheduleRuntimeWarning[];
102
+ }
103
+
104
+ export interface ScheduleRuntimeStopReport {
105
+ readonly resources: ResourceDrainReport;
106
+ readonly settledRuns: number;
107
+ readonly stopped: readonly ScheduleRuntimeRegistrationReport[];
108
+ }
109
+
110
+ export interface ScheduleRuntime {
111
+ readonly state: () => ScheduleRuntimeState;
112
+ readonly start: () => Promise<Result<ScheduleRuntimeStartReport, Error>>;
113
+ readonly stop: () => Promise<Result<ScheduleRuntimeStopReport, Error>>;
114
+ }
115
+
116
+ interface BunCronGlobal {
117
+ readonly Bun?: {
118
+ readonly cron?: ScheduleCronFactory | undefined;
119
+ };
120
+ }
121
+
122
+ interface ScheduleActivationRegistration {
123
+ readonly activation: ActivationEntry;
124
+ readonly cron: string;
125
+ readonly input: unknown;
126
+ readonly sourceId: string;
127
+ readonly timezone?: string | undefined;
128
+ readonly trail: AnyTrail;
129
+ }
130
+
131
+ interface RunningScheduleRegistration extends ScheduleActivationRegistration {
132
+ readonly job: ScheduleCronJob;
133
+ }
134
+
135
+ const EMPTY_INPUT = Object.freeze({}) as ScheduleInputDefault;
136
+
137
+ const defaultCronFactory: ScheduleCronFactory = (cron, handler) => {
138
+ const bunCron = (globalThis as unknown as BunCronGlobal).Bun?.cron;
139
+ if (typeof bunCron !== 'function') {
140
+ throw new InternalError(
141
+ 'Bun.cron is not available in this runtime; provide a cron factory to createScheduleRuntime()'
142
+ );
143
+ }
144
+ return bunCron(cron, handler);
145
+ };
146
+
147
+ const errorFromUnknown = (error: unknown): Error =>
148
+ error instanceof Error ? error : new Error(String(error));
149
+
150
+ const errorRecord = (error: Error): Record<string, unknown> => ({
151
+ message: error.message,
152
+ name: error.name,
153
+ ...(error.cause instanceof Error
154
+ ? { cause: { message: error.cause.message, name: error.cause.name } }
155
+ : {}),
156
+ });
157
+
158
+ const registrationReport = (
159
+ registration: ScheduleActivationRegistration
160
+ ): ScheduleRuntimeRegistrationReport => ({
161
+ cron: registration.cron,
162
+ sourceId: registration.sourceId,
163
+ ...(registration.timezone === undefined
164
+ ? {}
165
+ : { timezone: registration.timezone }),
166
+ trailId: registration.trail.id,
167
+ });
168
+
169
+ const timezoneMetadataWarning = (
170
+ registration: ScheduleActivationRegistration
171
+ ): ScheduleRuntimeWarning | undefined =>
172
+ registration.timezone === undefined || registration.timezone === 'UTC'
173
+ ? undefined
174
+ : {
175
+ code: 'schedule_timezone_metadata_only',
176
+ message:
177
+ 'The built-in Bun cron runtime records schedule timezone metadata but does not apply timezone-aware scheduling.',
178
+ sourceId: registration.sourceId,
179
+ timezone: registration.timezone,
180
+ trailId: registration.trail.id,
181
+ };
182
+
183
+ const collectStartWarnings = (
184
+ registrations: readonly ScheduleActivationRegistration[]
185
+ ): ScheduleRuntimeWarning[] =>
186
+ registrations.flatMap((registration) => {
187
+ const warning = timezoneMetadataWarning(registration);
188
+ return warning === undefined ? [] : [warning];
189
+ });
190
+
191
+ const logStartWarnings = (
192
+ warnings: readonly ScheduleRuntimeWarning[],
193
+ logger: ScheduleRuntimeLogger | undefined
194
+ ): void => {
195
+ for (const warning of warnings) {
196
+ logger?.warn(warning.message, {
197
+ code: warning.code,
198
+ sourceId: warning.sourceId,
199
+ timezone: warning.timezone,
200
+ trailId: warning.trailId,
201
+ });
202
+ }
203
+ };
204
+
205
+ const sourceInput = (activation: ActivationEntry): unknown =>
206
+ Object.hasOwn(activation.source, 'input')
207
+ ? activation.source.input
208
+ : EMPTY_INPUT;
209
+
210
+ const collectScheduleActivations = (
211
+ graph: Topo
212
+ ): ScheduleActivationRegistration[] => {
213
+ const registrations = new Map<string, ScheduleActivationRegistration>();
214
+ for (const trail of graph.list()) {
215
+ for (const activation of trail.activationSources) {
216
+ if (activation.source.kind !== 'schedule') {
217
+ continue;
218
+ }
219
+ const key = `${trail.id}\0${activationSourceKey(activation.source)}`;
220
+ const previous = registrations.get(key);
221
+ if (
222
+ previous !== undefined &&
223
+ (previous.activation.where !== undefined ||
224
+ activation.where === undefined)
225
+ ) {
226
+ continue;
227
+ }
228
+ registrations.set(key, {
229
+ activation,
230
+ cron:
231
+ typeof activation.source.cron === 'string'
232
+ ? activation.source.cron
233
+ : '',
234
+ input: sourceInput(activation),
235
+ sourceId: activation.source.id,
236
+ ...(activation.source.timezone === undefined
237
+ ? {}
238
+ : { timezone: activation.source.timezone }),
239
+ trail,
240
+ });
241
+ }
242
+ }
243
+
244
+ return [...registrations.values()];
245
+ };
246
+
247
+ const scheduleActivationProvenance = (
248
+ registration: ScheduleActivationRegistration
249
+ ): ActivationProvenance => {
250
+ const fireId = Bun.randomUUIDv7();
251
+ return {
252
+ fireId,
253
+ rootFireId: fireId,
254
+ source: {
255
+ cron: registration.cron,
256
+ id: registration.sourceId,
257
+ kind: 'schedule',
258
+ ...(registration.activation.source.meta === undefined
259
+ ? {}
260
+ : { meta: registration.activation.source.meta }),
261
+ ...(registration.timezone === undefined
262
+ ? {}
263
+ : { timezone: registration.timezone }),
264
+ },
265
+ };
266
+ };
267
+
268
+ const scheduleActivationTraceAttrs = (
269
+ registration: ScheduleActivationRegistration,
270
+ activation: ActivationProvenance
271
+ ): Readonly<Record<string, unknown>> => ({
272
+ ...buildActivationProvenanceTraceAttrs(activation),
273
+ 'trails.activation.target_trail.id': registration.trail.id,
274
+ });
275
+
276
+ const recordScheduleActivationTrace = async (
277
+ registration: ScheduleActivationRegistration,
278
+ activation: ActivationProvenance,
279
+ graph: Topo
280
+ ): Promise<TraceContext | undefined> => {
281
+ const record = await writeActivationTraceRecord(
282
+ 'activation.scheduled',
283
+ scheduleActivationTraceAttrs(registration, activation),
284
+ 'ok',
285
+ undefined,
286
+ undefined,
287
+ graph.observe?.trace ?? getTraceSink()
288
+ );
289
+ return record === undefined ? undefined : traceContextFromRecord(record);
290
+ };
291
+
292
+ const emitRunRecord = async (
293
+ options: ScheduleRuntimeOptions,
294
+ record: ScheduleRuntimeRunRecord
295
+ ): Promise<void> => {
296
+ try {
297
+ await options.onRun?.(record);
298
+ } catch (error) {
299
+ options.logger?.warn('Schedule runtime onRun callback failed', {
300
+ error: errorFromUnknown(error).message,
301
+ sourceId: record.sourceId,
302
+ trailId: record.trailId,
303
+ });
304
+ }
305
+ };
306
+
307
+ const runContextOption = (
308
+ options: ScheduleRuntimeOptions,
309
+ activation: ActivationProvenance | undefined,
310
+ traceContext?: TraceContext | undefined
311
+ ): Pick<ExecuteTrailOptions, 'ctx'> | Record<string, never> => {
312
+ if (activation === undefined && traceContext === undefined) {
313
+ return options.ctx === undefined ? {} : { ctx: options.ctx };
314
+ }
315
+ const withActivation =
316
+ activation === undefined
317
+ ? (options.ctx ?? {})
318
+ : withActivationProvenance(options.ctx ?? {}, activation);
319
+ const ctx =
320
+ traceContext === undefined
321
+ ? withActivation
322
+ : {
323
+ ...withActivation,
324
+ extensions: {
325
+ ...withActivation.extensions,
326
+ [TRACE_CONTEXT_KEY]: traceContext,
327
+ },
328
+ };
329
+ return { ctx };
330
+ };
331
+
332
+ const runOptions = (
333
+ options: ScheduleRuntimeOptions,
334
+ activation?: ActivationProvenance | undefined,
335
+ traceContext?: TraceContext | undefined
336
+ ): Pick<
337
+ ExecuteTrailOptions,
338
+ | 'abortSignal'
339
+ | 'configValues'
340
+ | 'createContext'
341
+ | 'ctx'
342
+ | 'layers'
343
+ | 'resources'
344
+ > => ({
345
+ ...(options.abortSignal === undefined
346
+ ? {}
347
+ : { abortSignal: options.abortSignal }),
348
+ ...(options.configValues === undefined
349
+ ? {}
350
+ : { configValues: options.configValues }),
351
+ ...(options.createContext === undefined
352
+ ? {}
353
+ : { createContext: options.createContext }),
354
+ ...runContextOption(options, activation, traceContext),
355
+ ...(options.layers === undefined ? {} : { layers: options.layers }),
356
+ ...(options.resources === undefined ? {} : { resources: options.resources }),
357
+ });
358
+
359
+ const shouldRunActivation = async (
360
+ registration: ScheduleActivationRegistration,
361
+ options: ScheduleRuntimeOptions,
362
+ activation: ActivationProvenance
363
+ ): Promise<boolean> => {
364
+ const predicate = getActivationWherePredicate(registration.activation.where);
365
+ if (predicate === undefined) {
366
+ return true;
367
+ }
368
+
369
+ try {
370
+ const matched = await predicate(registration.input);
371
+ if (!matched) {
372
+ await emitRunRecord(options, {
373
+ activation,
374
+ skipReason: 'where_false',
375
+ sourceId: registration.sourceId,
376
+ status: 'skipped',
377
+ trailId: registration.trail.id,
378
+ });
379
+ }
380
+ return matched;
381
+ } catch (error) {
382
+ const cause = errorFromUnknown(error);
383
+ options.logger?.warn('Schedule activation predicate failed', {
384
+ error: cause.message,
385
+ sourceId: registration.sourceId,
386
+ trailId: registration.trail.id,
387
+ });
388
+ await emitRunRecord(options, {
389
+ activation,
390
+ error: cause,
391
+ skipReason: 'where_error',
392
+ sourceId: registration.sourceId,
393
+ status: 'skipped',
394
+ trailId: registration.trail.id,
395
+ });
396
+ return false;
397
+ }
398
+ };
399
+
400
+ const executeScheduleActivation = async (
401
+ graph: Topo,
402
+ registration: ScheduleActivationRegistration,
403
+ options: ScheduleRuntimeOptions,
404
+ activation: ActivationProvenance
405
+ ): Promise<void> => {
406
+ const shouldRun = await shouldRunActivation(
407
+ registration,
408
+ options,
409
+ activation
410
+ );
411
+ if (!shouldRun) {
412
+ return;
413
+ }
414
+
415
+ const traceContext = await recordScheduleActivationTrace(
416
+ registration,
417
+ activation,
418
+ graph
419
+ );
420
+
421
+ try {
422
+ const result = await run(
423
+ graph,
424
+ registration.trail.id,
425
+ registration.input,
426
+ runOptions(options, activation, traceContext)
427
+ );
428
+ if (result.isErr()) {
429
+ options.logger?.warn('Scheduled trail failed', {
430
+ error: result.error.message,
431
+ sourceId: registration.sourceId,
432
+ trailId: registration.trail.id,
433
+ });
434
+ await emitRunRecord(options, {
435
+ activation,
436
+ result,
437
+ sourceId: registration.sourceId,
438
+ status: 'err',
439
+ trailId: registration.trail.id,
440
+ });
441
+ return;
442
+ }
443
+ await emitRunRecord(options, {
444
+ activation,
445
+ result,
446
+ sourceId: registration.sourceId,
447
+ status: 'ok',
448
+ trailId: registration.trail.id,
449
+ });
450
+ } catch (error) {
451
+ const cause = errorFromUnknown(error);
452
+ options.logger?.error('Scheduled trail rejected unexpectedly', {
453
+ error: cause.message,
454
+ sourceId: registration.sourceId,
455
+ trailId: registration.trail.id,
456
+ });
457
+ await emitRunRecord(options, {
458
+ activation,
459
+ error: cause,
460
+ sourceId: registration.sourceId,
461
+ status: 'err',
462
+ trailId: registration.trail.id,
463
+ });
464
+ }
465
+ };
466
+
467
+ const mergeContextOverrides = (
468
+ base: TrailContextInit,
469
+ options: ScheduleRuntimeOptions
470
+ ): TrailContextInit => {
471
+ const withCtx =
472
+ options.ctx === undefined
473
+ ? base
474
+ : {
475
+ ...base,
476
+ ...options.ctx,
477
+ extensions: { ...base.extensions, ...options.ctx.extensions },
478
+ };
479
+
480
+ return options.abortSignal === undefined
481
+ ? withCtx
482
+ : { ...withCtx, abortSignal: options.abortSignal };
483
+ };
484
+
485
+ const createDrainContext = async (
486
+ options: ScheduleRuntimeOptions
487
+ ): Promise<TrailContext> => {
488
+ const seed =
489
+ options.createContext === undefined
490
+ ? undefined
491
+ : await options.createContext();
492
+ const base = createTrailContext(seed);
493
+ return createTrailContext(mergeContextOverrides(base, options));
494
+ };
495
+
496
+ const stopCronJobs = (
497
+ registrations: readonly RunningScheduleRegistration[]
498
+ ): Error[] => {
499
+ const failures: Error[] = [];
500
+ for (const registration of registrations) {
501
+ try {
502
+ registration.job.stop();
503
+ } catch (error) {
504
+ failures.push(errorFromUnknown(error));
505
+ }
506
+ }
507
+ return failures;
508
+ };
509
+
510
+ const startFailure = (
511
+ registration: ScheduleActivationRegistration,
512
+ error: unknown
513
+ ): InternalError => {
514
+ const cause = errorFromUnknown(error);
515
+ return new InternalError(
516
+ `Schedule source "${registration.sourceId}" failed to register for trail "${registration.trail.id}": ${cause.message}`,
517
+ {
518
+ cause,
519
+ context: { ...registrationReport(registration) },
520
+ }
521
+ );
522
+ };
523
+
524
+ const stopFailure = (
525
+ failures: readonly Error[],
526
+ report: Omit<ScheduleRuntimeStopReport, 'resources'>,
527
+ drainError?: Error
528
+ ): InternalError => {
529
+ const primary = failures[0] ?? drainError;
530
+ return new InternalError('Schedule runtime stop failed', {
531
+ ...(primary === undefined ? {} : { cause: primary }),
532
+ context: {
533
+ failures: [
534
+ ...failures.map(errorRecord),
535
+ ...(drainError === undefined ? [] : [errorRecord(drainError)]),
536
+ ],
537
+ settledRuns: report.settledRuns,
538
+ stopped: report.stopped,
539
+ },
540
+ });
541
+ };
542
+
543
+ export const createScheduleRuntime = (
544
+ graph: Topo,
545
+ options: ScheduleRuntimeOptions = {}
546
+ ): ScheduleRuntime => {
547
+ const cron = options.cron ?? defaultCronFactory;
548
+ const inFlight = new Set<Promise<void>>();
549
+ let state: ScheduleRuntimeState = 'idle';
550
+ let running: RunningScheduleRegistration[] = [];
551
+
552
+ const runAndTrack = (
553
+ registration: ScheduleActivationRegistration
554
+ ): Promise<void> => {
555
+ const activation = scheduleActivationProvenance(registration);
556
+ if (state !== 'running') {
557
+ return emitRunRecord(options, {
558
+ activation,
559
+ skipReason: 'stopped',
560
+ sourceId: registration.sourceId,
561
+ status: 'skipped',
562
+ trailId: registration.trail.id,
563
+ });
564
+ }
565
+
566
+ const execution = executeScheduleActivation(
567
+ graph,
568
+ registration,
569
+ options,
570
+ activation
571
+ );
572
+ inFlight.add(execution);
573
+ void (async () => {
574
+ try {
575
+ await execution;
576
+ } finally {
577
+ inFlight.delete(execution);
578
+ }
579
+ })();
580
+ return execution;
581
+ };
582
+
583
+ const start = async (): Promise<
584
+ Result<ScheduleRuntimeStartReport, Error>
585
+ > => {
586
+ if (state !== 'idle') {
587
+ return Result.err(
588
+ new ConflictError(`Schedule runtime cannot start from state "${state}"`)
589
+ );
590
+ }
591
+
592
+ const validated = validateTopo(graph);
593
+ if (validated.isErr()) {
594
+ return Result.err(validated.error);
595
+ }
596
+
597
+ const registrations = collectScheduleActivations(graph);
598
+ const started: RunningScheduleRegistration[] = [];
599
+ state = 'running';
600
+
601
+ for (const registration of registrations) {
602
+ try {
603
+ const job = cron(registration.cron, () => runAndTrack(registration));
604
+ if (options.unref === true) {
605
+ job.unref?.();
606
+ }
607
+ started.push({ ...registration, job });
608
+ } catch (error) {
609
+ state = 'stopping';
610
+ stopCronJobs(started);
611
+ state = 'idle';
612
+ running = [];
613
+ return Result.err(startFailure(registration, error));
614
+ }
615
+ }
616
+
617
+ running = started;
618
+ const warnings = collectStartWarnings(running);
619
+ logStartWarnings(warnings, options.logger);
620
+ return Result.ok({
621
+ registered: running.map(registrationReport),
622
+ warnings,
623
+ });
624
+ };
625
+
626
+ const stop = async (): Promise<Result<ScheduleRuntimeStopReport, Error>> => {
627
+ if (state === 'idle' || state === 'stopped') {
628
+ return Result.ok({
629
+ resources: { disposed: [], evicted: [] },
630
+ settledRuns: 0,
631
+ stopped: [],
632
+ });
633
+ }
634
+ if (state === 'stopping') {
635
+ return Result.err(
636
+ new ConflictError('Schedule runtime stop is already in progress')
637
+ );
638
+ }
639
+
640
+ state = 'stopping';
641
+ const stopping = running;
642
+ const stopped = stopping.map(registrationReport);
643
+ const stopFailures = stopCronJobs(stopping);
644
+ const pending = [...inFlight];
645
+ const settled = await Promise.allSettled(pending);
646
+ const rejectedRuns = settled
647
+ .filter((entry) => entry.status === 'rejected')
648
+ .map((entry) => errorFromUnknown(entry.reason));
649
+
650
+ const partialReport = { settledRuns: pending.length, stopped };
651
+ const failures = [...stopFailures, ...rejectedRuns];
652
+ const drained = await (async () => {
653
+ try {
654
+ const drainCtx = await createDrainContext(options);
655
+ return await drainResources(
656
+ graph.listResources(),
657
+ drainCtx,
658
+ options.configValues
659
+ );
660
+ } catch (error) {
661
+ return Result.err(errorFromUnknown(error));
662
+ }
663
+ })();
664
+
665
+ running = [];
666
+ state = 'stopped';
667
+
668
+ if (drained.isErr() || failures.length > 0) {
669
+ return Result.err(
670
+ stopFailure(
671
+ failures,
672
+ partialReport,
673
+ drained.isErr() ? drained.error : undefined
674
+ )
675
+ );
676
+ }
677
+
678
+ return Result.ok({
679
+ resources: drained.value,
680
+ ...partialReport,
681
+ });
682
+ };
683
+
684
+ return Object.freeze({
685
+ start,
686
+ state: () => state,
687
+ stop,
688
+ });
689
+ };