@uploadista/core 0.0.2

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 (359) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-check.log +231 -0
  3. package/.turbo/turbo-format.log +5 -0
  4. package/LICENSE +21 -0
  5. package/README.md +1120 -0
  6. package/dist/chunk-CUT6urMc.cjs +1 -0
  7. package/dist/debounce-C2SeqcxD.js +2 -0
  8. package/dist/debounce-C2SeqcxD.js.map +1 -0
  9. package/dist/debounce-LZK7yS7Z.cjs +1 -0
  10. package/dist/errors/index.cjs +1 -0
  11. package/dist/errors/index.d.cts +3 -0
  12. package/dist/errors/index.d.ts +3 -0
  13. package/dist/errors/index.d.ts.map +1 -0
  14. package/dist/errors/index.js +2 -0
  15. package/dist/errors/uploadista-error.d.ts +209 -0
  16. package/dist/errors/uploadista-error.d.ts.map +1 -0
  17. package/dist/errors/uploadista-error.js +322 -0
  18. package/dist/errors-8i_aMxOE.js +1 -0
  19. package/dist/errors-CRm1FHHT.cjs +0 -0
  20. package/dist/flow/edge.d.ts +47 -0
  21. package/dist/flow/edge.d.ts.map +1 -0
  22. package/dist/flow/edge.js +40 -0
  23. package/dist/flow/event.d.ts +206 -0
  24. package/dist/flow/event.d.ts.map +1 -0
  25. package/dist/flow/event.js +53 -0
  26. package/dist/flow/flow-server.d.ts +223 -0
  27. package/dist/flow/flow-server.d.ts.map +1 -0
  28. package/dist/flow/flow-server.js +614 -0
  29. package/dist/flow/flow.d.ts +238 -0
  30. package/dist/flow/flow.d.ts.map +1 -0
  31. package/dist/flow/flow.js +629 -0
  32. package/dist/flow/index.cjs +1 -0
  33. package/dist/flow/index.d.cts +6 -0
  34. package/dist/flow/index.d.ts +24 -0
  35. package/dist/flow/index.d.ts.map +1 -0
  36. package/dist/flow/index.js +24 -0
  37. package/dist/flow/node.d.ts +136 -0
  38. package/dist/flow/node.d.ts.map +1 -0
  39. package/dist/flow/node.js +153 -0
  40. package/dist/flow/nodes/index.d.ts +8 -0
  41. package/dist/flow/nodes/index.d.ts.map +1 -0
  42. package/dist/flow/nodes/index.js +7 -0
  43. package/dist/flow/nodes/input-node.d.ts +78 -0
  44. package/dist/flow/nodes/input-node.d.ts.map +1 -0
  45. package/dist/flow/nodes/input-node.js +233 -0
  46. package/dist/flow/nodes/storage-node.d.ts +67 -0
  47. package/dist/flow/nodes/storage-node.d.ts.map +1 -0
  48. package/dist/flow/nodes/storage-node.js +94 -0
  49. package/dist/flow/nodes/streaming-input-node.d.ts +69 -0
  50. package/dist/flow/nodes/streaming-input-node.d.ts.map +1 -0
  51. package/dist/flow/nodes/streaming-input-node.js +156 -0
  52. package/dist/flow/nodes/transform-node.d.ts +85 -0
  53. package/dist/flow/nodes/transform-node.d.ts.map +1 -0
  54. package/dist/flow/nodes/transform-node.js +107 -0
  55. package/dist/flow/parallel-scheduler.d.ts +175 -0
  56. package/dist/flow/parallel-scheduler.d.ts.map +1 -0
  57. package/dist/flow/parallel-scheduler.js +193 -0
  58. package/dist/flow/plugins/credential-provider.d.ts +47 -0
  59. package/dist/flow/plugins/credential-provider.d.ts.map +1 -0
  60. package/dist/flow/plugins/credential-provider.js +24 -0
  61. package/dist/flow/plugins/image-ai-plugin.d.ts +61 -0
  62. package/dist/flow/plugins/image-ai-plugin.d.ts.map +1 -0
  63. package/dist/flow/plugins/image-ai-plugin.js +21 -0
  64. package/dist/flow/plugins/image-plugin.d.ts +52 -0
  65. package/dist/flow/plugins/image-plugin.d.ts.map +1 -0
  66. package/dist/flow/plugins/image-plugin.js +22 -0
  67. package/dist/flow/plugins/types/describe-image-node.d.ts +16 -0
  68. package/dist/flow/plugins/types/describe-image-node.d.ts.map +1 -0
  69. package/dist/flow/plugins/types/describe-image-node.js +9 -0
  70. package/dist/flow/plugins/types/index.d.ts +9 -0
  71. package/dist/flow/plugins/types/index.d.ts.map +1 -0
  72. package/dist/flow/plugins/types/index.js +8 -0
  73. package/dist/flow/plugins/types/optimize-node.d.ts +20 -0
  74. package/dist/flow/plugins/types/optimize-node.d.ts.map +1 -0
  75. package/dist/flow/plugins/types/optimize-node.js +11 -0
  76. package/dist/flow/plugins/types/remove-background-node.d.ts +16 -0
  77. package/dist/flow/plugins/types/remove-background-node.d.ts.map +1 -0
  78. package/dist/flow/plugins/types/remove-background-node.js +9 -0
  79. package/dist/flow/plugins/types/resize-node.d.ts +21 -0
  80. package/dist/flow/plugins/types/resize-node.d.ts.map +1 -0
  81. package/dist/flow/plugins/types/resize-node.js +16 -0
  82. package/dist/flow/plugins/zip-plugin.d.ts +62 -0
  83. package/dist/flow/plugins/zip-plugin.d.ts.map +1 -0
  84. package/dist/flow/plugins/zip-plugin.js +21 -0
  85. package/dist/flow/typed-flow.d.ts +90 -0
  86. package/dist/flow/typed-flow.d.ts.map +1 -0
  87. package/dist/flow/typed-flow.js +59 -0
  88. package/dist/flow/types/flow-file.d.ts +45 -0
  89. package/dist/flow/types/flow-file.d.ts.map +1 -0
  90. package/dist/flow/types/flow-file.js +27 -0
  91. package/dist/flow/types/flow-job.d.ts +118 -0
  92. package/dist/flow/types/flow-job.d.ts.map +1 -0
  93. package/dist/flow/types/flow-job.js +11 -0
  94. package/dist/flow/types/flow-types.d.ts +321 -0
  95. package/dist/flow/types/flow-types.d.ts.map +1 -0
  96. package/dist/flow/types/flow-types.js +52 -0
  97. package/dist/flow/types/index.d.ts +4 -0
  98. package/dist/flow/types/index.d.ts.map +1 -0
  99. package/dist/flow/types/index.js +3 -0
  100. package/dist/flow/types/run-args.d.ts +38 -0
  101. package/dist/flow/types/run-args.d.ts.map +1 -0
  102. package/dist/flow/types/run-args.js +30 -0
  103. package/dist/flow/types/type-validator.d.ts +26 -0
  104. package/dist/flow/types/type-validator.d.ts.map +1 -0
  105. package/dist/flow/types/type-validator.js +134 -0
  106. package/dist/flow/utils/resolve-upload-metadata.d.ts +11 -0
  107. package/dist/flow/utils/resolve-upload-metadata.d.ts.map +1 -0
  108. package/dist/flow/utils/resolve-upload-metadata.js +28 -0
  109. package/dist/flow-2zXnEiWL.cjs +1 -0
  110. package/dist/flow-CRaKy7Vj.js +2 -0
  111. package/dist/flow-CRaKy7Vj.js.map +1 -0
  112. package/dist/generate-id-Dm-Vboxq.d.ts +34 -0
  113. package/dist/generate-id-Dm-Vboxq.d.ts.map +1 -0
  114. package/dist/generate-id-LjJRLD6N.d.cts +34 -0
  115. package/dist/generate-id-LjJRLD6N.d.cts.map +1 -0
  116. package/dist/generate-id-xHp_Z7Cl.cjs +1 -0
  117. package/dist/generate-id-yohS1ZDk.js +2 -0
  118. package/dist/generate-id-yohS1ZDk.js.map +1 -0
  119. package/dist/index-BO8GZlbD.d.cts +1040 -0
  120. package/dist/index-BO8GZlbD.d.cts.map +1 -0
  121. package/dist/index-BoGG5KAY.d.ts +1 -0
  122. package/dist/index-BtBZHVmz.d.cts +1 -0
  123. package/dist/index-D-CoVpkZ.d.ts +1004 -0
  124. package/dist/index-D-CoVpkZ.d.ts.map +1 -0
  125. package/dist/index.cjs +1 -0
  126. package/dist/index.d.cts +6 -0
  127. package/dist/index.d.ts +5 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +5 -0
  130. package/dist/logger/logger.cjs +1 -0
  131. package/dist/logger/logger.d.cts +8 -0
  132. package/dist/logger/logger.d.cts.map +1 -0
  133. package/dist/logger/logger.d.ts +5 -0
  134. package/dist/logger/logger.d.ts.map +1 -0
  135. package/dist/logger/logger.js +10 -0
  136. package/dist/logger/logger.js.map +1 -0
  137. package/dist/semaphore-0ZwjVpyF.js +2 -0
  138. package/dist/semaphore-0ZwjVpyF.js.map +1 -0
  139. package/dist/semaphore-BHprIjFI.d.cts +37 -0
  140. package/dist/semaphore-BHprIjFI.d.cts.map +1 -0
  141. package/dist/semaphore-DThupBkc.d.ts +37 -0
  142. package/dist/semaphore-DThupBkc.d.ts.map +1 -0
  143. package/dist/semaphore-DVrONiAV.cjs +1 -0
  144. package/dist/stream-limiter-CoWKv39w.js +2 -0
  145. package/dist/stream-limiter-CoWKv39w.js.map +1 -0
  146. package/dist/stream-limiter-JgOwmkMa.cjs +1 -0
  147. package/dist/streams/multi-stream.cjs +1 -0
  148. package/dist/streams/multi-stream.d.cts +91 -0
  149. package/dist/streams/multi-stream.d.cts.map +1 -0
  150. package/dist/streams/multi-stream.d.ts +86 -0
  151. package/dist/streams/multi-stream.d.ts.map +1 -0
  152. package/dist/streams/multi-stream.js +149 -0
  153. package/dist/streams/multi-stream.js.map +1 -0
  154. package/dist/streams/stream-limiter.cjs +1 -0
  155. package/dist/streams/stream-limiter.d.cts +36 -0
  156. package/dist/streams/stream-limiter.d.cts.map +1 -0
  157. package/dist/streams/stream-limiter.d.ts +27 -0
  158. package/dist/streams/stream-limiter.d.ts.map +1 -0
  159. package/dist/streams/stream-limiter.js +49 -0
  160. package/dist/streams/stream-splitter.cjs +1 -0
  161. package/dist/streams/stream-splitter.d.cts +68 -0
  162. package/dist/streams/stream-splitter.d.cts.map +1 -0
  163. package/dist/streams/stream-splitter.d.ts +51 -0
  164. package/dist/streams/stream-splitter.d.ts.map +1 -0
  165. package/dist/streams/stream-splitter.js +175 -0
  166. package/dist/streams/stream-splitter.js.map +1 -0
  167. package/dist/types/data-store-registry.d.ts +13 -0
  168. package/dist/types/data-store-registry.d.ts.map +1 -0
  169. package/dist/types/data-store-registry.js +4 -0
  170. package/dist/types/data-store.d.ts +316 -0
  171. package/dist/types/data-store.d.ts.map +1 -0
  172. package/dist/types/data-store.js +157 -0
  173. package/dist/types/event-broadcaster.d.ts +28 -0
  174. package/dist/types/event-broadcaster.d.ts.map +1 -0
  175. package/dist/types/event-broadcaster.js +6 -0
  176. package/dist/types/event-emitter.d.ts +378 -0
  177. package/dist/types/event-emitter.d.ts.map +1 -0
  178. package/dist/types/event-emitter.js +223 -0
  179. package/dist/types/index.cjs +1 -0
  180. package/dist/types/index.d.cts +6 -0
  181. package/dist/types/index.d.ts +10 -0
  182. package/dist/types/index.d.ts.map +1 -0
  183. package/dist/types/index.js +9 -0
  184. package/dist/types/input-file.d.ts +104 -0
  185. package/dist/types/input-file.d.ts.map +1 -0
  186. package/dist/types/input-file.js +27 -0
  187. package/dist/types/kv-store.d.ts +281 -0
  188. package/dist/types/kv-store.d.ts.map +1 -0
  189. package/dist/types/kv-store.js +234 -0
  190. package/dist/types/middleware.d.ts +17 -0
  191. package/dist/types/middleware.d.ts.map +1 -0
  192. package/dist/types/middleware.js +21 -0
  193. package/dist/types/upload-event.d.ts +105 -0
  194. package/dist/types/upload-event.d.ts.map +1 -0
  195. package/dist/types/upload-event.js +71 -0
  196. package/dist/types/upload-file.d.ts +136 -0
  197. package/dist/types/upload-file.d.ts.map +1 -0
  198. package/dist/types/upload-file.js +34 -0
  199. package/dist/types/websocket.d.ts +144 -0
  200. package/dist/types/websocket.d.ts.map +1 -0
  201. package/dist/types/websocket.js +40 -0
  202. package/dist/types-BT-cvi7T.cjs +1 -0
  203. package/dist/types-DhU2j-XF.js +2 -0
  204. package/dist/types-DhU2j-XF.js.map +1 -0
  205. package/dist/upload/convert-to-stream.d.ts +38 -0
  206. package/dist/upload/convert-to-stream.d.ts.map +1 -0
  207. package/dist/upload/convert-to-stream.js +43 -0
  208. package/dist/upload/convert-upload-to-flow-file.d.ts +14 -0
  209. package/dist/upload/convert-upload-to-flow-file.d.ts.map +1 -0
  210. package/dist/upload/convert-upload-to-flow-file.js +21 -0
  211. package/dist/upload/create-upload.d.ts +68 -0
  212. package/dist/upload/create-upload.d.ts.map +1 -0
  213. package/dist/upload/create-upload.js +157 -0
  214. package/dist/upload/index.cjs +1 -0
  215. package/dist/upload/index.d.cts +6 -0
  216. package/dist/upload/index.d.ts +4 -0
  217. package/dist/upload/index.d.ts.map +1 -0
  218. package/dist/upload/index.js +3 -0
  219. package/dist/upload/mime.d.ts +24 -0
  220. package/dist/upload/mime.d.ts.map +1 -0
  221. package/dist/upload/mime.js +351 -0
  222. package/dist/upload/upload-chunk.d.ts +58 -0
  223. package/dist/upload/upload-chunk.d.ts.map +1 -0
  224. package/dist/upload/upload-chunk.js +277 -0
  225. package/dist/upload/upload-server.d.ts +221 -0
  226. package/dist/upload/upload-server.d.ts.map +1 -0
  227. package/dist/upload/upload-server.js +181 -0
  228. package/dist/upload/upload-strategy-negotiator.d.ts +148 -0
  229. package/dist/upload/upload-strategy-negotiator.d.ts.map +1 -0
  230. package/dist/upload/upload-strategy-negotiator.js +217 -0
  231. package/dist/upload/upload-url.d.ts +68 -0
  232. package/dist/upload/upload-url.d.ts.map +1 -0
  233. package/dist/upload/upload-url.js +142 -0
  234. package/dist/upload/write-to-store.d.ts +77 -0
  235. package/dist/upload/write-to-store.d.ts.map +1 -0
  236. package/dist/upload/write-to-store.js +147 -0
  237. package/dist/upload-DLuICjpP.cjs +1 -0
  238. package/dist/upload-DaXO34dE.js +2 -0
  239. package/dist/upload-DaXO34dE.js.map +1 -0
  240. package/dist/uploadista-error-BB-Wdiz9.cjs +22 -0
  241. package/dist/uploadista-error-BVsVxqvz.js +23 -0
  242. package/dist/uploadista-error-BVsVxqvz.js.map +1 -0
  243. package/dist/uploadista-error-CwxYs4EB.d.ts +52 -0
  244. package/dist/uploadista-error-CwxYs4EB.d.ts.map +1 -0
  245. package/dist/uploadista-error-kKlhLRhY.d.cts +52 -0
  246. package/dist/uploadista-error-kKlhLRhY.d.cts.map +1 -0
  247. package/dist/utils/checksum.d.ts +22 -0
  248. package/dist/utils/checksum.d.ts.map +1 -0
  249. package/dist/utils/checksum.js +49 -0
  250. package/dist/utils/debounce.cjs +1 -0
  251. package/dist/utils/debounce.d.cts +38 -0
  252. package/dist/utils/debounce.d.cts.map +1 -0
  253. package/dist/utils/debounce.d.ts +36 -0
  254. package/dist/utils/debounce.d.ts.map +1 -0
  255. package/dist/utils/debounce.js +73 -0
  256. package/dist/utils/generate-id.cjs +1 -0
  257. package/dist/utils/generate-id.d.cts +2 -0
  258. package/dist/utils/generate-id.d.ts +32 -0
  259. package/dist/utils/generate-id.d.ts.map +1 -0
  260. package/dist/utils/generate-id.js +23 -0
  261. package/dist/utils/md5.cjs +1 -0
  262. package/dist/utils/md5.d.cts +73 -0
  263. package/dist/utils/md5.d.cts.map +1 -0
  264. package/dist/utils/md5.d.ts +71 -0
  265. package/dist/utils/md5.d.ts.map +1 -0
  266. package/dist/utils/md5.js +417 -0
  267. package/dist/utils/md5.js.map +1 -0
  268. package/dist/utils/once.cjs +1 -0
  269. package/dist/utils/once.d.cts +25 -0
  270. package/dist/utils/once.d.cts.map +1 -0
  271. package/dist/utils/once.d.ts +21 -0
  272. package/dist/utils/once.d.ts.map +1 -0
  273. package/dist/utils/once.js +54 -0
  274. package/dist/utils/once.js.map +1 -0
  275. package/dist/utils/semaphore.cjs +1 -0
  276. package/dist/utils/semaphore.d.cts +3 -0
  277. package/dist/utils/semaphore.d.ts +78 -0
  278. package/dist/utils/semaphore.d.ts.map +1 -0
  279. package/dist/utils/semaphore.js +134 -0
  280. package/dist/utils/throttle.cjs +1 -0
  281. package/dist/utils/throttle.d.cts +24 -0
  282. package/dist/utils/throttle.d.cts.map +1 -0
  283. package/dist/utils/throttle.d.ts +18 -0
  284. package/dist/utils/throttle.d.ts.map +1 -0
  285. package/dist/utils/throttle.js +20 -0
  286. package/dist/utils/throttle.js.map +1 -0
  287. package/docs/PARALLEL_EXECUTION.md +206 -0
  288. package/docs/PARALLEL_EXECUTION_QUICKSTART.md +142 -0
  289. package/docs/PARALLEL_EXECUTION_REFACTOR.md +184 -0
  290. package/package.json +80 -0
  291. package/src/errors/__tests__/uploadista-error.test.ts +251 -0
  292. package/src/errors/index.ts +2 -0
  293. package/src/errors/uploadista-error.ts +394 -0
  294. package/src/flow/README.md +352 -0
  295. package/src/flow/edge.test.ts +146 -0
  296. package/src/flow/edge.ts +60 -0
  297. package/src/flow/event.ts +229 -0
  298. package/src/flow/flow-server.ts +1089 -0
  299. package/src/flow/flow.ts +1050 -0
  300. package/src/flow/index.ts +28 -0
  301. package/src/flow/node.ts +249 -0
  302. package/src/flow/nodes/index.ts +8 -0
  303. package/src/flow/nodes/input-node.ts +296 -0
  304. package/src/flow/nodes/storage-node.ts +128 -0
  305. package/src/flow/nodes/transform-node.ts +154 -0
  306. package/src/flow/parallel-scheduler.ts +259 -0
  307. package/src/flow/plugins/credential-provider.ts +48 -0
  308. package/src/flow/plugins/image-ai-plugin.ts +66 -0
  309. package/src/flow/plugins/image-plugin.ts +60 -0
  310. package/src/flow/plugins/types/describe-image-node.ts +16 -0
  311. package/src/flow/plugins/types/index.ts +9 -0
  312. package/src/flow/plugins/types/optimize-node.ts +18 -0
  313. package/src/flow/plugins/types/remove-background-node.ts +18 -0
  314. package/src/flow/plugins/types/resize-node.ts +26 -0
  315. package/src/flow/plugins/zip-plugin.ts +69 -0
  316. package/src/flow/typed-flow.ts +279 -0
  317. package/src/flow/types/flow-file.ts +51 -0
  318. package/src/flow/types/flow-job.ts +138 -0
  319. package/src/flow/types/flow-types.ts +353 -0
  320. package/src/flow/types/index.ts +6 -0
  321. package/src/flow/types/run-args.ts +40 -0
  322. package/src/flow/types/type-validator.ts +204 -0
  323. package/src/flow/utils/resolve-upload-metadata.ts +48 -0
  324. package/src/index.ts +5 -0
  325. package/src/logger/logger.ts +14 -0
  326. package/src/streams/stream-limiter.test.ts +150 -0
  327. package/src/streams/stream-limiter.ts +75 -0
  328. package/src/types/data-store.ts +427 -0
  329. package/src/types/event-broadcaster.ts +39 -0
  330. package/src/types/event-emitter.ts +349 -0
  331. package/src/types/index.ts +9 -0
  332. package/src/types/input-file.ts +107 -0
  333. package/src/types/kv-store.ts +375 -0
  334. package/src/types/middleware.ts +54 -0
  335. package/src/types/upload-event.ts +75 -0
  336. package/src/types/upload-file.ts +139 -0
  337. package/src/types/websocket.ts +65 -0
  338. package/src/upload/convert-to-stream.ts +48 -0
  339. package/src/upload/create-upload.ts +214 -0
  340. package/src/upload/index.ts +3 -0
  341. package/src/upload/mime.ts +436 -0
  342. package/src/upload/upload-chunk.ts +364 -0
  343. package/src/upload/upload-server.ts +390 -0
  344. package/src/upload/upload-strategy-negotiator.ts +316 -0
  345. package/src/upload/upload-url.ts +173 -0
  346. package/src/upload/write-to-store.ts +211 -0
  347. package/src/utils/checksum.ts +61 -0
  348. package/src/utils/debounce.test.ts +126 -0
  349. package/src/utils/debounce.ts +89 -0
  350. package/src/utils/generate-id.ts +35 -0
  351. package/src/utils/md5.ts +475 -0
  352. package/src/utils/once.test.ts +83 -0
  353. package/src/utils/once.ts +63 -0
  354. package/src/utils/throttle.test.ts +101 -0
  355. package/src/utils/throttle.ts +29 -0
  356. package/tsconfig.json +20 -0
  357. package/tsconfig.tsbuildinfo +1 -0
  358. package/tsdown.config.ts +25 -0
  359. package/vitest.config.ts +15 -0
@@ -0,0 +1,1089 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import type { z } from "zod";
3
+ import { UploadistaError } from "../errors";
4
+ import {
5
+ createFlowWithSchema,
6
+ EventType,
7
+ type Flow,
8
+ type FlowData,
9
+ getFlowData,
10
+ runArgsSchema,
11
+ } from "../flow";
12
+ import type {
13
+ EventEmitter,
14
+ KvStore,
15
+ UploadFile,
16
+ WebSocketConnection,
17
+ } from "../types";
18
+ import { FlowEventEmitter, FlowJobKVStore } from "../types";
19
+ import { UploadServer } from "../upload";
20
+ import type { FlowEvent } from "./event";
21
+ import type { FlowJob } from "./types/flow-job";
22
+
23
+ /**
24
+ * Flow provider interface that applications must implement.
25
+ *
26
+ * This interface defines how the FlowServer retrieves flow definitions.
27
+ * Applications provide their own implementation to load flows from a database,
28
+ * configuration files, or any other source.
29
+ *
30
+ * @template TRequirements - Additional Effect requirements for flow execution
31
+ *
32
+ * @property getFlow - Retrieves a flow definition by ID with authorization check
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * // Implement a flow provider from database
37
+ * const dbFlowProvider: FlowProviderShape = {
38
+ * getFlow: (flowId, clientId) => Effect.gen(function* () {
39
+ * // Load flow from database
40
+ * const flowData = yield* db.getFlow(flowId);
41
+ *
42
+ * // Check authorization
43
+ * if (flowData.ownerId !== clientId) {
44
+ * return yield* Effect.fail(
45
+ * UploadistaError.fromCode("FLOW_NOT_AUTHORIZED")
46
+ * );
47
+ * }
48
+ *
49
+ * // Create flow instance
50
+ * return createFlow(flowData);
51
+ * })
52
+ * };
53
+ *
54
+ * // Provide to FlowServer
55
+ * const flowProviderLayer = Layer.succeed(FlowProvider, dbFlowProvider);
56
+ * ```
57
+ */
58
+ export type FlowProviderShape<TRequirements = any> = {
59
+ getFlow: (
60
+ flowId: string,
61
+ clientId: string | null,
62
+ ) => Effect.Effect<Flow<any, any, TRequirements>, UploadistaError>;
63
+ };
64
+
65
+ /**
66
+ * Effect-TS context tag for the FlowProvider service.
67
+ *
68
+ * Applications must provide an implementation of FlowProviderShape
69
+ * to enable the FlowServer to retrieve flow definitions.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * // Access FlowProvider in an Effect
74
+ * const effect = Effect.gen(function* () {
75
+ * const provider = yield* FlowProvider;
76
+ * const flow = yield* provider.getFlow("flow123", "client456");
77
+ * return flow;
78
+ * });
79
+ * ```
80
+ */
81
+ export class FlowProvider extends Context.Tag("FlowProvider")<
82
+ FlowProvider,
83
+ FlowProviderShape<any>
84
+ >() {}
85
+
86
+ /**
87
+ * FlowServer service interface.
88
+ *
89
+ * This is the core flow processing service that executes DAG-based file processing pipelines.
90
+ * It manages flow execution, job tracking, node processing, pause/resume functionality,
91
+ * and real-time event broadcasting.
92
+ *
93
+ * All operations return Effect types for composable, type-safe error handling.
94
+ *
95
+ * @property getFlow - Retrieves a flow definition by ID
96
+ * @property getFlowData - Retrieves flow metadata (nodes, edges) without full flow instance
97
+ * @property runFlow - Starts a new flow execution and returns immediately with job ID
98
+ * @property continueFlow - Resumes a paused flow with new data for a specific node
99
+ * @property getJobStatus - Retrieves current status and results of a flow job
100
+ * @property subscribeToFlowEvents - Subscribes WebSocket to flow execution events
101
+ * @property unsubscribeFromFlowEvents - Unsubscribes from flow events
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * // Execute a flow
106
+ * const program = Effect.gen(function* () {
107
+ * const server = yield* FlowServer;
108
+ *
109
+ * // Start flow execution (returns immediately)
110
+ * const job = yield* server.runFlow({
111
+ * flowId: "resize-optimize",
112
+ * storageId: "s3-production",
113
+ * clientId: "client123",
114
+ * inputs: {
115
+ * input_1: { uploadId: "upload_abc123" }
116
+ * }
117
+ * });
118
+ *
119
+ * // Subscribe to events
120
+ * yield* server.subscribeToFlowEvents(job.id, websocket);
121
+ *
122
+ * // Poll for status
123
+ * const status = yield* server.getJobStatus(job.id);
124
+ * console.log(status.status); // "running", "paused", "completed", or "failed"
125
+ *
126
+ * return job;
127
+ * });
128
+ *
129
+ * // Resume a paused flow
130
+ * const resume = Effect.gen(function* () {
131
+ * const server = yield* FlowServer;
132
+ *
133
+ * // Flow paused waiting for user input at node "approval_1"
134
+ * const job = yield* server.continueFlow({
135
+ * jobId: "job123",
136
+ * nodeId: "approval_1",
137
+ * newData: { approved: true },
138
+ * clientId: "client123"
139
+ * });
140
+ *
141
+ * return job;
142
+ * });
143
+ *
144
+ * // Check flow structure before execution
145
+ * const inspect = Effect.gen(function* () {
146
+ * const server = yield* FlowServer;
147
+ *
148
+ * const flowData = yield* server.getFlowData("resize-optimize", "client123");
149
+ * console.log("Nodes:", flowData.nodes);
150
+ * console.log("Edges:", flowData.edges);
151
+ *
152
+ * return flowData;
153
+ * });
154
+ * ```
155
+ */
156
+ export type FlowServerShape = {
157
+ getFlow: <TRequirements>(
158
+ flowId: string,
159
+ clientId: string | null,
160
+ ) => Effect.Effect<Flow<any, any, TRequirements>, UploadistaError>;
161
+
162
+ getFlowData: (
163
+ flowId: string,
164
+ clientId: string | null,
165
+ ) => Effect.Effect<FlowData, UploadistaError>;
166
+
167
+ runFlow: <TRequirements>({
168
+ flowId,
169
+ storageId,
170
+ clientId,
171
+ inputs,
172
+ }: {
173
+ flowId: string;
174
+ storageId: string;
175
+ clientId: string | null;
176
+ inputs: any;
177
+ }) => Effect.Effect<FlowJob, UploadistaError, TRequirements>;
178
+
179
+ continueFlow: <TRequirements>({
180
+ jobId,
181
+ nodeId,
182
+ newData,
183
+ clientId,
184
+ }: {
185
+ jobId: string;
186
+ nodeId: string;
187
+ newData: unknown;
188
+ clientId: string | null;
189
+ }) => Effect.Effect<FlowJob, UploadistaError, TRequirements>;
190
+
191
+ getJobStatus: (jobId: string) => Effect.Effect<FlowJob, UploadistaError>;
192
+
193
+ subscribeToFlowEvents: (
194
+ jobId: string,
195
+ connection: WebSocketConnection,
196
+ ) => Effect.Effect<void, UploadistaError>;
197
+
198
+ unsubscribeFromFlowEvents: (
199
+ jobId: string,
200
+ ) => Effect.Effect<void, UploadistaError>;
201
+ };
202
+
203
+ /**
204
+ * Effect-TS context tag for the FlowServer service.
205
+ *
206
+ * Use this tag to access the FlowServer in an Effect context.
207
+ * The server must be provided via a Layer or dependency injection.
208
+ *
209
+ * @example
210
+ * ```typescript
211
+ * // Access FlowServer in an Effect
212
+ * const flowEffect = Effect.gen(function* () {
213
+ * const server = yield* FlowServer;
214
+ * const job = yield* server.runFlow({
215
+ * flowId: "my-flow",
216
+ * storageId: "s3",
217
+ * clientId: null,
218
+ * inputs: {}
219
+ * });
220
+ * return job;
221
+ * });
222
+ *
223
+ * // Provide FlowServer layer
224
+ * const program = flowEffect.pipe(
225
+ * Effect.provide(flowServer),
226
+ * Effect.provide(flowProviderLayer),
227
+ * Effect.provide(flowJobKvStore)
228
+ * );
229
+ * ```
230
+ */
231
+ export class FlowServer extends Context.Tag("FlowServer")<
232
+ FlowServer,
233
+ FlowServerShape
234
+ >() {}
235
+
236
+ /**
237
+ * Legacy configuration options for FlowServer.
238
+ *
239
+ * @deprecated Use Effect Layers and FlowProvider instead.
240
+ * This type is kept for backward compatibility.
241
+ *
242
+ * @property getFlow - Function to retrieve flow definitions
243
+ * @property kvStore - KV store for flow job metadata
244
+ */
245
+ export type FlowServerOptions = {
246
+ getFlow: <TRequirements>({
247
+ flowId,
248
+ storageId,
249
+ }: {
250
+ flowId: string;
251
+ storageId: string;
252
+ }) => Promise<Flow<any, any, TRequirements>>;
253
+ kvStore: KvStore<FlowJob>;
254
+ };
255
+
256
+ const isResultUploadFile = (result: unknown): result is UploadFile => {
257
+ return typeof result === "object" && result !== null && "id" in result;
258
+ };
259
+
260
+ // Function to enhance a flow with event emission capabilities
261
+ function withFlowEvents<
262
+ TFlowInputSchema extends z.ZodSchema<any>,
263
+ TFlowOutputSchema extends z.ZodSchema<any>,
264
+ TRequirements,
265
+ >(
266
+ flow: Flow<TFlowInputSchema, TFlowOutputSchema, TRequirements>,
267
+ eventEmitter: EventEmitter<FlowEvent>,
268
+ kvStore: KvStore<FlowJob>,
269
+ ): Flow<TFlowInputSchema, TFlowOutputSchema, TRequirements> {
270
+ // Shared helper to create onEvent callback for a given jobId
271
+ const createOnEventCallback = (executionJobId: string) => {
272
+ // Helper to update job in KV store
273
+ const updateJobInStore = (updates: Partial<FlowJob>) =>
274
+ Effect.gen(function* () {
275
+ const job = yield* kvStore.get(executionJobId);
276
+ if (job) {
277
+ yield* kvStore.set(executionJobId, {
278
+ ...job,
279
+ ...updates,
280
+ updatedAt: new Date(),
281
+ });
282
+ }
283
+ });
284
+
285
+ // Create the onEvent callback that calls original onEvent, emits to eventEmitter, and updates job
286
+ return (event: FlowEvent) =>
287
+ Effect.gen(function* () {
288
+ // Call the original onEvent from the flow if it exists
289
+ // Catch errors to prevent them from blocking flow execution
290
+ if (flow.onEvent) {
291
+ yield* Effect.catchAll(flow.onEvent(event), (error) => {
292
+ // Log the error but don't fail the flow
293
+ Effect.logError("Original onEvent failed", error);
294
+ return Effect.succeed({ eventId: null });
295
+ });
296
+ }
297
+
298
+ // Emit event
299
+ yield* eventEmitter.emit(executionJobId, event);
300
+
301
+ Effect.logInfo(
302
+ `Updating job ${executionJobId} with event ${event.eventType}`,
303
+ );
304
+
305
+ // Update job based on event type
306
+ switch (event.eventType) {
307
+ case EventType.FlowStart:
308
+ yield* updateJobInStore({ status: "running" });
309
+ break;
310
+
311
+ case EventType.FlowEnd:
312
+ // Flow end is handled by executeFlowInBackground
313
+ // This case ensures the event is still emitted
314
+ break;
315
+
316
+ case EventType.FlowError:
317
+ yield* updateJobInStore({
318
+ status: "failed",
319
+ error: event.error,
320
+ });
321
+ break;
322
+
323
+ case EventType.NodeStart:
324
+ yield* Effect.gen(function* () {
325
+ const job = yield* kvStore.get(executionJobId);
326
+ if (job) {
327
+ const existingTask = job.tasks.find(
328
+ (t) => t.nodeId === event.nodeId,
329
+ );
330
+ const updatedTasks = existingTask
331
+ ? job.tasks.map((t) =>
332
+ t.nodeId === event.nodeId
333
+ ? {
334
+ ...t,
335
+ status: "running" as const,
336
+ updatedAt: new Date(),
337
+ }
338
+ : t,
339
+ )
340
+ : [
341
+ ...job.tasks,
342
+ {
343
+ nodeId: event.nodeId,
344
+ status: "running" as const,
345
+ createdAt: new Date(),
346
+ updatedAt: new Date(),
347
+ },
348
+ ];
349
+
350
+ yield* kvStore.set(executionJobId, {
351
+ ...job,
352
+ tasks: updatedTasks,
353
+ updatedAt: new Date(),
354
+ });
355
+ }
356
+ });
357
+ break;
358
+
359
+ case EventType.NodePause:
360
+ yield* Effect.gen(function* () {
361
+ const job = yield* kvStore.get(executionJobId);
362
+ if (job) {
363
+ const existingTask = job.tasks.find(
364
+ (t) => t.nodeId === event.nodeId,
365
+ );
366
+ const updatedTasks = existingTask
367
+ ? job.tasks.map((t) =>
368
+ t.nodeId === event.nodeId
369
+ ? {
370
+ ...t,
371
+ status: "paused" as const,
372
+ result: event.partialData,
373
+ updatedAt: new Date(),
374
+ }
375
+ : t,
376
+ )
377
+ : [
378
+ ...job.tasks,
379
+ {
380
+ nodeId: event.nodeId,
381
+ status: "paused" as const,
382
+ result: event.partialData,
383
+ createdAt: new Date(),
384
+ updatedAt: new Date(),
385
+ },
386
+ ];
387
+
388
+ yield* kvStore.set(executionJobId, {
389
+ ...job,
390
+ tasks: updatedTasks,
391
+ updatedAt: new Date(),
392
+ });
393
+ }
394
+ });
395
+ break;
396
+
397
+ case EventType.NodeResume:
398
+ yield* Effect.gen(function* () {
399
+ const job = yield* kvStore.get(executionJobId);
400
+ if (job) {
401
+ const updatedTasks = job.tasks.map((t) =>
402
+ t.nodeId === event.nodeId
403
+ ? {
404
+ ...t,
405
+ status: "running" as const,
406
+ updatedAt: new Date(),
407
+ }
408
+ : t,
409
+ );
410
+
411
+ yield* kvStore.set(executionJobId, {
412
+ ...job,
413
+ tasks: updatedTasks,
414
+ updatedAt: new Date(),
415
+ });
416
+ }
417
+ });
418
+ break;
419
+
420
+ case EventType.NodeEnd:
421
+ yield* Effect.gen(function* () {
422
+ const job = yield* kvStore.get(executionJobId);
423
+ if (job) {
424
+ const updatedTasks = job.tasks.map((t) =>
425
+ t.nodeId === event.nodeId
426
+ ? {
427
+ ...t,
428
+ status: "completed" as const,
429
+ result: event.result,
430
+ updatedAt: new Date(),
431
+ }
432
+ : t,
433
+ );
434
+
435
+ // Track intermediate files for cleanup
436
+ // Check if result is an UploadFile and node is not an output node
437
+ const node = flow.nodes.find((n) => n.id === event.nodeId);
438
+ const isOutputNode = node?.type === "output";
439
+ const result = event.result;
440
+
441
+ let intermediateFiles = job.intermediateFiles || [];
442
+
443
+ if (isOutputNode && isResultUploadFile(result) && result.id) {
444
+ // If this is an output node and it returns a file that was an intermediate file,
445
+ // remove it from the intermediate files list (it's now the final output)
446
+ intermediateFiles = intermediateFiles.filter(
447
+ (fileId) => fileId !== result.id,
448
+ );
449
+ } else if (
450
+ !isOutputNode &&
451
+ isResultUploadFile(result) &&
452
+ result.id
453
+ ) {
454
+ // Only add to intermediate files if it's not an output node
455
+ if (!intermediateFiles.includes(result.id)) {
456
+ intermediateFiles.push(result.id);
457
+ }
458
+ }
459
+
460
+ yield* kvStore.set(executionJobId, {
461
+ ...job,
462
+ tasks: updatedTasks,
463
+ intermediateFiles,
464
+ updatedAt: new Date(),
465
+ });
466
+ }
467
+ });
468
+ break;
469
+
470
+ case EventType.NodeError:
471
+ yield* Effect.gen(function* () {
472
+ const job = yield* kvStore.get(executionJobId);
473
+ if (job) {
474
+ const updatedTasks = job.tasks.map((t) =>
475
+ t.nodeId === event.nodeId
476
+ ? {
477
+ ...t,
478
+ status: "failed" as const,
479
+ error: event.error,
480
+ retryCount: event.retryCount,
481
+ updatedAt: new Date(),
482
+ }
483
+ : t,
484
+ );
485
+
486
+ yield* kvStore.set(executionJobId, {
487
+ ...job,
488
+ tasks: updatedTasks,
489
+ error: event.error,
490
+ updatedAt: new Date(),
491
+ });
492
+ }
493
+ });
494
+ break;
495
+ }
496
+
497
+ return { eventId: executionJobId };
498
+ });
499
+ };
500
+
501
+ return {
502
+ ...flow,
503
+ run: (args: {
504
+ inputs?: Record<string, z.infer<TFlowInputSchema>>;
505
+ storageId: string;
506
+ jobId?: string;
507
+ clientId: string | null;
508
+ }) => {
509
+ return Effect.gen(function* () {
510
+ // Use provided jobId or generate a new one
511
+ const executionJobId = args.jobId || crypto.randomUUID();
512
+
513
+ const onEventCallback = createOnEventCallback(executionJobId);
514
+
515
+ // Create a new flow with the same configuration but with onEvent callback
516
+ const flowWithEvents = yield* createFlowWithSchema({
517
+ flowId: flow.id,
518
+ name: flow.name,
519
+ nodes: flow.nodes,
520
+ edges: flow.edges,
521
+ inputSchema: flow.inputSchema,
522
+ outputSchema: flow.outputSchema,
523
+ onEvent: onEventCallback,
524
+ });
525
+
526
+ // Run the enhanced flow with consistent jobId
527
+ const result = yield* flowWithEvents.run({
528
+ ...args,
529
+ jobId: executionJobId,
530
+ clientId: args.clientId,
531
+ });
532
+
533
+ // Return the result directly (can be completed or paused)
534
+ return result;
535
+ });
536
+ },
537
+ resume: (args: {
538
+ jobId: string;
539
+ storageId: string;
540
+ nodeResults: Record<string, unknown>;
541
+ executionState: {
542
+ executionOrder: string[];
543
+ currentIndex: number;
544
+ inputs: Record<string, z.infer<TFlowInputSchema>>;
545
+ };
546
+ clientId: string | null;
547
+ }) => {
548
+ return Effect.gen(function* () {
549
+ const executionJobId = args.jobId;
550
+
551
+ const onEventCallback = createOnEventCallback(executionJobId);
552
+
553
+ // Create a new flow with the same configuration but with onEvent callback
554
+ const flowWithEvents = yield* createFlowWithSchema({
555
+ flowId: flow.id,
556
+ name: flow.name,
557
+ nodes: flow.nodes,
558
+ edges: flow.edges,
559
+ inputSchema: flow.inputSchema,
560
+ outputSchema: flow.outputSchema,
561
+ onEvent: onEventCallback,
562
+ });
563
+
564
+ // Resume the enhanced flow
565
+ const result = yield* flowWithEvents.resume(args);
566
+
567
+ // Return the result directly (can be completed or paused)
568
+ return result;
569
+ });
570
+ },
571
+ };
572
+ }
573
+
574
+ // Core FlowServer implementation
575
+ export function createFlowServer() {
576
+ return Effect.gen(function* () {
577
+ const flowProvider = yield* FlowProvider;
578
+ const eventEmitter = yield* FlowEventEmitter;
579
+ const kvStore = yield* FlowJobKVStore;
580
+ const uploadServer = yield* UploadServer;
581
+
582
+ const updateJob = (jobId: string, updates: Partial<FlowJob>) =>
583
+ Effect.gen(function* () {
584
+ const job = yield* kvStore.get(jobId);
585
+ if (!job) {
586
+ return yield* Effect.fail(
587
+ UploadistaError.fromCode("FLOW_JOB_NOT_FOUND", {
588
+ cause: `Job ${jobId} not found`,
589
+ }),
590
+ );
591
+ }
592
+ return yield* kvStore.set(jobId, { ...job, ...updates });
593
+ });
594
+
595
+ // Helper function to cleanup intermediate files
596
+ const cleanupIntermediateFiles = (jobId: string, clientId: string | null) =>
597
+ Effect.gen(function* () {
598
+ const job = yield* kvStore.get(jobId);
599
+ if (
600
+ !job ||
601
+ !job.intermediateFiles ||
602
+ job.intermediateFiles.length === 0
603
+ ) {
604
+ return;
605
+ }
606
+
607
+ yield* Effect.logInfo(
608
+ `Cleaning up ${job.intermediateFiles.length} intermediate files for job ${jobId}`,
609
+ );
610
+
611
+ // Delete each intermediate file
612
+ yield* Effect.all(
613
+ job.intermediateFiles.map((fileId) =>
614
+ Effect.gen(function* () {
615
+ yield* uploadServer.delete(fileId, clientId);
616
+ yield* Effect.logDebug(`Deleted intermediate file ${fileId}`);
617
+ }).pipe(
618
+ Effect.catchAll((error) =>
619
+ Effect.gen(function* () {
620
+ yield* Effect.logWarning(
621
+ `Failed to delete intermediate file ${fileId}: ${error}`,
622
+ );
623
+ return Effect.succeed(undefined);
624
+ }),
625
+ ),
626
+ ),
627
+ ),
628
+ { concurrency: 5 },
629
+ );
630
+
631
+ // Clear the intermediateFiles array
632
+ yield* updateJob(jobId, {
633
+ intermediateFiles: [],
634
+ });
635
+ });
636
+
637
+ // Helper function to execute flow in background
638
+ const executeFlowInBackground = ({
639
+ jobId,
640
+ flow,
641
+ storageId,
642
+ clientId,
643
+ inputs,
644
+ }: {
645
+ jobId: string;
646
+ flow: Flow<any, any, any>;
647
+ storageId: string;
648
+ clientId: string | null;
649
+ inputs: Record<string, any>;
650
+ }) =>
651
+ Effect.gen(function* () {
652
+ // Update job status to running
653
+ yield* updateJob(jobId, {
654
+ status: "running",
655
+ });
656
+
657
+ const flowWithEvents = withFlowEvents(flow, eventEmitter, kvStore);
658
+
659
+ // Run the flow with the consistent jobId
660
+ const result = yield* flowWithEvents.run({
661
+ inputs,
662
+ storageId,
663
+ jobId,
664
+ clientId,
665
+ });
666
+
667
+ // Handle result based on type
668
+ if (result.type === "paused") {
669
+ // Update job as paused (node results are in tasks, not executionState)
670
+ yield* updateJob(jobId, {
671
+ status: "paused",
672
+ pausedAt: result.nodeId,
673
+ executionState: result.executionState,
674
+ updatedAt: new Date(),
675
+ });
676
+ } else {
677
+ // Update job as completed with final result
678
+ yield* updateJob(jobId, {
679
+ status: "completed",
680
+ result: result.result,
681
+ updatedAt: new Date(),
682
+ endedAt: new Date(),
683
+ });
684
+
685
+ // Cleanup intermediate files
686
+ yield* cleanupIntermediateFiles(jobId, clientId);
687
+ }
688
+
689
+ return result;
690
+ }).pipe(
691
+ Effect.catchAll((error) =>
692
+ Effect.gen(function* () {
693
+ yield* Effect.logError("Flow execution failed", error);
694
+
695
+ // Convert error to a proper message
696
+ const errorMessage =
697
+ error instanceof UploadistaError ? error.body : String(error);
698
+
699
+ yield* Effect.logInfo(
700
+ `Updating job ${jobId} to failed status with error: ${errorMessage}`,
701
+ );
702
+
703
+ // Update job as failed - do this FIRST before cleanup
704
+ yield* updateJob(jobId, {
705
+ status: "failed",
706
+ error: errorMessage,
707
+ updatedAt: new Date(),
708
+ }).pipe(
709
+ Effect.catchAll((updateError) =>
710
+ Effect.gen(function* () {
711
+ yield* Effect.logError(
712
+ `Failed to update job ${jobId}`,
713
+ updateError,
714
+ );
715
+ return Effect.succeed(undefined);
716
+ }),
717
+ ),
718
+ );
719
+
720
+ // Emit FlowError event to notify client
721
+ const job = yield* kvStore.get(jobId);
722
+ if (job) {
723
+ yield* eventEmitter
724
+ .emit(jobId, {
725
+ jobId,
726
+ eventType: EventType.FlowError,
727
+ flowId: job.flowId,
728
+ error: errorMessage,
729
+ })
730
+ .pipe(
731
+ Effect.catchAll((emitError) =>
732
+ Effect.gen(function* () {
733
+ yield* Effect.logError(
734
+ `Failed to emit FlowError event for job ${jobId}`,
735
+ emitError,
736
+ );
737
+ return Effect.succeed(undefined);
738
+ }),
739
+ ),
740
+ );
741
+ }
742
+
743
+ // Cleanup intermediate files even on failure (don't let this fail the error handling)
744
+ yield* cleanupIntermediateFiles(jobId, clientId).pipe(
745
+ Effect.catchAll((cleanupError) =>
746
+ Effect.gen(function* () {
747
+ yield* Effect.logWarning(
748
+ `Failed to cleanup intermediate files for job ${jobId}`,
749
+ cleanupError,
750
+ );
751
+ return Effect.succeed(undefined);
752
+ }),
753
+ ),
754
+ );
755
+
756
+ return Effect.fail(error);
757
+ }),
758
+ ),
759
+ );
760
+
761
+ return {
762
+ getFlow: (flowId, clientId) =>
763
+ Effect.gen(function* () {
764
+ const flow = yield* flowProvider.getFlow(flowId, clientId);
765
+ return flow;
766
+ }),
767
+
768
+ getFlowData: (flowId, clientId) =>
769
+ Effect.gen(function* () {
770
+ const flow = yield* flowProvider.getFlow(flowId, clientId);
771
+ return getFlowData(flow);
772
+ }),
773
+
774
+ runFlow: ({
775
+ flowId,
776
+ storageId,
777
+ clientId,
778
+ inputs,
779
+ }: {
780
+ flowId: string;
781
+ storageId: string;
782
+ clientId: string | null;
783
+ inputs: unknown;
784
+ }) =>
785
+ Effect.gen(function* () {
786
+ const parsedParams = yield* Effect.try({
787
+ try: () => runArgsSchema.parse({ inputs }),
788
+ catch: (error) =>
789
+ UploadistaError.fromCode("FLOW_INPUT_VALIDATION_ERROR", {
790
+ cause: error,
791
+ }),
792
+ });
793
+
794
+ // Generate a unique jobId
795
+ const jobId = crypto.randomUUID();
796
+ const createdAt = new Date();
797
+
798
+ // Store initial job metadata
799
+ const job: FlowJob = {
800
+ id: jobId,
801
+ flowId,
802
+ storageId,
803
+ clientId,
804
+ status: "started",
805
+ createdAt,
806
+ updatedAt: createdAt,
807
+ tasks: [],
808
+ };
809
+
810
+ yield* kvStore.set(jobId, job);
811
+
812
+ // Get the flow and start background execution
813
+ const flow = yield* flowProvider.getFlow(flowId, clientId);
814
+
815
+ // Fork the flow execution to run in background as daemon
816
+ yield* Effect.forkDaemon(
817
+ executeFlowInBackground({
818
+ jobId,
819
+ flow,
820
+ storageId,
821
+ clientId,
822
+ inputs: parsedParams.inputs,
823
+ }).pipe(
824
+ Effect.tapErrorCause((cause) =>
825
+ Effect.logError("Flow execution failed", cause),
826
+ ),
827
+ ),
828
+ );
829
+
830
+ // Return immediately with jobId
831
+ return job;
832
+ }),
833
+
834
+ getJobStatus: (jobId: string) =>
835
+ Effect.gen(function* () {
836
+ const job = yield* kvStore.get(jobId);
837
+ if (!job) {
838
+ return yield* Effect.fail(
839
+ UploadistaError.fromCode("FLOW_JOB_NOT_FOUND", {
840
+ cause: `Job ${jobId} not found`,
841
+ }),
842
+ );
843
+ }
844
+
845
+ return job;
846
+ }),
847
+
848
+ continueFlow: ({
849
+ jobId,
850
+ nodeId,
851
+ newData,
852
+ clientId,
853
+ }: {
854
+ jobId: string;
855
+ nodeId: string;
856
+ newData: unknown;
857
+ clientId: string | null;
858
+ }) =>
859
+ Effect.gen(function* () {
860
+ console.log("continueFlow", jobId, nodeId, newData);
861
+ // Get the current job
862
+ const job = yield* kvStore.get(jobId);
863
+ if (!job) {
864
+ console.error("Job not found");
865
+ return yield* Effect.fail(
866
+ UploadistaError.fromCode("FLOW_JOB_NOT_FOUND", {
867
+ cause: `Job ${jobId} not found`,
868
+ }),
869
+ );
870
+ }
871
+
872
+ // Verify job is paused
873
+ if (job.status !== "paused") {
874
+ console.error("Job is not paused");
875
+ return yield* Effect.fail(
876
+ UploadistaError.fromCode("FLOW_JOB_ERROR", {
877
+ cause: `Job ${jobId} is not paused (status: ${job.status})`,
878
+ }),
879
+ );
880
+ }
881
+
882
+ // Verify it's paused at the expected node
883
+ if (job.pausedAt !== nodeId) {
884
+ console.error("Job is not paused at the expected node");
885
+ return yield* Effect.fail(
886
+ UploadistaError.fromCode("FLOW_JOB_ERROR", {
887
+ cause: `Job ${jobId} is paused at node ${job.pausedAt}, not ${nodeId}`,
888
+ }),
889
+ );
890
+ }
891
+
892
+ // Verify we have execution state
893
+ if (!job.executionState) {
894
+ console.error("Job has no execution state");
895
+ return yield* Effect.fail(
896
+ UploadistaError.fromCode("FLOW_JOB_ERROR", {
897
+ cause: `Job ${jobId} has no execution state`,
898
+ }),
899
+ );
900
+ }
901
+
902
+ // Reconstruct nodeResults from tasks
903
+ const nodeResults = job.tasks.reduce(
904
+ (acc, task) => {
905
+ if (task.result !== undefined) {
906
+ acc[task.nodeId] = task.result;
907
+ }
908
+ return acc;
909
+ },
910
+ {} as Record<string, unknown>,
911
+ );
912
+
913
+ // Update with new data
914
+ const updatedNodeResults = {
915
+ ...nodeResults,
916
+ [nodeId]: newData,
917
+ };
918
+
919
+ const updatedInputs = {
920
+ ...job.executionState.inputs,
921
+ [nodeId]: newData,
922
+ };
923
+
924
+ // Update job status to running BEFORE forking background execution
925
+ // This ensures the status is updated synchronously before events start firing
926
+ yield* updateJob(jobId, {
927
+ status: "running",
928
+ });
929
+
930
+ // Get the flow
931
+ const flow = yield* flowProvider.getFlow(job.flowId, job.clientId);
932
+
933
+ // Helper to resume flow in background
934
+ const resumeFlowInBackground = Effect.gen(function* () {
935
+ const flowWithEvents = withFlowEvents(flow, eventEmitter, kvStore);
936
+
937
+ if (!job.executionState) {
938
+ return yield* Effect.fail(
939
+ UploadistaError.fromCode("FLOW_JOB_ERROR", {
940
+ cause: `Job ${jobId} has no execution state`,
941
+ }),
942
+ );
943
+ }
944
+
945
+ // Resume the flow with updated state
946
+ const result = yield* flowWithEvents.resume({
947
+ jobId,
948
+ storageId: job.storageId,
949
+ nodeResults: updatedNodeResults,
950
+ executionState: {
951
+ ...job.executionState,
952
+ inputs: updatedInputs,
953
+ },
954
+ clientId: job.clientId,
955
+ });
956
+
957
+ // Handle result based on type
958
+ if (result.type === "paused") {
959
+ // Update job as paused again (node results are in tasks, not executionState)
960
+ yield* updateJob(jobId, {
961
+ status: "paused",
962
+ pausedAt: result.nodeId,
963
+ executionState: result.executionState,
964
+ updatedAt: new Date(),
965
+ });
966
+ } else {
967
+ // Update job as completed with final result
968
+ yield* updateJob(jobId, {
969
+ status: "completed",
970
+ pausedAt: undefined,
971
+ executionState: undefined,
972
+ result: result.result,
973
+ updatedAt: new Date(),
974
+ endedAt: new Date(),
975
+ });
976
+
977
+ // Cleanup intermediate files
978
+ yield* cleanupIntermediateFiles(jobId, clientId);
979
+ }
980
+
981
+ return result;
982
+ }).pipe(
983
+ Effect.catchAll((error) =>
984
+ Effect.gen(function* () {
985
+ yield* Effect.logError("Flow resume failed", error);
986
+
987
+ // Convert error to a proper message
988
+ const errorMessage =
989
+ error instanceof UploadistaError ? error.body : String(error);
990
+
991
+ yield* Effect.logInfo(
992
+ `Updating job ${jobId} to failed status with error: ${errorMessage}`,
993
+ );
994
+
995
+ // Update job as failed - do this FIRST before cleanup
996
+ yield* updateJob(jobId, {
997
+ status: "failed",
998
+ error: errorMessage,
999
+ updatedAt: new Date(),
1000
+ }).pipe(
1001
+ Effect.catchAll((updateError) =>
1002
+ Effect.gen(function* () {
1003
+ yield* Effect.logError(
1004
+ `Failed to update job ${jobId}`,
1005
+ updateError,
1006
+ );
1007
+ return Effect.succeed(undefined);
1008
+ }),
1009
+ ),
1010
+ );
1011
+
1012
+ // Emit FlowError event to notify client
1013
+ const currentJob = yield* kvStore.get(jobId);
1014
+ if (currentJob) {
1015
+ yield* eventEmitter
1016
+ .emit(jobId, {
1017
+ jobId,
1018
+ eventType: EventType.FlowError,
1019
+ flowId: currentJob.flowId,
1020
+ error: errorMessage,
1021
+ })
1022
+ .pipe(
1023
+ Effect.catchAll((emitError) =>
1024
+ Effect.gen(function* () {
1025
+ yield* Effect.logError(
1026
+ `Failed to emit FlowError event for job ${jobId}`,
1027
+ emitError,
1028
+ );
1029
+ return Effect.succeed(undefined);
1030
+ }),
1031
+ ),
1032
+ );
1033
+ }
1034
+
1035
+ // Cleanup intermediate files even on failure (don't let this fail the error handling)
1036
+ yield* cleanupIntermediateFiles(jobId, clientId).pipe(
1037
+ Effect.catchAll((cleanupError) =>
1038
+ Effect.gen(function* () {
1039
+ yield* Effect.logWarning(
1040
+ `Failed to cleanup intermediate files for job ${jobId}`,
1041
+ cleanupError,
1042
+ );
1043
+ return Effect.succeed(undefined);
1044
+ }),
1045
+ ),
1046
+ );
1047
+
1048
+ return Effect.fail(error);
1049
+ }),
1050
+ ),
1051
+ );
1052
+
1053
+ // Fork the resume execution to run in background as daemon
1054
+ yield* Effect.forkDaemon(
1055
+ resumeFlowInBackground.pipe(
1056
+ Effect.tapErrorCause((cause) =>
1057
+ Effect.logError("Flow resume failed", cause),
1058
+ ),
1059
+ ),
1060
+ );
1061
+
1062
+ // Return immediately with updated job
1063
+ const updatedJob = yield* kvStore.get(jobId);
1064
+ if (!updatedJob) {
1065
+ return yield* Effect.fail(
1066
+ UploadistaError.fromCode("FLOW_JOB_NOT_FOUND", {
1067
+ cause: `Job ${jobId} not found after update`,
1068
+ }),
1069
+ );
1070
+ }
1071
+ return updatedJob;
1072
+ }),
1073
+
1074
+ subscribeToFlowEvents: (jobId: string, connection: WebSocketConnection) =>
1075
+ Effect.gen(function* () {
1076
+ yield* eventEmitter.subscribe(jobId, connection);
1077
+ }),
1078
+
1079
+ unsubscribeFromFlowEvents: (jobId: string) =>
1080
+ Effect.gen(function* () {
1081
+ yield* eventEmitter.unsubscribe(jobId);
1082
+ }),
1083
+ } satisfies FlowServerShape;
1084
+ });
1085
+ }
1086
+
1087
+ // Export the FlowServer layer with job store dependency
1088
+ export const flowServer = Layer.effect(FlowServer, createFlowServer());
1089
+ export type FlowServerLayer = typeof flowServer;