@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,324 @@
1
+ import { ValidationError } from './errors.js';
2
+ import type {
3
+ ActivationSource,
4
+ ActivationSourceMeta,
5
+ } from './activation-source.js';
6
+
7
+ type ScheduleInputDefault = Record<string, never>;
8
+
9
+ export interface ScheduleSpec<TInput = unknown> {
10
+ readonly cron: string;
11
+ readonly input?: TInput | undefined;
12
+ readonly meta?: ActivationSourceMeta | undefined;
13
+ readonly timezone?: string | undefined;
14
+ }
15
+
16
+ export interface ScheduleSource<TInput = unknown> extends ActivationSource {
17
+ readonly cron: string;
18
+ readonly input: TInput;
19
+ readonly kind: 'schedule';
20
+ readonly meta?: ActivationSourceMeta | undefined;
21
+ readonly timezone?: string | undefined;
22
+ }
23
+
24
+ export interface ScheduleValidationIssue {
25
+ readonly field: 'cron' | 'input' | 'timezone';
26
+ readonly message: string;
27
+ }
28
+
29
+ const CRON_FIELD_BOUNDS = Object.freeze([
30
+ { max: 59, min: 0, name: 'minute' },
31
+ { max: 23, min: 0, name: 'hour' },
32
+ { max: 31, min: 1, name: 'day of month' },
33
+ { max: 12, min: 1, name: 'month' },
34
+ { max: 7, min: 0, name: 'day of week' },
35
+ ] as const);
36
+
37
+ const EMPTY_INPUT = Object.freeze({}) as ScheduleInputDefault;
38
+
39
+ const normalizeCron = (cron: string): string =>
40
+ cron.trim().replaceAll(/\s+/g, ' ');
41
+
42
+ const isIntegerToken = (value: string): boolean => /^\d+$/.test(value);
43
+
44
+ const parseIntegerToken = (value: string): number | undefined =>
45
+ isIntegerToken(value) ? Number.parseInt(value, 10) : undefined;
46
+
47
+ const isNumberInRange = (
48
+ value: string,
49
+ bounds: (typeof CRON_FIELD_BOUNDS)[number]
50
+ ): boolean => {
51
+ const parsed = parseIntegerToken(value);
52
+ return parsed !== undefined && parsed >= bounds.min && parsed <= bounds.max;
53
+ };
54
+
55
+ const isCronAtomValid = (
56
+ atom: string,
57
+ bounds: (typeof CRON_FIELD_BOUNDS)[number]
58
+ ): boolean => {
59
+ if (atom === '*') {
60
+ return true;
61
+ }
62
+ if (isNumberInRange(atom, bounds)) {
63
+ return true;
64
+ }
65
+
66
+ const range = atom.split('-');
67
+ if (range.length !== 2) {
68
+ return false;
69
+ }
70
+ const [rangeStart, rangeEnd] = range;
71
+ if (rangeStart === undefined || rangeEnd === undefined) {
72
+ return false;
73
+ }
74
+ if (
75
+ !isNumberInRange(rangeStart, bounds) ||
76
+ !isNumberInRange(rangeEnd, bounds)
77
+ ) {
78
+ return false;
79
+ }
80
+ const start = Number.parseInt(rangeStart, 10);
81
+ const end = Number.parseInt(rangeEnd, 10);
82
+ return start <= end;
83
+ };
84
+
85
+ const isCronPartValid = (
86
+ part: string,
87
+ bounds: (typeof CRON_FIELD_BOUNDS)[number]
88
+ ): boolean => {
89
+ const stepped = part.split('/');
90
+ if (stepped.length > 2) {
91
+ return false;
92
+ }
93
+ const [atom, step] = stepped;
94
+ if (!atom || !isCronAtomValid(atom, bounds)) {
95
+ return false;
96
+ }
97
+ if (step === undefined) {
98
+ return true;
99
+ }
100
+ const parsedStep = parseIntegerToken(step);
101
+ return parsedStep !== undefined && parsedStep > 0;
102
+ };
103
+
104
+ const validateCronField = (
105
+ field: string,
106
+ bounds: (typeof CRON_FIELD_BOUNDS)[number]
107
+ ): string | undefined => {
108
+ if (field.length === 0) {
109
+ return `${bounds.name} field is empty`;
110
+ }
111
+ const parts = field.split(',');
112
+ return parts.every((part) => isCronPartValid(part, bounds))
113
+ ? undefined
114
+ : `${bounds.name} field is not a supported cron expression`;
115
+ };
116
+
117
+ const validateCron = (cron: unknown): ScheduleValidationIssue[] => {
118
+ if (typeof cron !== 'string' || cron.trim().length === 0) {
119
+ return [
120
+ { field: 'cron', message: 'Cron expression must be a non-empty string' },
121
+ ];
122
+ }
123
+
124
+ const normalized = normalizeCron(cron);
125
+ const fields = normalized.split(' ');
126
+ if (fields.length !== CRON_FIELD_BOUNDS.length) {
127
+ return [
128
+ {
129
+ field: 'cron',
130
+ message: 'Cron expression must contain exactly five fields',
131
+ },
132
+ ];
133
+ }
134
+
135
+ return fields.flatMap((field, index) => {
136
+ const bounds = CRON_FIELD_BOUNDS[index];
137
+ if (!bounds) {
138
+ return [];
139
+ }
140
+ const issue = validateCronField(field, bounds);
141
+ return issue === undefined
142
+ ? []
143
+ : [{ field: 'cron' as const, message: issue }];
144
+ });
145
+ };
146
+
147
+ const isTimezoneValid = (timezone: string): boolean => {
148
+ try {
149
+ new Intl.DateTimeFormat('en-US', { timeZone: timezone }).format();
150
+ return true;
151
+ } catch {
152
+ return false;
153
+ }
154
+ };
155
+
156
+ const validateTimezone = (
157
+ timezone: unknown
158
+ ): readonly ScheduleValidationIssue[] => {
159
+ if (timezone === undefined) {
160
+ return [];
161
+ }
162
+ if (typeof timezone !== 'string' || timezone.trim().length === 0) {
163
+ return [
164
+ {
165
+ field: 'timezone',
166
+ message: 'Timezone must be a non-empty IANA timezone string',
167
+ },
168
+ ];
169
+ }
170
+ return isTimezoneValid(timezone.trim())
171
+ ? []
172
+ : [
173
+ {
174
+ field: 'timezone',
175
+ message: `Timezone "${timezone}" is not supported by Intl.DateTimeFormat`,
176
+ },
177
+ ];
178
+ };
179
+
180
+ const isNonJsonLeaf = (value: unknown): boolean => {
181
+ if (value === undefined) {
182
+ return true;
183
+ }
184
+ const kind = typeof value;
185
+ if (kind === 'function' || kind === 'symbol' || kind === 'bigint') {
186
+ return true;
187
+ }
188
+ return (
189
+ value instanceof Date ||
190
+ value instanceof RegExp ||
191
+ value instanceof Map ||
192
+ value instanceof Set
193
+ );
194
+ };
195
+
196
+ const containsNonSerializableLeaf = (
197
+ value: unknown,
198
+ seen: WeakSet<object>
199
+ ): boolean => {
200
+ if (isNonJsonLeaf(value)) {
201
+ return true;
202
+ }
203
+ if (value === null || typeof value !== 'object') {
204
+ return false;
205
+ }
206
+ if (seen.has(value)) {
207
+ return true;
208
+ }
209
+ seen.add(value);
210
+ if (Array.isArray(value)) {
211
+ return value.some((entry) => containsNonSerializableLeaf(entry, seen));
212
+ }
213
+ return Object.values(value).some((entry) =>
214
+ containsNonSerializableLeaf(entry, seen)
215
+ );
216
+ };
217
+
218
+ const validateInput = (input: unknown): readonly ScheduleValidationIssue[] => {
219
+ if (containsNonSerializableLeaf(input, new WeakSet<object>())) {
220
+ return [
221
+ {
222
+ field: 'input',
223
+ message: 'Schedule input must be JSON-serializable data',
224
+ },
225
+ ];
226
+ }
227
+ try {
228
+ JSON.stringify(input);
229
+ return [];
230
+ } catch {
231
+ return [
232
+ {
233
+ field: 'input',
234
+ message: 'Schedule input must be JSON-serializable data',
235
+ },
236
+ ];
237
+ }
238
+ };
239
+
240
+ const scheduleIssuesMessage = (
241
+ id: string,
242
+ issues: readonly ScheduleValidationIssue[]
243
+ ): string =>
244
+ `schedule("${id}") is invalid: ${issues.map((issue) => `${issue.field}: ${issue.message}`).join('; ')}`;
245
+
246
+ const assertScheduleSpec = (
247
+ id: string,
248
+ spec: ScheduleSpec
249
+ ): {
250
+ readonly cron: string;
251
+ readonly input: unknown;
252
+ readonly timezone?: string | undefined;
253
+ } => {
254
+ const input = spec.input === undefined ? EMPTY_INPUT : spec.input;
255
+ const timezone = spec.timezone?.trim();
256
+ const issues = [
257
+ ...validateCron(spec.cron),
258
+ ...validateTimezone(timezone),
259
+ ...validateInput(input),
260
+ ];
261
+
262
+ if (issues.length > 0) {
263
+ throw new ValidationError(scheduleIssuesMessage(id, issues), {
264
+ context: { issues },
265
+ });
266
+ }
267
+
268
+ return {
269
+ cron: normalizeCron(spec.cron),
270
+ input,
271
+ ...(timezone === undefined ? {} : { timezone }),
272
+ };
273
+ };
274
+
275
+ export const validateScheduleSource = (
276
+ source: ActivationSource
277
+ ): readonly ScheduleValidationIssue[] => {
278
+ if (source.kind !== 'schedule') {
279
+ return [];
280
+ }
281
+ const input = Object.hasOwn(source, 'input') ? source.input : EMPTY_INPUT;
282
+ return [
283
+ ...validateCron(source.cron),
284
+ ...validateTimezone(source.timezone),
285
+ ...validateInput(input),
286
+ ];
287
+ };
288
+
289
+ export function schedule<TInput>(
290
+ id: string,
291
+ spec: ScheduleSpec<TInput> & { readonly input: TInput }
292
+ ): ScheduleSource<TInput>;
293
+ export function schedule(
294
+ id: string,
295
+ spec: ScheduleSpec
296
+ ): ScheduleSource<ScheduleInputDefault>;
297
+ export function schedule<TInput>(
298
+ spec: ScheduleSpec<TInput> & { readonly id: string; readonly input: TInput }
299
+ ): ScheduleSource<TInput>;
300
+ export function schedule(
301
+ spec: ScheduleSpec & { readonly id: string }
302
+ ): ScheduleSource<ScheduleInputDefault>;
303
+ export function schedule<TInput>(
304
+ idOrSpec: string | (ScheduleSpec<TInput> & { readonly id: string }),
305
+ maybeSpec?: ScheduleSpec<TInput>
306
+ ): ScheduleSource<TInput | ScheduleInputDefault> {
307
+ const id = typeof idOrSpec === 'string' ? idOrSpec : idOrSpec.id;
308
+ // oxlint-disable-next-line no-non-null-assertion -- overload guarantees maybeSpec when idOrSpec is string
309
+ const spec = typeof idOrSpec === 'string' ? maybeSpec! : idOrSpec;
310
+ const normalized = assertScheduleSpec(id, spec);
311
+
312
+ return Object.freeze({
313
+ cron: normalized.cron,
314
+ id,
315
+ input: normalized.input as TInput | ScheduleInputDefault,
316
+ kind: 'schedule' as const,
317
+ ...(spec.meta === undefined
318
+ ? {}
319
+ : { meta: Object.freeze({ ...spec.meta }) }),
320
+ ...(normalized.timezone === undefined
321
+ ? {}
322
+ : { timezone: normalized.timezone }),
323
+ });
324
+ }
@@ -7,21 +7,25 @@
7
7
 
8
8
  import type { ErrorCategory, TrailsError } from './errors.js';
9
9
  import {
10
+ AuthError,
11
+ CancelledError,
12
+ ConflictError,
10
13
  ValidationError,
11
- AmbiguousError,
12
- AssertionError,
14
+ InternalError,
15
+ NetworkError,
13
16
  NotFoundError,
14
- AlreadyExistsError,
15
- ConflictError,
16
17
  PermissionError,
17
- TimeoutError,
18
18
  RateLimitError,
19
- NetworkError,
20
- InternalError,
21
- AuthError,
22
- CancelledError,
19
+ RetryExhaustedError,
20
+ TimeoutError,
21
+ errorClasses,
23
22
  isTrailsError,
24
23
  } from './errors.js';
24
+ import {
25
+ redactErrorContext,
26
+ redactErrorStack,
27
+ redactErrorString,
28
+ } from './error-projection.js';
25
29
  import { Result } from './result.js';
26
30
 
27
31
  // ---------------------------------------------------------------------------
@@ -31,7 +35,10 @@ import { Result } from './result.js';
31
35
  export interface SerializedError {
32
36
  readonly name: string;
33
37
  readonly message: string;
38
+ readonly attempts?: number | undefined;
34
39
  readonly category?: ErrorCategory | undefined;
40
+ readonly cause?: SerializedError | undefined;
41
+ readonly detour?: string | undefined;
35
42
  readonly retryable?: boolean | undefined;
36
43
  readonly retryAfter?: number | undefined;
37
44
  readonly context?: Record<string, unknown> | undefined;
@@ -56,10 +63,15 @@ const buildOpts = (
56
63
 
57
64
  type ErrorFactory = (
58
65
  message: string,
59
- opts: { context?: Record<string, unknown> },
66
+ opts: { cause?: Error; context?: Record<string, unknown> },
60
67
  retryAfter: number | undefined
61
68
  ) => TrailsError;
62
69
 
70
+ type FixedErrorConstructor = new (
71
+ message: string,
72
+ options?: { cause?: Error; context?: Record<string, unknown> }
73
+ ) => TrailsError;
74
+
63
75
  const errorFactories: Record<ErrorCategory, ErrorFactory> = {
64
76
  auth: (msg, opts) => new AuthError(msg, opts),
65
77
  cancelled: (msg, opts) => new CancelledError(msg, opts),
@@ -92,30 +104,42 @@ const createErrorByCategory = (
92
104
  return factory(message, opts, retryAfter);
93
105
  };
94
106
 
95
- /** Map error class names to their constructors for precise round-tripping. */
96
- const errorConstructorsByName: Record<string, ErrorFactory> = {
97
- AlreadyExistsError: (msg, opts) => new AlreadyExistsError(msg, opts),
98
- AmbiguousError: (msg, opts) => new AmbiguousError(msg, opts),
99
- AssertionError: (msg, opts) => new AssertionError(msg, opts),
100
- AuthError: (msg, opts) => new AuthError(msg, opts),
101
- CancelledError: (msg, opts) => new CancelledError(msg, opts),
102
- ConflictError: (msg, opts) => new ConflictError(msg, opts),
103
- InternalError: (msg, opts) => new InternalError(msg, opts),
104
- NetworkError: (msg, opts) => new NetworkError(msg, opts),
105
- NotFoundError: (msg, opts) => new NotFoundError(msg, opts),
106
- PermissionError: (msg, opts) => new PermissionError(msg, opts),
107
- RateLimitError: (msg, opts, retryAfter) => {
108
- const rlOpts: { context?: Record<string, unknown>; retryAfter?: number } = {
109
- ...opts,
110
- };
111
- if (retryAfter !== undefined) {
112
- rlOpts.retryAfter = retryAfter;
113
- }
114
- return new RateLimitError(msg, rlOpts);
115
- },
116
- TimeoutError: (msg, opts) => new TimeoutError(msg, opts),
117
- ValidationError: (msg, opts) => new ValidationError(msg, opts),
118
- };
107
+ /** Map fixed error class names to constructors for precise round-tripping. */
108
+ const errorConstructorsByName: Readonly<Record<string, ErrorFactory>> =
109
+ Object.fromEntries(
110
+ errorClasses.flatMap((entry): [string, ErrorFactory][] => {
111
+ if (entry.category === 'dynamic') {
112
+ return [];
113
+ }
114
+ const ctor = entry.ctor as FixedErrorConstructor;
115
+ return [
116
+ [
117
+ entry.name,
118
+ (message, opts, retryAfter) => {
119
+ if (ctor === RateLimitError) {
120
+ const rateLimitOptions:
121
+ | {
122
+ cause?: Error;
123
+ context?: Record<string, unknown>;
124
+ retryAfter?: number;
125
+ }
126
+ | undefined =
127
+ opts.context === undefined &&
128
+ opts.cause === undefined &&
129
+ retryAfter === undefined
130
+ ? undefined
131
+ : {
132
+ ...opts,
133
+ ...(retryAfter === undefined ? {} : { retryAfter }),
134
+ };
135
+ return new RateLimitError(message, rateLimitOptions);
136
+ }
137
+ return new ctor(message, opts);
138
+ },
139
+ ],
140
+ ];
141
+ })
142
+ );
119
143
 
120
144
  // ---------------------------------------------------------------------------
121
145
  // Error serialization
@@ -124,16 +148,23 @@ const errorConstructorsByName: Record<string, ErrorFactory> = {
124
148
  /** Extract structured data from an Error for transport. */
125
149
  export const serializeError = (error: Error): SerializedError => {
126
150
  const result: SerializedError = {
127
- message: error.message,
151
+ message: redactErrorString(error.message),
128
152
  name: error.name,
129
- stack: error.stack,
153
+ stack: redactErrorStack(error.stack),
130
154
  };
131
155
 
132
156
  if (isTrailsError(error)) {
133
157
  return {
134
158
  ...result,
135
159
  category: error.category,
136
- context: error.context,
160
+ context: redactErrorContext(error.context),
161
+ ...(error instanceof RetryExhaustedError
162
+ ? {
163
+ attempts: error.attempts,
164
+ cause: serializeError(error.cause),
165
+ detour: error.detour,
166
+ }
167
+ : {}),
137
168
  retryAfter:
138
169
  error instanceof RateLimitError ? error.retryAfter : undefined,
139
170
  retryable: error.retryable,
@@ -146,6 +177,28 @@ export const serializeError = (error: Error): SerializedError => {
146
177
  /** Reconstruct a TrailsError from serialized data. */
147
178
  export const deserializeError = (data: SerializedError): TrailsError => {
148
179
  const opts = buildOpts(data.context);
180
+ if (data.name === 'RetryExhaustedError') {
181
+ const wrapped =
182
+ data.cause === undefined
183
+ ? createErrorByCategory(
184
+ data.category ?? 'internal',
185
+ data.message,
186
+ data.context,
187
+ data.retryAfter
188
+ )
189
+ : deserializeError(data.cause);
190
+ const error = new RetryExhaustedError(wrapped, {
191
+ attempts: data.attempts ?? 0,
192
+ detour: data.detour ?? 'unknown',
193
+ });
194
+ if (data.message !== error.message) {
195
+ error.message = data.message;
196
+ }
197
+ if (data.stack) {
198
+ error.stack = data.stack;
199
+ }
200
+ return error;
201
+ }
149
202
  const nameFactory = errorConstructorsByName[data.name];
150
203
 
151
204
  const error = nameFactory
@@ -164,24 +217,6 @@ export const deserializeError = (data: SerializedError): TrailsError => {
164
217
  return error;
165
218
  };
166
219
 
167
- // ---------------------------------------------------------------------------
168
- // Safe JSON
169
- // ---------------------------------------------------------------------------
170
-
171
- /** Parse a JSON string, returning a Result instead of throwing. */
172
- export const safeParse = (json: string): Result<unknown, ValidationError> => {
173
- try {
174
- return Result.ok(JSON.parse(json) as unknown);
175
- } catch (error) {
176
- return Result.err(
177
- new ValidationError('Invalid JSON', {
178
- cause: error instanceof Error ? error : new Error(String(error)),
179
- context: { input: json.slice(0, 200) },
180
- })
181
- );
182
- }
183
- };
184
-
185
220
  /** Stringify a value, returning a Result. Handles circular references. */
186
221
  export const safeStringify = (
187
222
  value: unknown