@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,472 @@
1
+ import type { DocumentReference, Timestamp } from 'firebase-admin/firestore'
2
+ import { CORE_CONFIG } from '../config'
3
+ import type { Core } from '../index'
4
+ import type { RouteMode } from '../router'
5
+ import type { Duration } from '../utils'
6
+ import {
7
+ ReplayCompleteSignal,
8
+ TaskYieldSignal,
9
+ type ClaimCheckPointer,
10
+ type CreateStepRunnerOptions,
11
+ type RollbackLeg,
12
+ type StepContext,
13
+ type StepFn,
14
+ type StepMapOptions,
15
+ type StepOptions,
16
+ type StepRetryPolicy,
17
+ type StepRunner,
18
+ type StepSpec,
19
+ type TaskProgressUpdate,
20
+ type WorkflowContext,
21
+ type WorkflowTargetRef
22
+ } from './schema'
23
+ import {
24
+ createStaleEpochError,
25
+ executeCompensateState,
26
+ isClaimCheckPointer,
27
+ isFencedOut,
28
+ resolveTargetName,
29
+ runSerializedCompensation,
30
+ runStepWithRetry,
31
+ type StepRunnerState
32
+ } from './utils'
33
+
34
+ const CUMULATIVE_STEPS_THRESHOLD_BYTES = 150 * 1024
35
+
36
+ export class TaskStepsEngine {
37
+ readonly #core: Core
38
+ readonly #invokeChildWorkflow: (
39
+ targetName: string,
40
+ input: unknown,
41
+ options?: { timeout?: Duration; dedupeKey?: string; wait?: boolean }
42
+ ) => Promise<any>
43
+
44
+ constructor(
45
+ coreClient: Core,
46
+ invokeChildWorkflow: (
47
+ targetName: string,
48
+ input: unknown,
49
+ options?: { timeout?: Duration; dedupeKey?: string; wait?: boolean }
50
+ ) => Promise<any>
51
+ ) {
52
+ this.#core = coreClient
53
+ this.#invokeChildWorkflow = invokeChildWorkflow
54
+ }
55
+
56
+ maybeOffloadStepValue = async (
57
+ docRef: DocumentReference | undefined,
58
+ stepId: string,
59
+ val: unknown,
60
+ isDev = false,
61
+ forceOffload = false
62
+ ): Promise<unknown> => {
63
+ if (!docRef || val === undefined || val === null || isClaimCheckPointer(val)) return val
64
+ try {
65
+ const json = JSON.stringify(val)
66
+ if (!json) return val
67
+ if (!forceOffload && Buffer.byteLength(json, 'utf8') <= CORE_CONFIG.tasks.claimCheckThresholdBytes) {
68
+ return val
69
+ }
70
+ const gcsPath = `_tasks/${docRef.id}/steps/${encodeURIComponent(stepId)}.json`
71
+ await this.#core.db
72
+ .bucket(isDev)
73
+ .file(gcsPath)
74
+ .save(Buffer.from(json, 'utf8'), {
75
+ resumable: false,
76
+ validation: false,
77
+ contentType: 'application/json'
78
+ })
79
+ return { __claimCheck: gcsPath } satisfies ClaimCheckPointer
80
+ } catch {
81
+ return val
82
+ }
83
+ }
84
+
85
+ maybeHydrateStepValue = async <T>(raw: unknown, isDev = false): Promise<T> => {
86
+ if (!isClaimCheckPointer(raw)) return raw as T
87
+ const [buf] = await this.#core.db.bucket(isDev).file(raw.__claimCheck).download()
88
+ return JSON.parse(buf.toString('utf8')) as T
89
+ }
90
+
91
+ allWithRollback = async <T extends readonly unknown[]>(
92
+ legs: { [K in keyof T]: RollbackLeg<T[K]> }
93
+ ): Promise<T> => {
94
+ const settled = await Promise.allSettled(legs.map((l) => l.fn()))
95
+ const failed = settled.find((s): s is PromiseRejectedResult => s.status === 'rejected')
96
+ if (failed) {
97
+ await Promise.allSettled(
98
+ settled.map((s, i) => (s.status === 'fulfilled' && legs[i]?.rollback ? legs[i]!.rollback!(s.value) : undefined))
99
+ )
100
+ throw failed.reason
101
+ }
102
+ return settled.map((s) => (s as PromiseFulfilledResult<unknown>).value) as unknown as T
103
+ }
104
+
105
+ #assertNotFenced = (s: StepRunnerState): void => {
106
+ if (s.fencedError) throw s.fencedError
107
+ if (s.wfCtx?.signal?.aborted && this.#core.errors.isStaleWorkerEpochError(s.wfCtx.signal.reason)) {
108
+ s.fencedError = s.wfCtx.signal.reason
109
+ throw s.fencedError
110
+ }
111
+ }
112
+
113
+ #triggerFenceAbort = (s: StepRunnerState, actualEpoch?: number, status?: string): never => {
114
+ if (!s.fencedError) {
115
+ s.fencedError = createStaleEpochError(s.docRef?.id, s.claimedEpoch, actualEpoch, status)
116
+ }
117
+ s.abortController?.abort(s.fencedError)
118
+ throw s.fencedError
119
+ }
120
+
121
+ #runFencedPersistOnce = async (s: StepRunnerState): Promise<void> => {
122
+ if (!s.docRef) return
123
+ this.#assertNotFenced(s)
124
+ try {
125
+ if (Buffer.byteLength(JSON.stringify(s.steps), 'utf8') > CUMULATIVE_STEPS_THRESHOLD_BYTES) {
126
+ for (const [k, v] of Object.entries(s.steps)) {
127
+ if (v !== undefined && v !== null && typeof v === 'object' && !isClaimCheckPointer(v)) {
128
+ s.steps[k] = await this.maybeOffloadStepValue(s.docRef, k, v, s.isDev, true)
129
+ }
130
+ }
131
+ }
132
+ } catch {}
133
+
134
+ const docRef = s.docRef
135
+ await docRef.firestore.runTransaction(async (tx) => {
136
+ const snap = await tx.get(docRef)
137
+ const data = snap.data()
138
+ const docEpoch = typeof data?.epoch === 'number' ? data.epoch : undefined
139
+ const docStatus = typeof data?.status === 'string' ? data.status : undefined
140
+ if (s.claimedEpoch === undefined && docEpoch !== undefined) s.claimedEpoch = docEpoch
141
+ if (isFencedOut(s.claimedEpoch, docEpoch, docStatus)) {
142
+ this.#triggerFenceAbort(s, docEpoch, docStatus)
143
+ }
144
+ const patch: Record<string, unknown> = {
145
+ steps: { ...s.steps },
146
+ lastHeartbeatAt: Date.now(),
147
+ ...(s.expiresAt ? { expiresAt: s.expiresAt } : {})
148
+ }
149
+ tx.set(docRef, patch, { mergeFields: Object.keys(patch) })
150
+ })
151
+ }
152
+
153
+ #persistSteps = (s: StepRunnerState): Promise<void> => {
154
+ if (!s.docRef) return Promise.resolve()
155
+ this.#assertNotFenced(s)
156
+ if (s.activePersistPromise) {
157
+ s.pendingPersist = true
158
+ return s.activePersistPromise
159
+ }
160
+ const loop = (async () => {
161
+ try {
162
+ do {
163
+ s.pendingPersist = false
164
+ await this.#runFencedPersistOnce(s)
165
+ } while (s.pendingPersist && !s.fencedError)
166
+ } finally {
167
+ s.activePersistPromise = null
168
+ }
169
+ })()
170
+ s.activePersistPromise = loop
171
+ s.writes.push(loop.catch(() => {}))
172
+ return loop
173
+ }
174
+
175
+ #executeSingleStep = async <T>(
176
+ s: StepRunnerState,
177
+ flushFn: () => Promise<unknown>,
178
+ stepId: string,
179
+ fnOrSpec: StepSpec<T>,
180
+ options?: StepOptions<T>
181
+ ): Promise<T> => {
182
+ this.#assertNotFenced(s)
183
+ const fn = typeof fnOrSpec === 'function' ? fnOrSpec : fnOrSpec.fn
184
+ const rollback = options?.rollback ?? (typeof fnOrSpec === 'function' ? undefined : fnOrSpec.rollback)
185
+ const retry = options?.retry ?? (typeof fnOrSpec === 'function' ? undefined : fnOrSpec.retry)
186
+ const timeout = options?.timeout ?? (typeof fnOrSpec === 'function' ? undefined : fnOrSpec.timeout)
187
+
188
+ const registerRollback = (val: T) => {
189
+ if (!rollback) return
190
+ const idx = s.rollbacks.findIndex((r) => r.name === stepId)
191
+ if (idx !== -1) s.rollbacks.splice(idx, 1)
192
+ s.rollbacks.push({ name: stepId, run: () => rollback(val) })
193
+ }
194
+
195
+ if (stepId in s.steps) {
196
+ const cached = await this.maybeHydrateStepValue<T>(s.steps[stepId], s.isDev)
197
+ registerRollback(cached)
198
+ return cached
199
+ }
200
+ if (s.replayOnly) throw new ReplayCompleteSignal()
201
+
202
+ if (s.canYield && s.wfCtx) {
203
+ const budgetMs = (s.wfCtx.deadlineMs - s.wfCtx.startedAt) * CORE_CONFIG.tasks.yieldBudgetRatio
204
+ if (budgetMs > 0 && Date.now() - s.wfCtx.startedAt > budgetMs) {
205
+ await flushFn()
206
+ throw new TaskYieldSignal('yield', { delaySeconds: 0 })
207
+ }
208
+ }
209
+ this.#assertNotFenced(s)
210
+
211
+ const val = await (s.mode !== 'rest' || retry !== undefined || timeout !== undefined
212
+ ? runStepWithRetry(
213
+ fn,
214
+ retry ?? (s.mode !== 'rest' ? 2 : 0),
215
+ 1000,
216
+ stepId,
217
+ timeout,
218
+ s.wfCtx?.signal,
219
+ this.#core.errors.parseDuration,
220
+ this.#core.errors.isNonRetryableError
221
+ )
222
+ : fn({ signal: s.wfCtx?.signal ?? new AbortController().signal, attempt: 1, stepId }))
223
+
224
+ this.#assertNotFenced(s)
225
+ s.steps[stepId] = await this.maybeOffloadStepValue(s.docRef, stepId, val, s.isDev)
226
+ if (s.docRef) {
227
+ try {
228
+ await this.#persistSteps(s)
229
+ } catch (err) {
230
+ if (this.#core.errors.isStaleWorkerEpochError(err)) {
231
+ delete s.steps[stepId]
232
+ throw err
233
+ }
234
+ }
235
+ }
236
+ registerRollback(val)
237
+ return val
238
+ }
239
+
240
+ #mapStep = async <T, R>(
241
+ s: StepRunnerState,
242
+ flushFn: () => Promise<unknown>,
243
+ stepId: string,
244
+ items: readonly T[],
245
+ fn: (item: T, index: number, ctx: StepContext) => Promise<R>,
246
+ options?: StepMapOptions<T, R>
247
+ ): Promise<R[]> => {
248
+ if (items.length === 0) return []
249
+ if (stepId in s.steps) return this.maybeHydrateStepValue<R[]>(s.steps[stepId], s.isDev)
250
+
251
+ const results = new Array<R>(items.length)
252
+ const pendingIndices: number[] = []
253
+
254
+ if (options?.shortCircuit) {
255
+ const checks = await Promise.all(items.map((item, idx) => options.shortCircuit!(item, idx)))
256
+ for (let i = 0; i < items.length; i++) {
257
+ if (checks[i] !== undefined) results[i] = checks[i]!
258
+ else pendingIndices.push(i)
259
+ }
260
+ } else {
261
+ for (let i = 0; i < items.length; i++) pendingIndices.push(i)
262
+ }
263
+
264
+ if (pendingIndices.length > 0) {
265
+ const concurrency = Math.max(1, options?.concurrency ?? CORE_CONFIG.tasks.defaultShardsCount)
266
+ let cursor = 0
267
+ const workers = Array.from({ length: Math.min(concurrency, pendingIndices.length) }, async () => {
268
+ while (cursor < pendingIndices.length) {
269
+ const idx = pendingIndices[cursor++]!
270
+ results[idx] = await this.#executeSingleStep(
271
+ s,
272
+ flushFn,
273
+ `${stepId}:${idx}`,
274
+ (ctx) => fn(items[idx]!, idx, ctx),
275
+ options
276
+ )
277
+ }
278
+ })
279
+ await Promise.all(workers)
280
+ }
281
+
282
+ for (const idx of pendingIndices) delete s.steps[`${stepId}:${idx}`]
283
+ s.steps[stepId] = await this.maybeOffloadStepValue(s.docRef, stepId, results, s.isDev, items.length > 25)
284
+ if (s.docRef) await this.#persistSteps(s)
285
+ return results
286
+ }
287
+
288
+ #waitForSignalStep = async <T = unknown>(
289
+ s: StepRunnerState,
290
+ flushFn: () => Promise<unknown>,
291
+ stepId: string,
292
+ signalName: string,
293
+ options?: { timeout?: Duration }
294
+ ): Promise<T> => {
295
+ this.#assertNotFenced(s)
296
+ if (stepId in s.steps) return this.maybeHydrateStepValue<T>(s.steps[stepId], s.isDev)
297
+ if (s.replayOnly) throw new ReplayCompleteSignal()
298
+ if (signalName in s.signals) {
299
+ const payload = s.signals[signalName] as T
300
+ s.steps[stepId] = await this.maybeOffloadStepValue(s.docRef, stepId, payload, s.isDev)
301
+ if (s.docRef) await this.#persistSteps(s)
302
+ return payload
303
+ }
304
+ if (s.docRef) {
305
+ const freshSnap = await s.docRef.get().catch(() => null)
306
+ const freshSignals = (freshSnap?.data()?.signals as Record<string, unknown> | undefined) ?? {}
307
+ if (signalName in freshSignals) {
308
+ const payload = freshSignals[signalName] as T
309
+ s.steps[stepId] = await this.maybeOffloadStepValue(s.docRef, stepId, payload, s.isDev)
310
+ await this.#persistSteps(s)
311
+ return payload
312
+ }
313
+ const timeoutKey = `__signal_deadline:${stepId}`
314
+ const timeoutMs = options?.timeout ? this.#core.errors.parseDuration(options.timeout, 0) : 0
315
+ const now = Date.now()
316
+ const existingDeadline = typeof s.steps[timeoutKey] === 'number' ? (s.steps[timeoutKey] as number) : undefined
317
+ if (existingDeadline && now >= existingDeadline) {
318
+ throw this.#core.errors.nonRetryableError(`Timed out waiting for signal "${signalName}"`, 'SIGNAL_TIMEOUT')
319
+ }
320
+ if (!existingDeadline && timeoutMs > 0) {
321
+ s.steps[timeoutKey] = now + timeoutMs
322
+ await this.#persistSteps(s)
323
+ }
324
+ if (s.canYield) {
325
+ await flushFn()
326
+ throw new TaskYieldSignal('waiting_signal', {
327
+ signalName,
328
+ ...(timeoutMs > 0 ? { timeoutMs: existingDeadline ? Math.max(1000, existingDeadline - now) : timeoutMs } : {})
329
+ })
330
+ }
331
+ }
332
+ throw this.#core.errors.nonRetryableError(`Signal "${signalName}" not received`, 'SIGNAL_TIMEOUT')
333
+ }
334
+
335
+ #invokeChildStep = <I, O>(
336
+ s: StepRunnerState,
337
+ flush: () => Promise<PromiseSettledResult<unknown>[]>,
338
+ step: StepFn,
339
+ stepId: string,
340
+ target: string | WorkflowTargetRef<I, any>,
341
+ input: I,
342
+ options: { timeout?: Duration; dedupeKey?: string } | undefined,
343
+ wait: boolean
344
+ ): Promise<O> =>
345
+ this.#executeSingleStep(s, flush, stepId, async () => {
346
+ if (wait && typeof target === 'object' && target.handler && !target.workflowName) {
347
+ return target.handler({ input, step })
348
+ }
349
+ const workflowName = resolveTargetName(target)
350
+ const timeout = options?.timeout ?? (typeof target === 'object' ? target.timeout : undefined)
351
+ return this.#invokeChildWorkflow(workflowName, input, {
352
+ ...(timeout !== undefined ? { timeout } : {}),
353
+ ...(options?.dedupeKey ? { dedupeKey: options.dedupeKey } : {}),
354
+ wait
355
+ }) as Promise<O>
356
+ })
357
+
358
+ #sleepStep = async (
359
+ s: StepRunnerState,
360
+ flush: () => Promise<PromiseSettledResult<unknown>[]>,
361
+ stepId: string,
362
+ duration: Duration
363
+ ): Promise<void> => {
364
+ this.#assertNotFenced(s)
365
+ if (s.steps[stepId] === true) return
366
+ if (s.replayOnly) throw new ReplayCompleteSignal()
367
+ const ms = Math.max(0, this.#core.errors.parseDuration(duration, 0))
368
+ if (!s.canYield || ms <= 2_000) {
369
+ if (ms > 0) await new Promise((r) => setTimeout(r, ms))
370
+ s.steps[stepId] = true
371
+ if (s.docRef) await this.#persistSteps(s)
372
+ return
373
+ }
374
+ s.steps[stepId] = true
375
+ await this.#persistSteps(s)
376
+ await flush()
377
+ throw new TaskYieldSignal('sleeping', { delaySeconds: Math.ceil(ms / 1000) })
378
+ }
379
+
380
+ #progressStep = async (s: StepRunnerState, update: TaskProgressUpdate): Promise<void> => {
381
+ this.#assertNotFenced(s)
382
+ if (!s.docRef || !s.expiresAt) return
383
+ const ref = s.docRef
384
+ const p = ref.firestore
385
+ .runTransaction(async (tx) => {
386
+ const data = (await tx.get(ref)).data()
387
+ const docEpoch = typeof data?.epoch === 'number' ? data.epoch : undefined
388
+ const docStatus = typeof data?.status === 'string' ? data.status : undefined
389
+ if (isFencedOut(s.claimedEpoch, docEpoch, docStatus)) this.#triggerFenceAbort(s, docEpoch, docStatus)
390
+ tx.set(ref, { progress: update, updatedAt: new Date().toISOString(), expiresAt: s.expiresAt }, { merge: true })
391
+ })
392
+ .catch((err) => {
393
+ if (this.#core.errors.isStaleWorkerEpochError(err)) throw err
394
+ })
395
+ s.writes.push(p)
396
+ await p
397
+ }
398
+
399
+ #flushSteps = async (s: StepRunnerState): Promise<PromiseSettledResult<unknown>[]> => {
400
+ this.#assertNotFenced(s)
401
+ if (s.docRef && s.claimedEpoch !== undefined && s.writes.length === 0) await this.#persistSteps(s)
402
+ const settled = await Promise.allSettled(s.writes)
403
+ for (const item of settled) {
404
+ if (item.status === 'rejected' && this.#core.errors.isStaleWorkerEpochError(item.reason)) throw item.reason
405
+ }
406
+ this.#assertNotFenced(s)
407
+ return settled
408
+ }
409
+
410
+ createStepRunner = (
411
+ mode: RouteMode = 'workflow',
412
+ docRef?: DocumentReference,
413
+ steps: Record<string, unknown> = {},
414
+ expiresAt?: Timestamp,
415
+ runnerOpts?: CreateStepRunnerOptions
416
+ ): StepRunner => {
417
+ const s: StepRunnerState = {
418
+ mode,
419
+ docRef,
420
+ steps,
421
+ expiresAt,
422
+ isDev: Boolean(runnerOpts?.isDev ?? this.#core.env.isDev),
423
+ signals: runnerOpts?.signals ?? {},
424
+ wfCtx: runnerOpts?.workflowContext,
425
+ canYield: Boolean(runnerOpts?.canYield && docRef && expiresAt && mode === 'workflow'),
426
+ compensatedSteps: runnerOpts?.compensatedSteps ?? {},
427
+ replayOnly: Boolean(runnerOpts?.replayOnly),
428
+ claimedEpoch: runnerOpts?.claimedEpoch,
429
+ fencedError: null,
430
+ activePersistPromise: null,
431
+ pendingPersist: false,
432
+ writes: [],
433
+ rollbacks: [],
434
+ callCounts: new Map<string, number>(),
435
+ abortController: runnerOpts?.abortController
436
+ }
437
+
438
+ const nextStepKey = (baseName: string): string => {
439
+ const count = (s.callCounts.get(baseName) ?? 0) + 1
440
+ s.callCounts.set(baseName, count)
441
+ return count === 1 ? baseName : `${baseName}#${count}`
442
+ }
443
+
444
+ const flush = (): Promise<PromiseSettledResult<unknown>[]> => this.#flushSteps(s)
445
+ const step: StepFn = <T>(name: string, fnOrSpec: StepSpec<T>, options?: StepOptions<T>): Promise<T> =>
446
+ this.#executeSingleStep(s, flush, nextStepKey(name), fnOrSpec, options)
447
+ const compensate = (): Promise<void> =>
448
+ s.docRef ? runSerializedCompensation(s.docRef.id, () => executeCompensateState(s)) : executeCompensateState(s)
449
+
450
+ return Object.assign(step, {
451
+ run: step,
452
+ map: <T, R>(name: string, items: readonly T[], fn: (item: T, index: number, ctx: StepContext) => Promise<R>, options?: StepMapOptions<T, R>): Promise<R[]> =>
453
+ this.#mapStep(s, flush, nextStepKey(name), items, fn, options),
454
+ invoke: <I = unknown, O = unknown>(name: string, target: string | WorkflowTargetRef<I, O>, input: I, options?: { timeout?: Duration; dedupeKey?: string }): Promise<O> =>
455
+ this.#invokeChildStep(s, flush, step, nextStepKey(name), target, input, options, true),
456
+ spawn: <I = unknown>(name: string, target: string | WorkflowTargetRef<I, unknown>, input: I, options?: { dedupeKey?: string; timeout?: Duration }): Promise<{ taskId: string }> =>
457
+ this.#invokeChildStep<I, { taskId: string }>(s, flush, step, nextStepKey(name), target, input, options, false),
458
+ sleep: (name: string, duration: Duration) => this.#sleepStep(s, flush, nextStepKey(name), duration),
459
+ waitForSignal: <T = unknown>(name: string, signalName: string, options?: { timeout?: Duration }): Promise<T> =>
460
+ this.#waitForSignalStep(s, flush, nextStepKey(name), signalName, options),
461
+ continueAsNew: async <I = unknown>(newInput: I): Promise<never> => {
462
+ this.#assertNotFenced(s)
463
+ await flush()
464
+ throw new TaskYieldSignal('continue_as_new', { newInput })
465
+ },
466
+ progress: (update: TaskProgressUpdate) => this.#progressStep(s, update),
467
+ flush,
468
+ compensate,
469
+ onError: compensate
470
+ })
471
+ }
472
+ }