@ontrails/core 1.0.0-beta.15 → 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 (348) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +61 -16
  3. package/package.json +9 -8
  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/context.ts +4 -0
  10. package/src/{internal/cross-batch.ts → cross-batch.ts} +3 -11
  11. package/src/detours.ts +8 -0
  12. package/src/draft.ts +4 -0
  13. package/src/error-projection.ts +51 -0
  14. package/src/errors.ts +192 -39
  15. package/src/execute.ts +443 -44
  16. package/src/fire.ts +965 -91
  17. package/src/index.ts +285 -38
  18. package/src/layer-projection.ts +192 -0
  19. package/src/layer.ts +32 -1
  20. package/src/observe.ts +361 -0
  21. package/src/permits.ts +12 -0
  22. package/src/redaction/patterns.ts +6 -3
  23. package/src/resilience.ts +8 -8
  24. package/src/resource-config.ts +477 -44
  25. package/src/resource.ts +21 -1
  26. package/src/result.ts +18 -0
  27. package/src/run.ts +2 -2
  28. package/src/schedule-runtime.ts +689 -0
  29. package/src/schedule.ts +324 -0
  30. package/src/serialization.ts +90 -57
  31. package/src/signal-diagnostics.ts +633 -0
  32. package/src/{internal/signal-ref.ts → signal-ref.ts} +1 -1
  33. package/src/signal.ts +37 -0
  34. package/src/store/accessor-protocol.ts +5 -5
  35. package/src/structured-examples.ts +245 -0
  36. package/src/surface-derivation.ts +91 -0
  37. package/src/surface-filter.ts +7 -13
  38. package/src/topo.ts +386 -6
  39. package/src/tracing.ts +345 -0
  40. package/src/trail.ts +168 -22
  41. package/src/trails/derive-trail.ts +1 -1
  42. package/src/trails/ingest.ts +7 -15
  43. package/src/{internal/trails-db.ts → trails-db.ts} +42 -21
  44. package/src/transport-error-map.ts +146 -28
  45. package/src/types.ts +82 -28
  46. package/src/validate-established-topo.ts +6 -0
  47. package/src/validate-topo.ts +212 -5
  48. package/src/validation.ts +58 -1
  49. package/src/webhook.ts +276 -0
  50. package/src/{internal/zod-wrappers.ts → zod-wrappers.ts} +0 -6
  51. package/.turbo/turbo-build.log +0 -1
  52. package/.turbo/turbo-lint.log +0 -3
  53. package/.turbo/turbo-typecheck.log +0 -1
  54. package/dist/blob-ref.d.ts +0 -20
  55. package/dist/blob-ref.d.ts.map +0 -1
  56. package/dist/blob-ref.js +0 -22
  57. package/dist/blob-ref.js.map +0 -1
  58. package/dist/branded.d.ts +0 -36
  59. package/dist/branded.d.ts.map +0 -1
  60. package/dist/branded.js +0 -89
  61. package/dist/branded.js.map +0 -1
  62. package/dist/collections.d.ts +0 -31
  63. package/dist/collections.d.ts.map +0 -1
  64. package/dist/collections.js +0 -60
  65. package/dist/collections.js.map +0 -1
  66. package/dist/context.d.ts +0 -22
  67. package/dist/context.d.ts.map +0 -1
  68. package/dist/context.js +0 -37
  69. package/dist/context.js.map +0 -1
  70. package/dist/contour.d.ts +0 -81
  71. package/dist/contour.d.ts.map +0 -1
  72. package/dist/contour.js +0 -176
  73. package/dist/contour.js.map +0 -1
  74. package/dist/cross-schema.d.ts +0 -18
  75. package/dist/cross-schema.d.ts.map +0 -1
  76. package/dist/cross-schema.js +0 -29
  77. package/dist/cross-schema.js.map +0 -1
  78. package/dist/derive.d.ts +0 -44
  79. package/dist/derive.d.ts.map +0 -1
  80. package/dist/derive.js +0 -149
  81. package/dist/derive.js.map +0 -1
  82. package/dist/draft.d.ts +0 -28
  83. package/dist/draft.d.ts.map +0 -1
  84. package/dist/draft.js +0 -157
  85. package/dist/draft.js.map +0 -1
  86. package/dist/errors.d.ts +0 -141
  87. package/dist/errors.d.ts.map +0 -1
  88. package/dist/errors.js +0 -185
  89. package/dist/errors.js.map +0 -1
  90. package/dist/execute.d.ts +0 -52
  91. package/dist/execute.d.ts.map +0 -1
  92. package/dist/execute.js +0 -511
  93. package/dist/execute.js.map +0 -1
  94. package/dist/fetch.d.ts +0 -15
  95. package/dist/fetch.d.ts.map +0 -1
  96. package/dist/fetch.js +0 -102
  97. package/dist/fetch.js.map +0 -1
  98. package/dist/fire.d.ts +0 -43
  99. package/dist/fire.d.ts.map +0 -1
  100. package/dist/fire.js +0 -185
  101. package/dist/fire.js.map +0 -1
  102. package/dist/guards.d.ts +0 -17
  103. package/dist/guards.d.ts.map +0 -1
  104. package/dist/guards.js +0 -25
  105. package/dist/guards.js.map +0 -1
  106. package/dist/index.d.ts +0 -59
  107. package/dist/index.d.ts.map +0 -1
  108. package/dist/index.js +0 -61
  109. package/dist/index.js.map +0 -1
  110. package/dist/internal/cross-batch.d.ts +0 -44
  111. package/dist/internal/cross-batch.d.ts.map +0 -1
  112. package/dist/internal/cross-batch.js +0 -58
  113. package/dist/internal/cross-batch.js.map +0 -1
  114. package/dist/internal/fork-ctx.d.ts +0 -41
  115. package/dist/internal/fork-ctx.d.ts.map +0 -1
  116. package/dist/internal/fork-ctx.js +0 -47
  117. package/dist/internal/fork-ctx.js.map +0 -1
  118. package/dist/internal/signal-ref.d.ts +0 -15
  119. package/dist/internal/signal-ref.d.ts.map +0 -1
  120. package/dist/internal/signal-ref.js +0 -41
  121. package/dist/internal/signal-ref.js.map +0 -1
  122. package/dist/internal/topo-saves.d.ts +0 -53
  123. package/dist/internal/topo-saves.d.ts.map +0 -1
  124. package/dist/internal/topo-saves.js +0 -449
  125. package/dist/internal/topo-saves.js.map +0 -1
  126. package/dist/internal/topo-snapshots.d.ts +0 -57
  127. package/dist/internal/topo-snapshots.d.ts.map +0 -1
  128. package/dist/internal/topo-snapshots.js +0 -372
  129. package/dist/internal/topo-snapshots.js.map +0 -1
  130. package/dist/internal/topo-store-read.d.ts +0 -70
  131. package/dist/internal/topo-store-read.d.ts.map +0 -1
  132. package/dist/internal/topo-store-read.js +0 -221
  133. package/dist/internal/topo-store-read.js.map +0 -1
  134. package/dist/internal/topo-store.d.ts +0 -26
  135. package/dist/internal/topo-store.d.ts.map +0 -1
  136. package/dist/internal/topo-store.js +0 -621
  137. package/dist/internal/topo-store.js.map +0 -1
  138. package/dist/internal/tracing.d.ts +0 -98
  139. package/dist/internal/tracing.d.ts.map +0 -1
  140. package/dist/internal/tracing.js +0 -102
  141. package/dist/internal/tracing.js.map +0 -1
  142. package/dist/internal/trails-db.d.ts +0 -16
  143. package/dist/internal/trails-db.d.ts.map +0 -1
  144. package/dist/internal/trails-db.js +0 -118
  145. package/dist/internal/trails-db.js.map +0 -1
  146. package/dist/internal/zod-wrappers.d.ts +0 -40
  147. package/dist/internal/zod-wrappers.d.ts.map +0 -1
  148. package/dist/internal/zod-wrappers.js +0 -59
  149. package/dist/internal/zod-wrappers.js.map +0 -1
  150. package/dist/layer.d.ts +0 -23
  151. package/dist/layer.d.ts.map +0 -1
  152. package/dist/layer.js +0 -21
  153. package/dist/layer.js.map +0 -1
  154. package/dist/path-security.d.ts +0 -28
  155. package/dist/path-security.d.ts.map +0 -1
  156. package/dist/path-security.js +0 -63
  157. package/dist/path-security.js.map +0 -1
  158. package/dist/patterns/bulk.d.ts +0 -15
  159. package/dist/patterns/bulk.d.ts.map +0 -1
  160. package/dist/patterns/bulk.js +0 -14
  161. package/dist/patterns/bulk.js.map +0 -1
  162. package/dist/patterns/change.d.ts +0 -10
  163. package/dist/patterns/change.d.ts.map +0 -1
  164. package/dist/patterns/change.js +0 -10
  165. package/dist/patterns/change.js.map +0 -1
  166. package/dist/patterns/date-range.d.ts +0 -10
  167. package/dist/patterns/date-range.d.ts.map +0 -1
  168. package/dist/patterns/date-range.js +0 -10
  169. package/dist/patterns/date-range.js.map +0 -1
  170. package/dist/patterns/index.d.ts +0 -9
  171. package/dist/patterns/index.d.ts.map +0 -1
  172. package/dist/patterns/index.js +0 -9
  173. package/dist/patterns/index.js.map +0 -1
  174. package/dist/patterns/pagination.d.ts +0 -18
  175. package/dist/patterns/pagination.d.ts.map +0 -1
  176. package/dist/patterns/pagination.js +0 -18
  177. package/dist/patterns/pagination.js.map +0 -1
  178. package/dist/patterns/progress.d.ts +0 -11
  179. package/dist/patterns/progress.d.ts.map +0 -1
  180. package/dist/patterns/progress.js +0 -11
  181. package/dist/patterns/progress.js.map +0 -1
  182. package/dist/patterns/sorting.d.ts +0 -13
  183. package/dist/patterns/sorting.d.ts.map +0 -1
  184. package/dist/patterns/sorting.js +0 -10
  185. package/dist/patterns/sorting.js.map +0 -1
  186. package/dist/patterns/status.d.ts +0 -15
  187. package/dist/patterns/status.d.ts.map +0 -1
  188. package/dist/patterns/status.js +0 -9
  189. package/dist/patterns/status.js.map +0 -1
  190. package/dist/patterns/timestamps.d.ts +0 -10
  191. package/dist/patterns/timestamps.d.ts.map +0 -1
  192. package/dist/patterns/timestamps.js +0 -10
  193. package/dist/patterns/timestamps.js.map +0 -1
  194. package/dist/redaction/index.d.ts +0 -4
  195. package/dist/redaction/index.d.ts.map +0 -1
  196. package/dist/redaction/index.js +0 -3
  197. package/dist/redaction/index.js.map +0 -1
  198. package/dist/redaction/patterns.d.ts +0 -9
  199. package/dist/redaction/patterns.d.ts.map +0 -1
  200. package/dist/redaction/patterns.js +0 -39
  201. package/dist/redaction/patterns.js.map +0 -1
  202. package/dist/redaction/redactor.d.ts +0 -27
  203. package/dist/redaction/redactor.d.ts.map +0 -1
  204. package/dist/redaction/redactor.js +0 -89
  205. package/dist/redaction/redactor.js.map +0 -1
  206. package/dist/resilience.d.ts +0 -34
  207. package/dist/resilience.d.ts.map +0 -1
  208. package/dist/resilience.js +0 -164
  209. package/dist/resilience.js.map +0 -1
  210. package/dist/resource-config.d.ts +0 -22
  211. package/dist/resource-config.d.ts.map +0 -1
  212. package/dist/resource-config.js +0 -210
  213. package/dist/resource-config.js.map +0 -1
  214. package/dist/resource.d.ts +0 -74
  215. package/dist/resource.d.ts.map +0 -1
  216. package/dist/resource.js +0 -61
  217. package/dist/resource.js.map +0 -1
  218. package/dist/result.d.ts +0 -57
  219. package/dist/result.d.ts.map +0 -1
  220. package/dist/result.js +0 -156
  221. package/dist/result.js.map +0 -1
  222. package/dist/run.d.ts +0 -28
  223. package/dist/run.d.ts.map +0 -1
  224. package/dist/run.js +0 -35
  225. package/dist/run.js.map +0 -1
  226. package/dist/serialization.d.ts +0 -27
  227. package/dist/serialization.d.ts.map +0 -1
  228. package/dist/serialization.js +0 -154
  229. package/dist/serialization.js.map +0 -1
  230. package/dist/signal.d.ts +0 -33
  231. package/dist/signal.d.ts.map +0 -1
  232. package/dist/signal.js +0 -17
  233. package/dist/signal.js.map +0 -1
  234. package/dist/store/accessor-protocol.d.ts +0 -51
  235. package/dist/store/accessor-protocol.d.ts.map +0 -1
  236. package/dist/store/accessor-protocol.js +0 -2
  237. package/dist/store/accessor-protocol.js.map +0 -1
  238. package/dist/store/index.d.ts +0 -2
  239. package/dist/store/index.d.ts.map +0 -1
  240. package/dist/store/index.js +0 -2
  241. package/dist/store/index.js.map +0 -1
  242. package/dist/surface-filter.d.ts +0 -13
  243. package/dist/surface-filter.d.ts.map +0 -1
  244. package/dist/surface-filter.js +0 -76
  245. package/dist/surface-filter.js.map +0 -1
  246. package/dist/topo-store.d.ts +0 -62
  247. package/dist/topo-store.d.ts.map +0 -1
  248. package/dist/topo-store.js +0 -319
  249. package/dist/topo-store.js.map +0 -1
  250. package/dist/topo.d.ts +0 -39
  251. package/dist/topo.d.ts.map +0 -1
  252. package/dist/topo.js +0 -241
  253. package/dist/topo.js.map +0 -1
  254. package/dist/trail.d.ts +0 -163
  255. package/dist/trail.d.ts.map +0 -1
  256. package/dist/trail.js +0 -75
  257. package/dist/trail.js.map +0 -1
  258. package/dist/trails/derive-trail.d.ts +0 -66
  259. package/dist/trails/derive-trail.d.ts.map +0 -1
  260. package/dist/trails/derive-trail.js +0 -378
  261. package/dist/trails/derive-trail.js.map +0 -1
  262. package/dist/trails/index.d.ts +0 -5
  263. package/dist/trails/index.d.ts.map +0 -1
  264. package/dist/trails/index.js +0 -3
  265. package/dist/trails/index.js.map +0 -1
  266. package/dist/trails/ingest.d.ts +0 -24
  267. package/dist/trails/ingest.d.ts.map +0 -1
  268. package/dist/trails/ingest.js +0 -63
  269. package/dist/trails/ingest.js.map +0 -1
  270. package/dist/transport-error-map.d.ts +0 -106
  271. package/dist/transport-error-map.d.ts.map +0 -1
  272. package/dist/transport-error-map.js +0 -24
  273. package/dist/transport-error-map.js.map +0 -1
  274. package/dist/type-checks.test-d.d.ts +0 -71
  275. package/dist/type-checks.test-d.d.ts.map +0 -1
  276. package/dist/type-checks.test-d.js +0 -12
  277. package/dist/type-checks.test-d.js.map +0 -1
  278. package/dist/type-utils.d.ts +0 -33
  279. package/dist/type-utils.d.ts.map +0 -1
  280. package/dist/type-utils.js +0 -12
  281. package/dist/type-utils.js.map +0 -1
  282. package/dist/types.d.ts +0 -177
  283. package/dist/types.d.ts.map +0 -1
  284. package/dist/types.js +0 -3
  285. package/dist/types.js.map +0 -1
  286. package/dist/validate-established-topo.d.ts +0 -76
  287. package/dist/validate-established-topo.d.ts.map +0 -1
  288. package/dist/validate-established-topo.js +0 -43
  289. package/dist/validate-established-topo.js.map +0 -1
  290. package/dist/validate-topo.d.ts +0 -24
  291. package/dist/validate-topo.d.ts.map +0 -1
  292. package/dist/validate-topo.js +0 -193
  293. package/dist/validate-topo.js.map +0 -1
  294. package/dist/validation.d.ts +0 -27
  295. package/dist/validation.d.ts.map +0 -1
  296. package/dist/validation.js +0 -165
  297. package/dist/validation.js.map +0 -1
  298. package/dist/workspace.d.ts +0 -25
  299. package/dist/workspace.d.ts.map +0 -1
  300. package/dist/workspace.js +0 -57
  301. package/dist/workspace.js.map +0 -1
  302. package/src/__tests__/blob-ref.test.ts +0 -103
  303. package/src/__tests__/branded.test.ts +0 -148
  304. package/src/__tests__/collections.test.ts +0 -126
  305. package/src/__tests__/context.test.ts +0 -77
  306. package/src/__tests__/contour.test.ts +0 -263
  307. package/src/__tests__/derive-trail.test.ts +0 -668
  308. package/src/__tests__/derive.test.ts +0 -260
  309. package/src/__tests__/errors.test.ts +0 -427
  310. package/src/__tests__/execute.test.ts +0 -1563
  311. package/src/__tests__/fetch.test.ts +0 -217
  312. package/src/__tests__/fire.test.ts +0 -1056
  313. package/src/__tests__/fork-ctx.test.ts +0 -83
  314. package/src/__tests__/guards.test.ts +0 -102
  315. package/src/__tests__/ingest.test.ts +0 -191
  316. package/src/__tests__/layer.test.ts +0 -125
  317. package/src/__tests__/path-security.test.ts +0 -114
  318. package/src/__tests__/patterns.test.ts +0 -273
  319. package/src/__tests__/redaction.test.ts +0 -244
  320. package/src/__tests__/resilience.test.ts +0 -246
  321. package/src/__tests__/resource.test.ts +0 -204
  322. package/src/__tests__/result.test.ts +0 -155
  323. package/src/__tests__/run.test.ts +0 -181
  324. package/src/__tests__/serialization.test.ts +0 -401
  325. package/src/__tests__/service-config.test.ts +0 -224
  326. package/src/__tests__/signal.test.ts +0 -82
  327. package/src/__tests__/surface-filter.test.ts +0 -196
  328. package/src/__tests__/topo-store-read.test.ts +0 -256
  329. package/src/__tests__/topo-store.test.ts +0 -869
  330. package/src/__tests__/topo.test.ts +0 -573
  331. package/src/__tests__/trail-permit.test.ts +0 -60
  332. package/src/__tests__/trail.test.ts +0 -426
  333. package/src/__tests__/trails-db.test.ts +0 -198
  334. package/src/__tests__/transport-error-map.test.ts +0 -82
  335. package/src/__tests__/type-utils.test.ts +0 -118
  336. package/src/__tests__/validate-topo.test.ts +0 -611
  337. package/src/__tests__/validation.test.ts +0 -336
  338. package/src/__tests__/workspace.test.ts +0 -183
  339. package/src/__tests__/zod-wrappers.test.ts +0 -62
  340. package/src/internal/topo-snapshots.ts +0 -517
  341. package/src/internal/topo-store-read.ts +0 -481
  342. package/src/internal/topo-store.ts +0 -1192
  343. package/src/internal/tracing.ts +0 -187
  344. package/src/topo-store.ts +0 -510
  345. package/src/type-checks.test-d.ts +0 -106
  346. package/tsconfig.json +0 -9
  347. package/tsconfig.tests.json +0 -10
  348. 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,22 +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
- DerivationError,
22
- AuthError,
23
- CancelledError,
19
+ RetryExhaustedError,
20
+ TimeoutError,
21
+ errorClasses,
24
22
  isTrailsError,
25
23
  } from './errors.js';
24
+ import {
25
+ redactErrorContext,
26
+ redactErrorStack,
27
+ redactErrorString,
28
+ } from './error-projection.js';
26
29
  import { Result } from './result.js';
27
30
 
28
31
  // ---------------------------------------------------------------------------
@@ -32,7 +35,10 @@ import { Result } from './result.js';
32
35
  export interface SerializedError {
33
36
  readonly name: string;
34
37
  readonly message: string;
38
+ readonly attempts?: number | undefined;
35
39
  readonly category?: ErrorCategory | undefined;
40
+ readonly cause?: SerializedError | undefined;
41
+ readonly detour?: string | undefined;
36
42
  readonly retryable?: boolean | undefined;
37
43
  readonly retryAfter?: number | undefined;
38
44
  readonly context?: Record<string, unknown> | undefined;
@@ -57,10 +63,15 @@ const buildOpts = (
57
63
 
58
64
  type ErrorFactory = (
59
65
  message: string,
60
- opts: { context?: Record<string, unknown> },
66
+ opts: { cause?: Error; context?: Record<string, unknown> },
61
67
  retryAfter: number | undefined
62
68
  ) => TrailsError;
63
69
 
70
+ type FixedErrorConstructor = new (
71
+ message: string,
72
+ options?: { cause?: Error; context?: Record<string, unknown> }
73
+ ) => TrailsError;
74
+
64
75
  const errorFactories: Record<ErrorCategory, ErrorFactory> = {
65
76
  auth: (msg, opts) => new AuthError(msg, opts),
66
77
  cancelled: (msg, opts) => new CancelledError(msg, opts),
@@ -93,31 +104,42 @@ const createErrorByCategory = (
93
104
  return factory(message, opts, retryAfter);
94
105
  };
95
106
 
96
- /** Map error class names to their constructors for precise round-tripping. */
97
- const errorConstructorsByName: Record<string, ErrorFactory> = {
98
- AlreadyExistsError: (msg, opts) => new AlreadyExistsError(msg, opts),
99
- AmbiguousError: (msg, opts) => new AmbiguousError(msg, opts),
100
- AssertionError: (msg, opts) => new AssertionError(msg, opts),
101
- AuthError: (msg, opts) => new AuthError(msg, opts),
102
- CancelledError: (msg, opts) => new CancelledError(msg, opts),
103
- ConflictError: (msg, opts) => new ConflictError(msg, opts),
104
- DerivationError: (msg, opts) => new DerivationError(msg, opts),
105
- InternalError: (msg, opts) => new InternalError(msg, opts),
106
- NetworkError: (msg, opts) => new NetworkError(msg, opts),
107
- NotFoundError: (msg, opts) => new NotFoundError(msg, opts),
108
- PermissionError: (msg, opts) => new PermissionError(msg, opts),
109
- RateLimitError: (msg, opts, retryAfter) => {
110
- const rlOpts: { context?: Record<string, unknown>; retryAfter?: number } = {
111
- ...opts,
112
- };
113
- if (retryAfter !== undefined) {
114
- rlOpts.retryAfter = retryAfter;
115
- }
116
- return new RateLimitError(msg, rlOpts);
117
- },
118
- TimeoutError: (msg, opts) => new TimeoutError(msg, opts),
119
- ValidationError: (msg, opts) => new ValidationError(msg, opts),
120
- };
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
+ );
121
143
 
122
144
  // ---------------------------------------------------------------------------
123
145
  // Error serialization
@@ -126,16 +148,23 @@ const errorConstructorsByName: Record<string, ErrorFactory> = {
126
148
  /** Extract structured data from an Error for transport. */
127
149
  export const serializeError = (error: Error): SerializedError => {
128
150
  const result: SerializedError = {
129
- message: error.message,
151
+ message: redactErrorString(error.message),
130
152
  name: error.name,
131
- stack: error.stack,
153
+ stack: redactErrorStack(error.stack),
132
154
  };
133
155
 
134
156
  if (isTrailsError(error)) {
135
157
  return {
136
158
  ...result,
137
159
  category: error.category,
138
- 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
+ : {}),
139
168
  retryAfter:
140
169
  error instanceof RateLimitError ? error.retryAfter : undefined,
141
170
  retryable: error.retryable,
@@ -148,6 +177,28 @@ export const serializeError = (error: Error): SerializedError => {
148
177
  /** Reconstruct a TrailsError from serialized data. */
149
178
  export const deserializeError = (data: SerializedError): TrailsError => {
150
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
+ }
151
202
  const nameFactory = errorConstructorsByName[data.name];
152
203
 
153
204
  const error = nameFactory
@@ -166,24 +217,6 @@ export const deserializeError = (data: SerializedError): TrailsError => {
166
217
  return error;
167
218
  };
168
219
 
169
- // ---------------------------------------------------------------------------
170
- // Safe JSON
171
- // ---------------------------------------------------------------------------
172
-
173
- /** Parse a JSON string, returning a Result instead of throwing. */
174
- export const safeParse = (json: string): Result<unknown, ValidationError> => {
175
- try {
176
- return Result.ok(JSON.parse(json) as unknown);
177
- } catch (error) {
178
- return Result.err(
179
- new ValidationError('Invalid JSON', {
180
- cause: error instanceof Error ? error : new Error(String(error)),
181
- context: { input: json.slice(0, 200) },
182
- })
183
- );
184
- }
185
- };
186
-
187
220
  /** Stringify a value, returning a Result. Handles circular references. */
188
221
  export const safeStringify = (
189
222
  value: unknown