@corgelabs/corge 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/dist/ai/index.d.ts +3 -0
  2. package/dist/ai/index.d.ts.map +1 -0
  3. package/dist/ai/index.js +3 -0
  4. package/dist/ai/index.js.map +1 -0
  5. package/dist/ai/schema.d.ts +83 -0
  6. package/dist/ai/schema.d.ts.map +1 -0
  7. package/dist/ai/schema.js +19 -0
  8. package/dist/ai/schema.js.map +1 -0
  9. package/dist/ai/service.d.ts +14 -0
  10. package/dist/ai/service.d.ts.map +1 -0
  11. package/dist/ai/service.js +266 -0
  12. package/dist/ai/service.js.map +1 -0
  13. package/dist/ai/utils.d.ts +10 -0
  14. package/dist/ai/utils.d.ts.map +1 -0
  15. package/dist/ai/utils.js +27 -0
  16. package/dist/ai/utils.js.map +1 -0
  17. package/dist/attachments/index.d.ts +4 -0
  18. package/dist/attachments/index.d.ts.map +1 -0
  19. package/dist/attachments/index.js +4 -0
  20. package/dist/attachments/index.js.map +1 -0
  21. package/dist/attachments/routes.d.ts +75 -0
  22. package/dist/attachments/routes.d.ts.map +1 -0
  23. package/dist/attachments/routes.js +18 -0
  24. package/dist/attachments/routes.js.map +1 -0
  25. package/dist/attachments/schema.d.ts +61 -0
  26. package/dist/attachments/schema.d.ts.map +1 -0
  27. package/dist/attachments/schema.js +30 -0
  28. package/dist/attachments/schema.js.map +1 -0
  29. package/dist/attachments/service.d.ts +27 -0
  30. package/dist/attachments/service.d.ts.map +1 -0
  31. package/dist/attachments/service.js +224 -0
  32. package/dist/attachments/service.js.map +1 -0
  33. package/dist/attachments/utils.d.ts +9 -0
  34. package/dist/attachments/utils.d.ts.map +1 -0
  35. package/dist/attachments/utils.js +80 -0
  36. package/dist/attachments/utils.js.map +1 -0
  37. package/dist/auth/index.d.ts +4 -0
  38. package/dist/auth/index.d.ts.map +1 -0
  39. package/dist/auth/index.js +4 -0
  40. package/dist/auth/index.js.map +1 -0
  41. package/dist/auth/oidc.d.ts +25 -0
  42. package/dist/auth/oidc.d.ts.map +1 -0
  43. package/dist/auth/oidc.js +298 -0
  44. package/dist/auth/oidc.js.map +1 -0
  45. package/dist/auth/recovery.d.ts +41 -0
  46. package/dist/auth/recovery.d.ts.map +1 -0
  47. package/dist/auth/recovery.js +250 -0
  48. package/dist/auth/recovery.js.map +1 -0
  49. package/dist/auth/routes.d.ts +226 -0
  50. package/dist/auth/routes.d.ts.map +1 -0
  51. package/dist/auth/routes.js +96 -0
  52. package/dist/auth/routes.js.map +1 -0
  53. package/dist/auth/schema.d.ts +158 -0
  54. package/dist/auth/schema.d.ts.map +1 -0
  55. package/dist/auth/schema.js +82 -0
  56. package/dist/auth/schema.js.map +1 -0
  57. package/dist/auth/service.d.ts +96 -0
  58. package/dist/auth/service.d.ts.map +1 -0
  59. package/dist/auth/service.js +129 -0
  60. package/dist/auth/service.js.map +1 -0
  61. package/dist/auth/session.d.ts +51 -0
  62. package/dist/auth/session.d.ts.map +1 -0
  63. package/dist/auth/session.js +344 -0
  64. package/dist/auth/session.js.map +1 -0
  65. package/dist/auth/utils.d.ts +17 -0
  66. package/dist/auth/utils.d.ts.map +1 -0
  67. package/dist/auth/utils.js +96 -0
  68. package/dist/auth/utils.js.map +1 -0
  69. package/dist/bin/corge.js +547 -0
  70. package/dist/chat/context.d.ts +10 -0
  71. package/dist/chat/context.d.ts.map +1 -0
  72. package/dist/chat/context.js +221 -0
  73. package/dist/chat/context.js.map +1 -0
  74. package/dist/chat/index.d.ts +4 -0
  75. package/dist/chat/index.d.ts.map +1 -0
  76. package/dist/chat/index.js +4 -0
  77. package/dist/chat/index.js.map +1 -0
  78. package/dist/chat/routes.d.ts +160 -0
  79. package/dist/chat/routes.d.ts.map +1 -0
  80. package/dist/chat/routes.js +210 -0
  81. package/dist/chat/routes.js.map +1 -0
  82. package/dist/chat/schema.d.ts +285 -0
  83. package/dist/chat/schema.d.ts.map +1 -0
  84. package/dist/chat/schema.js +74 -0
  85. package/dist/chat/schema.js.map +1 -0
  86. package/dist/chat/service.d.ts +64 -0
  87. package/dist/chat/service.d.ts.map +1 -0
  88. package/dist/chat/service.js +279 -0
  89. package/dist/chat/service.js.map +1 -0
  90. package/dist/chat/stream.d.ts +53 -0
  91. package/dist/chat/stream.d.ts.map +1 -0
  92. package/dist/chat/stream.js +339 -0
  93. package/dist/chat/stream.js.map +1 -0
  94. package/dist/chat/utils.d.ts +61 -0
  95. package/dist/chat/utils.d.ts.map +1 -0
  96. package/dist/chat/utils.js +285 -0
  97. package/dist/chat/utils.js.map +1 -0
  98. package/dist/cli/builder.d.ts +6 -0
  99. package/dist/cli/builder.d.ts.map +1 -0
  100. package/dist/cli/builder.js +91 -0
  101. package/dist/cli/builder.js.map +1 -0
  102. package/dist/cli/deployer.d.ts +2 -0
  103. package/dist/cli/deployer.d.ts.map +1 -0
  104. package/dist/cli/deployer.js +152 -0
  105. package/dist/cli/deployer.js.map +1 -0
  106. package/dist/cli/dev-server.d.ts +2 -0
  107. package/dist/cli/dev-server.d.ts.map +1 -0
  108. package/dist/cli/dev-server.js +70 -0
  109. package/dist/cli/dev-server.js.map +1 -0
  110. package/dist/cli/env.d.ts +4 -0
  111. package/dist/cli/env.d.ts.map +1 -0
  112. package/dist/cli/env.js +68 -0
  113. package/dist/cli/env.js.map +1 -0
  114. package/dist/cli/init.d.ts +6 -0
  115. package/dist/cli/init.d.ts.map +1 -0
  116. package/dist/cli/init.js +103 -0
  117. package/dist/cli/init.js.map +1 -0
  118. package/dist/config.d.ts +262 -0
  119. package/dist/config.d.ts.map +1 -0
  120. package/dist/config.js +212 -0
  121. package/dist/config.js.map +1 -0
  122. package/dist/db/index.d.ts +3 -0
  123. package/dist/db/index.d.ts.map +1 -0
  124. package/dist/db/index.js +3 -0
  125. package/dist/db/index.js.map +1 -0
  126. package/dist/db/schema.d.ts +410 -0
  127. package/dist/db/schema.d.ts.map +1 -0
  128. package/dist/db/schema.js +294 -0
  129. package/dist/db/schema.js.map +1 -0
  130. package/dist/db/service.d.ts +42 -0
  131. package/dist/db/service.d.ts.map +1 -0
  132. package/dist/db/service.js +372 -0
  133. package/dist/db/service.js.map +1 -0
  134. package/dist/db/utils.d.ts +25 -0
  135. package/dist/db/utils.d.ts.map +1 -0
  136. package/dist/db/utils.js +188 -0
  137. package/dist/db/utils.js.map +1 -0
  138. package/dist/emails/index.d.ts +4 -0
  139. package/dist/emails/index.d.ts.map +1 -0
  140. package/dist/emails/index.js +4 -0
  141. package/dist/emails/index.js.map +1 -0
  142. package/dist/emails/routes.d.ts +191 -0
  143. package/dist/emails/routes.d.ts.map +1 -0
  144. package/dist/emails/routes.js +39 -0
  145. package/dist/emails/routes.js.map +1 -0
  146. package/dist/emails/schema.d.ts +237 -0
  147. package/dist/emails/schema.d.ts.map +1 -0
  148. package/dist/emails/schema.js +98 -0
  149. package/dist/emails/schema.js.map +1 -0
  150. package/dist/emails/service.d.ts +22 -0
  151. package/dist/emails/service.d.ts.map +1 -0
  152. package/dist/emails/service.js +337 -0
  153. package/dist/emails/service.js.map +1 -0
  154. package/dist/emails/utils.d.ts +49 -0
  155. package/dist/emails/utils.d.ts.map +1 -0
  156. package/dist/emails/utils.js +341 -0
  157. package/dist/emails/utils.js.map +1 -0
  158. package/dist/files/index.d.ts +4 -0
  159. package/dist/files/index.d.ts.map +1 -0
  160. package/dist/files/index.js +4 -0
  161. package/dist/files/index.js.map +1 -0
  162. package/dist/files/routes.d.ts +72 -0
  163. package/dist/files/routes.d.ts.map +1 -0
  164. package/dist/files/routes.js +38 -0
  165. package/dist/files/routes.js.map +1 -0
  166. package/dist/files/schema.d.ts +57 -0
  167. package/dist/files/schema.d.ts.map +1 -0
  168. package/dist/files/schema.js +39 -0
  169. package/dist/files/schema.js.map +1 -0
  170. package/dist/files/service.d.ts +27 -0
  171. package/dist/files/service.d.ts.map +1 -0
  172. package/dist/files/service.js +175 -0
  173. package/dist/files/service.js.map +1 -0
  174. package/dist/files/utils.d.ts +3 -0
  175. package/dist/files/utils.d.ts.map +1 -0
  176. package/dist/files/utils.js +63 -0
  177. package/dist/files/utils.js.map +1 -0
  178. package/dist/index.d.ts +875 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +9919 -0
  181. package/dist/index.js.map +7 -0
  182. package/dist/payments/billing.d.ts +29 -0
  183. package/dist/payments/billing.d.ts.map +1 -0
  184. package/dist/payments/billing.js +378 -0
  185. package/dist/payments/billing.js.map +1 -0
  186. package/dist/payments/index.d.ts +4 -0
  187. package/dist/payments/index.d.ts.map +1 -0
  188. package/dist/payments/index.js +4 -0
  189. package/dist/payments/index.js.map +1 -0
  190. package/dist/payments/routes.d.ts +340 -0
  191. package/dist/payments/routes.d.ts.map +1 -0
  192. package/dist/payments/routes.js +56 -0
  193. package/dist/payments/routes.js.map +1 -0
  194. package/dist/payments/schema.d.ts +412 -0
  195. package/dist/payments/schema.d.ts.map +1 -0
  196. package/dist/payments/schema.js +260 -0
  197. package/dist/payments/schema.js.map +1 -0
  198. package/dist/payments/service.d.ts +31 -0
  199. package/dist/payments/service.d.ts.map +1 -0
  200. package/dist/payments/service.js +322 -0
  201. package/dist/payments/service.js.map +1 -0
  202. package/dist/payments/utils.d.ts +61 -0
  203. package/dist/payments/utils.d.ts.map +1 -0
  204. package/dist/payments/utils.js +369 -0
  205. package/dist/payments/utils.js.map +1 -0
  206. package/dist/router/guard.d.ts +19 -0
  207. package/dist/router/guard.d.ts.map +1 -0
  208. package/dist/router/guard.js +243 -0
  209. package/dist/router/guard.js.map +1 -0
  210. package/dist/router/index.d.ts +3 -0
  211. package/dist/router/index.d.ts.map +1 -0
  212. package/dist/router/index.js +3 -0
  213. package/dist/router/index.js.map +1 -0
  214. package/dist/router/schema.d.ts +103 -0
  215. package/dist/router/schema.d.ts.map +1 -0
  216. package/dist/router/schema.js +282 -0
  217. package/dist/router/schema.js.map +1 -0
  218. package/dist/router/service.d.ts +25 -0
  219. package/dist/router/service.d.ts.map +1 -0
  220. package/dist/router/service.js +339 -0
  221. package/dist/router/service.js.map +1 -0
  222. package/dist/tasks/index.d.ts +4 -0
  223. package/dist/tasks/index.d.ts.map +1 -0
  224. package/dist/tasks/index.js +4 -0
  225. package/dist/tasks/index.js.map +1 -0
  226. package/dist/tasks/routes.d.ts +54 -0
  227. package/dist/tasks/routes.d.ts.map +1 -0
  228. package/dist/tasks/routes.js +195 -0
  229. package/dist/tasks/routes.js.map +1 -0
  230. package/dist/tasks/schema.d.ts +261 -0
  231. package/dist/tasks/schema.d.ts.map +1 -0
  232. package/dist/tasks/schema.js +35 -0
  233. package/dist/tasks/schema.js.map +1 -0
  234. package/dist/tasks/service.d.ts +32 -0
  235. package/dist/tasks/service.d.ts.map +1 -0
  236. package/dist/tasks/service.js +201 -0
  237. package/dist/tasks/service.js.map +1 -0
  238. package/dist/tasks/steps.d.ts +18 -0
  239. package/dist/tasks/steps.d.ts.map +1 -0
  240. package/dist/tasks/steps.js +365 -0
  241. package/dist/tasks/steps.js.map +1 -0
  242. package/dist/tasks/utils.d.ts +75 -0
  243. package/dist/tasks/utils.d.ts.map +1 -0
  244. package/dist/tasks/utils.js +311 -0
  245. package/dist/tasks/utils.js.map +1 -0
  246. package/dist/tasks/watcher.d.ts +13 -0
  247. package/dist/tasks/watcher.d.ts.map +1 -0
  248. package/dist/tasks/watcher.js +255 -0
  249. package/dist/tasks/watcher.js.map +1 -0
  250. package/dist/tasks/worker.d.ts +28 -0
  251. package/dist/tasks/worker.d.ts.map +1 -0
  252. package/dist/tasks/worker.js +326 -0
  253. package/dist/tasks/worker.js.map +1 -0
  254. package/dist/utils/index.d.ts +3 -0
  255. package/dist/utils/index.d.ts.map +1 -0
  256. package/dist/utils/index.js +3 -0
  257. package/dist/utils/index.js.map +1 -0
  258. package/dist/utils/schema.d.ts +79 -0
  259. package/dist/utils/schema.d.ts.map +1 -0
  260. package/dist/utils/schema.js +41 -0
  261. package/dist/utils/schema.js.map +1 -0
  262. package/dist/utils/service.d.ts +32 -0
  263. package/dist/utils/service.d.ts.map +1 -0
  264. package/dist/utils/service.js +384 -0
  265. package/dist/utils/service.js.map +1 -0
  266. package/docs/SDK_SPEC.md +1645 -0
  267. package/infra/firestore.base.indexes.json +138 -0
  268. package/infra/firestore.base.rules +152 -0
  269. package/infra/storage.base.rules +8 -0
  270. package/package.json +59 -13
  271. package/src/ai/index.ts +2 -0
  272. package/src/ai/schema.ts +64 -0
  273. package/src/ai/service.ts +317 -0
  274. package/src/ai/utils.ts +38 -0
  275. package/src/attachments/index.ts +3 -0
  276. package/src/attachments/routes.ts +28 -0
  277. package/src/attachments/schema.ts +45 -0
  278. package/src/attachments/service.ts +315 -0
  279. package/src/attachments/utils.ts +115 -0
  280. package/src/auth/index.ts +3 -0
  281. package/src/auth/oidc.ts +419 -0
  282. package/src/auth/recovery.ts +365 -0
  283. package/src/auth/routes.ts +142 -0
  284. package/src/auth/schema.ts +181 -0
  285. package/src/auth/service.ts +183 -0
  286. package/src/auth/session.ts +414 -0
  287. package/src/auth/utils.ts +106 -0
  288. package/src/chat/context.ts +310 -0
  289. package/src/chat/index.ts +3 -0
  290. package/src/chat/routes.ts +264 -0
  291. package/src/chat/schema.ts +295 -0
  292. package/src/chat/service.ts +360 -0
  293. package/src/chat/stream.ts +481 -0
  294. package/src/chat/utils.ts +370 -0
  295. package/src/cli/builder.ts +93 -0
  296. package/src/cli/deployer.ts +184 -0
  297. package/src/cli/dev-server.ts +77 -0
  298. package/src/cli/env.ts +89 -0
  299. package/src/cli/init.ts +125 -0
  300. package/src/config.ts +447 -0
  301. package/src/db/index.ts +2 -0
  302. package/src/db/schema.ts +409 -0
  303. package/src/db/service.ts +477 -0
  304. package/src/db/utils.ts +250 -0
  305. package/src/emails/index.ts +3 -0
  306. package/src/emails/routes.ts +64 -0
  307. package/src/emails/schema.ts +239 -0
  308. package/src/emails/service.ts +488 -0
  309. package/src/emails/utils.ts +449 -0
  310. package/src/files/index.ts +3 -0
  311. package/src/files/routes.ts +44 -0
  312. package/src/files/schema.ts +76 -0
  313. package/src/files/service.ts +238 -0
  314. package/src/files/utils.ts +77 -0
  315. package/src/index.ts +425 -0
  316. package/src/payments/billing.ts +488 -0
  317. package/src/payments/index.ts +3 -0
  318. package/src/payments/routes.ts +92 -0
  319. package/src/payments/schema.ts +401 -0
  320. package/src/payments/service.ts +445 -0
  321. package/src/payments/utils.ts +472 -0
  322. package/src/router/guard.ts +319 -0
  323. package/src/router/index.ts +2 -0
  324. package/src/router/schema.ts +458 -0
  325. package/src/router/service.ts +453 -0
  326. package/src/tasks/index.ts +11 -0
  327. package/src/tasks/routes.ts +230 -0
  328. package/src/tasks/schema.ts +310 -0
  329. package/src/tasks/service.ts +254 -0
  330. package/src/tasks/steps.ts +472 -0
  331. package/src/tasks/utils.ts +445 -0
  332. package/src/tasks/watcher.ts +307 -0
  333. package/src/tasks/worker.ts +461 -0
  334. package/src/utils/index.ts +2 -0
  335. package/src/utils/schema.ts +69 -0
  336. package/src/utils/service.ts +461 -0
  337. package/tests/e2e.test.ts +2531 -0
  338. package/tsconfig.base.json +12 -0
  339. package/README.md +0 -2
  340. package/index.d.ts +0 -1
  341. package/index.js +0 -1
@@ -0,0 +1,445 @@
1
+ import { Timestamp, type DocumentReference, type Firestore } from 'firebase-admin/firestore'
2
+ import { z } from 'zod'
3
+ import { CORE_CONFIG, type DbRole } from '../config'
4
+ import type { EntryId } from '../db'
5
+ import type { RouteMode } from '../router'
6
+ import type { Duration } from '../utils'
7
+ import {
8
+ StaleWorkerEpochError,
9
+ TaskWaitDetachedSignal,
10
+ TaskYieldSignal,
11
+ type ClaimCheckPointer,
12
+ type StepContext,
13
+ type StepRetryPolicy,
14
+ type TaskRecord,
15
+ type WorkflowContext,
16
+ type WorkflowTargetRef,
17
+ type WorkflowTaskPayload
18
+ } from './schema'
19
+
20
+ export interface StepRunnerState {
21
+ mode: RouteMode
22
+ docRef?: DocumentReference
23
+ steps: Record<string, unknown>
24
+ expiresAt?: Timestamp
25
+ isDev: boolean
26
+ signals: Record<string, unknown>
27
+ wfCtx?: WorkflowContext
28
+ canYield: boolean
29
+ compensatedSteps: Record<string, boolean>
30
+ replayOnly: boolean
31
+ claimedEpoch?: number
32
+ fencedError: Error | null
33
+ activePersistPromise: Promise<void> | null
34
+ pendingPersist: boolean
35
+ writes: Promise<unknown>[]
36
+ rollbacks: { name: string; run: () => Promise<unknown> }[]
37
+ callCounts: Map<string, number>
38
+ abortController?: AbortController
39
+ }
40
+
41
+ export const isTaskYieldSignal = (err: unknown): err is TaskYieldSignal =>
42
+ err instanceof TaskYieldSignal || Boolean(err && typeof err === 'object' && (err as any).isTaskYield === true)
43
+
44
+ export const isClaimCheckPointer = (val: unknown): val is ClaimCheckPointer =>
45
+ Boolean(val && typeof val === 'object' && typeof (val as any).__claimCheck === 'string')
46
+
47
+ export const isTaskWaitDetachedSignal = (err: unknown): err is TaskWaitDetachedSignal =>
48
+ err instanceof TaskWaitDetachedSignal ||
49
+ Boolean(err && typeof err === 'object' && (err as { isTaskWaitDetached?: unknown }).isTaskWaitDetached === true)
50
+
51
+ export const zodToJsonSchemaSafe = (schema?: z.ZodType<any>): Record<string, unknown> | null => {
52
+ if (!schema) return null
53
+ try {
54
+ const raw = z.toJSONSchema(schema, { unrepresentable: 'any' }) as Record<string, unknown>
55
+ const { $schema: _$schema, ...rest } = raw
56
+ return rest
57
+ } catch {
58
+ return null
59
+ }
60
+ }
61
+
62
+ export const resolveTaskDeadline = (workflowName: string, deadlineSeconds?: number): number =>
63
+ deadlineSeconds ??
64
+ (workflowName === 'scan-attachment'
65
+ ? CORE_CONFIG.tasks.scanDeadlineSeconds
66
+ : CORE_CONFIG.tasks.defaultDeadlineSeconds)
67
+
68
+ export const getQueueLoc = (): string => `projects/${CORE_CONFIG.gcpProject}/locations/${CORE_CONFIG.region}`
69
+
70
+ export const getQueueBase = (isDev = false): string =>
71
+ `${getQueueLoc()}/queues/${isDev ? CORE_CONFIG.queueBaseNames.dev : CORE_CONFIG.queueBaseNames.prod}`
72
+
73
+ export const getQueueParent = (idx: number, isDev = false): string => {
74
+ const base = getQueueBase(isDev)
75
+ return idx === 0 ? base : `${base}-${String(idx).padStart(2, '0')}`
76
+ }
77
+
78
+ export const getWorkerUri = (isDev = false): string =>
79
+ isDev ? CORE_CONFIG.workerUris.dev : CORE_CONFIG.workerUris.prod
80
+
81
+ export const isTerminalRecord = (d: TaskRecord<unknown> | undefined): d is TaskRecord<unknown> =>
82
+ Boolean(d && (d.status === 'completed' || d.status === 'errored' || d.status === 'cancelled'))
83
+
84
+ export const setTaskDoc = (docRef: DocumentReference, patch: Record<string, unknown>): Promise<void> =>
85
+ docRef
86
+ .set(patch, { mergeFields: Object.keys(patch).filter((k) => patch[k] !== undefined) })
87
+ .then(() => {})
88
+ .catch(() => {})
89
+
90
+ export const resolveTargetName = (target: string | WorkflowTargetRef<any, any>): string => {
91
+ if (typeof target === 'string') return target
92
+ if (target.workflowName) return target.workflowName
93
+ throw new Error('Workflow target must specify a workflowName or route key')
94
+ }
95
+
96
+ export const isFencedOut = (
97
+ claimedEpoch: number | undefined,
98
+ docEpoch: number | undefined,
99
+ docStatus: string | undefined,
100
+ requireRunning = false
101
+ ): boolean => {
102
+ if (claimedEpoch !== undefined && docEpoch !== undefined && docEpoch !== claimedEpoch) return true
103
+ if (requireRunning) return docStatus !== 'running'
104
+ return (
105
+ docStatus === 'compensating' ||
106
+ docStatus === 'cancelled' ||
107
+ docStatus === 'completed' ||
108
+ docStatus === 'errored'
109
+ )
110
+ }
111
+
112
+ export const createStaleEpochError = (
113
+ taskId: string | undefined,
114
+ claimedEpoch: number | undefined,
115
+ actualEpoch: number | undefined,
116
+ status?: string
117
+ ): StaleWorkerEpochError =>
118
+ new StaleWorkerEpochError(
119
+ `Worker epoch ${claimedEpoch ?? 'unknown'} superseded by epoch ${actualEpoch ?? 'unknown'} (status=${status ?? 'unknown'})`,
120
+ {
121
+ ...(taskId ? { taskId } : {}),
122
+ ...(claimedEpoch !== undefined ? { claimedEpoch } : {}),
123
+ ...(actualEpoch !== undefined ? { actualEpoch } : {})
124
+ }
125
+ )
126
+
127
+ const compensationLocks = new Map<string, Promise<void>>()
128
+
129
+ export const runSerializedCompensation = async (taskId: string, fn: () => Promise<void>): Promise<void> => {
130
+ const prev = compensationLocks.get(taskId) ?? Promise.resolve()
131
+ const next = prev.catch(() => {}).then(fn)
132
+ compensationLocks.set(taskId, next)
133
+ try {
134
+ await next
135
+ } finally {
136
+ if (compensationLocks.get(taskId) === next) compensationLocks.delete(taskId)
137
+ }
138
+ }
139
+
140
+ export const executeCompensateState = async (s: StepRunnerState): Promise<void> => {
141
+ await Promise.allSettled(s.writes)
142
+ if (!s.rollbacks.length) return
143
+ if (s.docRef) {
144
+ const snap = await s.docRef.get().catch(() => null)
145
+ const remoteComp = snap?.data()?.compensatedSteps as Record<string, boolean> | undefined
146
+ if (remoteComp && typeof remoteComp === 'object') Object.assign(s.compensatedSteps, remoteComp)
147
+ }
148
+ for (const item of [...s.rollbacks].reverse()) {
149
+ if (s.compensatedSteps[item.name]) {
150
+ delete s.steps[item.name]
151
+ continue
152
+ }
153
+ await item.run()
154
+ s.compensatedSteps[item.name] = true
155
+ delete s.steps[item.name]
156
+ if (s.docRef) {
157
+ const patch: Record<string, unknown> = {
158
+ compensatedSteps: { ...s.compensatedSteps },
159
+ steps: { ...s.steps },
160
+ updatedAt: new Date().toISOString(),
161
+ ...(s.expiresAt ? { expiresAt: s.expiresAt } : {})
162
+ }
163
+ await s.docRef.set(patch, { mergeFields: Object.keys(patch) })
164
+ }
165
+ }
166
+ }
167
+
168
+ export const runStepWithRetry = async <T>(
169
+ fn: (ctx: StepContext) => Promise<T>,
170
+ retryOrCount: number | StepRetryPolicy = 2,
171
+ delayMs = 1000,
172
+ stepId = 'step',
173
+ timeout: Duration | undefined,
174
+ parentSignal: AbortSignal | undefined,
175
+ parseDuration: (d: Duration | undefined, fallbackMs: number) => number,
176
+ isNonRetryableError: (err: unknown) => boolean
177
+ ): Promise<T> => {
178
+ const policy: StepRetryPolicy =
179
+ typeof retryOrCount === 'number'
180
+ ? { maxAttempts: retryOrCount + 1, initialInterval: delayMs, backoffCoefficient: 1.5 }
181
+ : retryOrCount
182
+ const maxAttempts = Math.max(1, policy.maxAttempts ?? 3)
183
+ const initialMs = parseDuration(policy.initialInterval, delayMs)
184
+ const backoff = policy.backoffCoefficient ?? 1.5
185
+ const maxIntervalMs = policy.maxInterval ? parseDuration(policy.maxInterval, 30_000) : 30_000
186
+ const useJitter = policy.jitter !== false
187
+ const timeoutMs = timeout ? parseDuration(timeout, 0) : 0
188
+
189
+ let lastErr: unknown
190
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
191
+ const controller = new AbortController()
192
+ const onParentAbort = () => controller.abort(parentSignal?.reason)
193
+ if (parentSignal) {
194
+ if (parentSignal.aborted) controller.abort(parentSignal.reason)
195
+ else parentSignal.addEventListener('abort', onParentAbort, { once: true })
196
+ }
197
+ const timer =
198
+ timeoutMs > 0
199
+ ? setTimeout(() => controller.abort(new Error(`Step "${stepId}" timed out after ${timeoutMs}ms`)), timeoutMs)
200
+ : undefined
201
+ try {
202
+ const result = await fn({ signal: controller.signal, attempt, stepId })
203
+ if (timer) clearTimeout(timer)
204
+ if (parentSignal) parentSignal.removeEventListener('abort', onParentAbort)
205
+ return result
206
+ } catch (err) {
207
+ if (timer) clearTimeout(timer)
208
+ if (parentSignal) parentSignal.removeEventListener('abort', onParentAbort)
209
+ if (isTaskYieldSignal(err)) throw err
210
+ lastErr = err
211
+ const errName = err instanceof Error ? err.name : ''
212
+ if (isNonRetryableError(err) || (errName && policy.nonRetryableErrorTypes?.includes(errName))) throw err
213
+ if (attempt < maxAttempts) {
214
+ const rawWait = Math.min(maxIntervalMs, initialMs * Math.pow(backoff, attempt - 1))
215
+ const waitMs = useJitter ? Math.round(rawWait * (0.75 + Math.random() * 0.5)) : Math.round(rawWait)
216
+ await new Promise((r) => setTimeout(r, waitMs))
217
+ }
218
+ }
219
+ }
220
+ throw lastErr
221
+ }
222
+
223
+ export const claimTaskEpochTx = (
224
+ db: Firestore,
225
+ docRef: DocumentReference,
226
+ payload: WorkflowTaskPayload,
227
+ attempt: number,
228
+ resolveRole: (userId?: EntryId, explicitRole?: DbRole) => DbRole
229
+ ) =>
230
+ db.runTransaction(async (tx) => {
231
+ const data = (await tx.get(docRef)).data()
232
+ const currentStatus = data?.status as string | undefined
233
+ if (['completed', 'errored', 'cancelled', 'compensating'].includes(String(currentStatus))) {
234
+ return { aborted: true as const }
235
+ }
236
+ const currentEpoch = typeof data?.epoch === 'number' ? data.epoch : 0
237
+ if (attempt === 0 && payload.expectedEpoch !== undefined && currentEpoch !== payload.expectedEpoch) {
238
+ return { aborted: true as const }
239
+ }
240
+ const claimedEpoch = currentEpoch + 1
241
+ const activeRunId = crypto.randomUUID()
242
+ const startedAt = Date.now()
243
+ const expiresAt = Timestamp.fromMillis(startedAt + CORE_CONFIG.tasks.ttlMs)
244
+ const effectiveParams = (data?.params as Record<string, unknown> | undefined) ?? payload.params
245
+ const userId: EntryId = typeof data?.userId === 'string' ? data.userId : CORE_CONFIG.auth.adminUserId
246
+ const userRole = resolveRole(userId, data?.userRole as DbRole | undefined)
247
+ const steps: Record<string, unknown> = { ...((data?.steps as Record<string, unknown> | undefined) ?? {}) }
248
+ const signals: Record<string, unknown> = { ...((data?.signals as Record<string, unknown> | undefined) ?? {}) }
249
+ const compensatedSteps: Record<string, boolean> = {
250
+ ...((data?.compensatedSteps as Record<string, boolean> | undefined) ?? {})
251
+ }
252
+ tx.set(
253
+ docRef,
254
+ {
255
+ taskId: payload.taskId,
256
+ workflowName: payload.workflowName,
257
+ userId,
258
+ userRole,
259
+ status: 'running',
260
+ epoch: claimedEpoch,
261
+ activeRunId,
262
+ lastHeartbeatAt: startedAt,
263
+ params: effectiveParams,
264
+ steps,
265
+ compensatedSteps,
266
+ updatedAt: new Date(startedAt).toISOString(),
267
+ expiresAt
268
+ },
269
+ { merge: true }
270
+ )
271
+ return {
272
+ aborted: false as const,
273
+ claimedEpoch,
274
+ startedAt,
275
+ expiresAt,
276
+ effectiveParams,
277
+ userId,
278
+ userRole,
279
+ steps,
280
+ signals,
281
+ compensatedSteps
282
+ }
283
+ })
284
+
285
+ export const startTaskHeartbeatTimer = (
286
+ db: Firestore,
287
+ docRef: DocumentReference,
288
+ taskId: string,
289
+ claimedEpoch: number,
290
+ expiresAt: Timestamp,
291
+ controller: AbortController
292
+ ): ReturnType<typeof setInterval> => {
293
+ const timer = setInterval(() => {
294
+ void db
295
+ .runTransaction(async (tx) => {
296
+ const data = (await tx.get(docRef)).data()
297
+ if (data?.epoch !== claimedEpoch || data?.status !== 'running') {
298
+ controller.abort(
299
+ new StaleWorkerEpochError(`Heartbeat detected stale epoch on ${taskId}`, {
300
+ taskId,
301
+ claimedEpoch,
302
+ actualEpoch: typeof data?.epoch === 'number' ? data.epoch : undefined
303
+ })
304
+ )
305
+ return
306
+ }
307
+ tx.set(docRef, { lastHeartbeatAt: Date.now(), updatedAt: new Date().toISOString(), expiresAt }, { merge: true })
308
+ })
309
+ .catch(() => {})
310
+ }, CORE_CONFIG.tasks.heartbeatIntervalMs)
311
+ timer.unref?.()
312
+ return timer
313
+ }
314
+
315
+ export const commitTaskCompletionTx = (
316
+ db: Firestore,
317
+ docRef: DocumentReference,
318
+ baseMeta: Record<string, unknown>,
319
+ taskId: string,
320
+ claimedEpoch: number,
321
+ storedResponse: unknown,
322
+ steps: Record<string, unknown>,
323
+ expiresAt: Timestamp
324
+ ): Promise<void> =>
325
+ db.runTransaction(async (tx) => {
326
+ const data = (await tx.get(docRef)).data()
327
+ const docEpoch = typeof data?.epoch === 'number' ? data.epoch : undefined
328
+ if (docEpoch !== claimedEpoch || data?.status !== 'running') {
329
+ throw new StaleWorkerEpochError(`Completion fenced on ${taskId}`, {
330
+ taskId,
331
+ claimedEpoch,
332
+ actualEpoch: docEpoch
333
+ })
334
+ }
335
+ tx.set(
336
+ docRef,
337
+ {
338
+ ...baseMeta,
339
+ status: 'completed',
340
+ epoch: claimedEpoch,
341
+ activeRunId: null,
342
+ response: storedResponse,
343
+ steps,
344
+ lastHeartbeatAt: Date.now(),
345
+ updatedAt: new Date().toISOString(),
346
+ expiresAt
347
+ },
348
+ { merge: true }
349
+ )
350
+ })
351
+
352
+ export const commitTaskYieldTx = async (
353
+ db: Firestore,
354
+ docRef: DocumentReference,
355
+ baseMeta: Record<string, unknown>,
356
+ taskId: string,
357
+ claimedEpoch: number,
358
+ err: TaskYieldSignal,
359
+ nextEpoch: number,
360
+ nextParams: Record<string, unknown>,
361
+ nextSteps: Record<string, unknown>,
362
+ nextStatus: string,
363
+ expiresAt: Timestamp
364
+ ): Promise<void> => {
365
+ const yieldCommitted = await db.runTransaction(async (tx) => {
366
+ const data = (await tx.get(docRef)).data()
367
+ const docEpoch = typeof data?.epoch === 'number' ? data.epoch : undefined
368
+ if (docEpoch !== claimedEpoch || data?.status !== 'running') {
369
+ return { fenced: true as const, docEpoch }
370
+ }
371
+ tx.set(
372
+ docRef,
373
+ {
374
+ ...baseMeta,
375
+ status: nextStatus,
376
+ epoch: nextEpoch,
377
+ activeRunId: null,
378
+ params: nextParams,
379
+ steps: nextSteps,
380
+ ...(err.reason === 'waiting_signal' ? { waitingSignal: err.meta.signalName ?? null } : {}),
381
+ updatedAt: new Date().toISOString(),
382
+ expiresAt
383
+ },
384
+ {
385
+ mergeFields: [
386
+ 'taskId',
387
+ 'workflowName',
388
+ 'userId',
389
+ 'userRole',
390
+ 'status',
391
+ 'epoch',
392
+ 'activeRunId',
393
+ 'params',
394
+ 'steps',
395
+ ...(err.reason === 'waiting_signal' ? ['waitingSignal'] : []),
396
+ 'updatedAt',
397
+ 'expiresAt'
398
+ ]
399
+ }
400
+ )
401
+ return { fenced: false as const }
402
+ })
403
+ if (yieldCommitted.fenced) {
404
+ throw new StaleWorkerEpochError(`Yield fenced on ${taskId}`, {
405
+ taskId,
406
+ claimedEpoch,
407
+ actualEpoch: yieldCommitted.docEpoch
408
+ })
409
+ }
410
+ }
411
+
412
+ export const commitTaskCompensatingTx = async (
413
+ db: Firestore,
414
+ docRef: DocumentReference,
415
+ taskId: string,
416
+ claimedEpoch: number,
417
+ expiresAt: Timestamp
418
+ ): Promise<void> => {
419
+ const compTransition = await db.runTransaction(async (tx) => {
420
+ const data = (await tx.get(docRef)).data()
421
+ const docEpoch = typeof data?.epoch === 'number' ? data.epoch : undefined
422
+ if (docEpoch !== claimedEpoch || data?.status !== 'running') {
423
+ return { fenced: true as const, docEpoch }
424
+ }
425
+ tx.set(
426
+ docRef,
427
+ {
428
+ status: 'compensating',
429
+ epoch: claimedEpoch + 1,
430
+ activeRunId: null,
431
+ updatedAt: new Date().toISOString(),
432
+ expiresAt
433
+ },
434
+ { merge: true }
435
+ )
436
+ return { fenced: false as const }
437
+ })
438
+ if (compTransition.fenced) {
439
+ throw new StaleWorkerEpochError(`Error compensation fenced on ${taskId}`, {
440
+ taskId,
441
+ claimedEpoch,
442
+ actualEpoch: compTransition.docEpoch
443
+ })
444
+ }
445
+ }