@shipfox/api-logs 2.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 (342) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +179 -0
  5. package/LICENSE +21 -0
  6. package/README.md +151 -0
  7. package/dist/api/object-storage.d.ts +69 -0
  8. package/dist/api/object-storage.d.ts.map +1 -0
  9. package/dist/api/object-storage.js +199 -0
  10. package/dist/api/object-storage.js.map +1 -0
  11. package/dist/config.d.ts +20 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +108 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/core/append-logs.d.ts +28 -0
  16. package/dist/core/append-logs.d.ts.map +1 -0
  17. package/dist/core/append-logs.js +318 -0
  18. package/dist/core/append-logs.js.map +1 -0
  19. package/dist/core/budget.d.ts +12 -0
  20. package/dist/core/budget.d.ts.map +1 -0
  21. package/dist/core/budget.js +9 -0
  22. package/dist/core/budget.js.map +1 -0
  23. package/dist/core/close-stream.d.ts +27 -0
  24. package/dist/core/close-stream.d.ts.map +1 -0
  25. package/dist/core/close-stream.js +59 -0
  26. package/dist/core/close-stream.js.map +1 -0
  27. package/dist/core/compaction.d.ts +28 -0
  28. package/dist/core/compaction.d.ts.map +1 -0
  29. package/dist/core/compaction.js +49 -0
  30. package/dist/core/compaction.js.map +1 -0
  31. package/dist/core/entities/attempt-stream.d.ts +37 -0
  32. package/dist/core/entities/attempt-stream.d.ts.map +1 -0
  33. package/dist/core/entities/attempt-stream.js +15 -0
  34. package/dist/core/entities/attempt-stream.js.map +1 -0
  35. package/dist/core/entities/job-accounting.d.ts +12 -0
  36. package/dist/core/entities/job-accounting.d.ts.map +1 -0
  37. package/dist/core/entities/job-accounting.js +6 -0
  38. package/dist/core/entities/job-accounting.js.map +1 -0
  39. package/dist/core/entities/log-object.d.ts +13 -0
  40. package/dist/core/entities/log-object.d.ts.map +1 -0
  41. package/dist/core/entities/log-object.js +9 -0
  42. package/dist/core/entities/log-object.js.map +1 -0
  43. package/dist/core/errors.d.ts +26 -0
  44. package/dist/core/errors.d.ts.map +1 -0
  45. package/dist/core/errors.js +31 -0
  46. package/dist/core/errors.js.map +1 -0
  47. package/dist/core/finalize-attempt-stream.d.ts +22 -0
  48. package/dist/core/finalize-attempt-stream.d.ts.map +1 -0
  49. package/dist/core/finalize-attempt-stream.js +48 -0
  50. package/dist/core/finalize-attempt-stream.js.map +1 -0
  51. package/dist/core/read-logs.d.ts +33 -0
  52. package/dist/core/read-logs.d.ts.map +1 -0
  53. package/dist/core/read-logs.js +49 -0
  54. package/dist/core/read-logs.js.map +1 -0
  55. package/dist/core/reap-stale-open-streams.d.ts +27 -0
  56. package/dist/core/reap-stale-open-streams.d.ts.map +1 -0
  57. package/dist/core/reap-stale-open-streams.js +50 -0
  58. package/dist/core/reap-stale-open-streams.js.map +1 -0
  59. package/dist/core/retention.d.ts +32 -0
  60. package/dist/core/retention.d.ts.map +1 -0
  61. package/dist/core/retention.js +148 -0
  62. package/dist/core/retention.js.map +1 -0
  63. package/dist/core/session/claude/rows.d.ts +6 -0
  64. package/dist/core/session/claude/rows.d.ts.map +1 -0
  65. package/dist/core/session/claude/rows.js +145 -0
  66. package/dist/core/session/claude/rows.js.map +1 -0
  67. package/dist/core/session/claude-parser.d.ts +4 -0
  68. package/dist/core/session/claude-parser.d.ts.map +1 -0
  69. package/dist/core/session/claude-parser.js +42 -0
  70. package/dist/core/session/claude-parser.js.map +1 -0
  71. package/dist/core/session/entry-schema.d.ts +51 -0
  72. package/dist/core/session/entry-schema.d.ts.map +1 -0
  73. package/dist/core/session/entry-schema.js +61 -0
  74. package/dist/core/session/entry-schema.js.map +1 -0
  75. package/dist/core/session/object.d.ts +12 -0
  76. package/dist/core/session/object.d.ts.map +1 -0
  77. package/dist/core/session/object.js +50 -0
  78. package/dist/core/session/object.js.map +1 -0
  79. package/dist/core/session/parse-session.d.ts +5 -0
  80. package/dist/core/session/parse-session.d.ts.map +1 -0
  81. package/dist/core/session/parse-session.js +18 -0
  82. package/dist/core/session/parse-session.js.map +1 -0
  83. package/dist/core/session/pi/message-rows.d.ts +4 -0
  84. package/dist/core/session/pi/message-rows.d.ts.map +1 -0
  85. package/dist/core/session/pi/message-rows.js +113 -0
  86. package/dist/core/session/pi/message-rows.js.map +1 -0
  87. package/dist/core/session/pi/metadata.d.ts +18 -0
  88. package/dist/core/session/pi/metadata.d.ts.map +1 -0
  89. package/dist/core/session/pi/metadata.js +120 -0
  90. package/dist/core/session/pi/metadata.js.map +1 -0
  91. package/dist/core/session/pi/text.d.ts +12 -0
  92. package/dist/core/session/pi/text.d.ts.map +1 -0
  93. package/dist/core/session/pi/text.js +59 -0
  94. package/dist/core/session/pi/text.js.map +1 -0
  95. package/dist/core/session/pi-parser.d.ts +5 -0
  96. package/dist/core/session/pi-parser.d.ts.map +1 -0
  97. package/dist/core/session/pi-parser.js +73 -0
  98. package/dist/core/session/pi-parser.js.map +1 -0
  99. package/dist/core/session/rows.d.ts +7 -0
  100. package/dist/core/session/rows.d.ts.map +1 -0
  101. package/dist/core/session/rows.js +39 -0
  102. package/dist/core/session/rows.js.map +1 -0
  103. package/dist/core/session/session-record.d.ts +5 -0
  104. package/dist/core/session/session-record.d.ts.map +1 -0
  105. package/dist/core/session/session-record.js +3 -0
  106. package/dist/core/session/session-record.js.map +1 -0
  107. package/dist/db/accounting.d.ts +33 -0
  108. package/dist/db/accounting.d.ts.map +1 -0
  109. package/dist/db/accounting.js +52 -0
  110. package/dist/db/accounting.js.map +1 -0
  111. package/dist/db/chunks.d.ts +62 -0
  112. package/dist/db/chunks.d.ts.map +1 -0
  113. package/dist/db/chunks.js +88 -0
  114. package/dist/db/chunks.js.map +1 -0
  115. package/dist/db/db.d.ts +752 -0
  116. package/dist/db/db.d.ts.map +1 -0
  117. package/dist/db/db.js +24 -0
  118. package/dist/db/db.js.map +1 -0
  119. package/dist/db/index.d.ts +2 -0
  120. package/dist/db/index.d.ts.map +1 -0
  121. package/dist/db/index.js +5 -0
  122. package/dist/db/index.js.map +1 -0
  123. package/dist/db/schema/attempt-streams.d.ts +304 -0
  124. package/dist/db/schema/attempt-streams.d.ts.map +1 -0
  125. package/dist/db/schema/attempt-streams.js +100 -0
  126. package/dist/db/schema/attempt-streams.js.map +1 -0
  127. package/dist/db/schema/chunks.d.ts +166 -0
  128. package/dist/db/schema/chunks.d.ts.map +1 -0
  129. package/dist/db/schema/chunks.js +47 -0
  130. package/dist/db/schema/chunks.js.map +1 -0
  131. package/dist/db/schema/common.d.ts +29 -0
  132. package/dist/db/schema/common.d.ts.map +1 -0
  133. package/dist/db/schema/common.js +11 -0
  134. package/dist/db/schema/common.js.map +1 -0
  135. package/dist/db/schema/job-accounting.d.ts +143 -0
  136. package/dist/db/schema/job-accounting.d.ts.map +1 -0
  137. package/dist/db/schema/job-accounting.js +43 -0
  138. package/dist/db/schema/job-accounting.js.map +1 -0
  139. package/dist/db/schema/outbox.d.ts +195 -0
  140. package/dist/db/schema/outbox.d.ts.map +1 -0
  141. package/dist/db/schema/outbox.js +5 -0
  142. package/dist/db/schema/outbox.js.map +1 -0
  143. package/dist/db/streams.d.ts +141 -0
  144. package/dist/db/streams.d.ts.map +1 -0
  145. package/dist/db/streams.js +197 -0
  146. package/dist/db/streams.js.map +1 -0
  147. package/dist/index.d.ts +4 -0
  148. package/dist/index.d.ts.map +1 -0
  149. package/dist/index.js +81 -0
  150. package/dist/index.js.map +1 -0
  151. package/dist/metrics/index.d.ts +2 -0
  152. package/dist/metrics/index.d.ts.map +1 -0
  153. package/dist/metrics/index.js +3 -0
  154. package/dist/metrics/index.js.map +1 -0
  155. package/dist/metrics/instance.d.ts +14 -0
  156. package/dist/metrics/instance.d.ts.map +1 -0
  157. package/dist/metrics/instance.js +16 -0
  158. package/dist/metrics/instance.js.map +1 -0
  159. package/dist/metrics/service.d.ts +2 -0
  160. package/dist/metrics/service.d.ts.map +1 -0
  161. package/dist/metrics/service.js +20 -0
  162. package/dist/metrics/service.js.map +1 -0
  163. package/dist/presentation/dto/read-logs.d.ts +4 -0
  164. package/dist/presentation/dto/read-logs.d.ts.map +1 -0
  165. package/dist/presentation/dto/read-logs.js +22 -0
  166. package/dist/presentation/dto/read-logs.js.map +1 -0
  167. package/dist/presentation/routes/append-logs.d.ts +2 -0
  168. package/dist/presentation/routes/append-logs.d.ts.map +1 -0
  169. package/dist/presentation/routes/append-logs.js +71 -0
  170. package/dist/presentation/routes/append-logs.js.map +1 -0
  171. package/dist/presentation/routes/index.d.ts +3 -0
  172. package/dist/presentation/routes/index.d.ts.map +1 -0
  173. package/dist/presentation/routes/index.js +32 -0
  174. package/dist/presentation/routes/index.js.map +1 -0
  175. package/dist/presentation/routes/read-logs.d.ts +2 -0
  176. package/dist/presentation/routes/read-logs.d.ts.map +1 -0
  177. package/dist/presentation/routes/read-logs.js +41 -0
  178. package/dist/presentation/routes/read-logs.js.map +1 -0
  179. package/dist/presentation/subscribers/on-job-terminated.d.ts +8 -0
  180. package/dist/presentation/subscribers/on-job-terminated.d.ts.map +1 -0
  181. package/dist/presentation/subscribers/on-job-terminated.js +32 -0
  182. package/dist/presentation/subscribers/on-job-terminated.js.map +1 -0
  183. package/dist/presentation/subscribers/on-log-stream-closed.d.ts +9 -0
  184. package/dist/presentation/subscribers/on-log-stream-closed.d.ts.map +1 -0
  185. package/dist/presentation/subscribers/on-log-stream-closed.js +31 -0
  186. package/dist/presentation/subscribers/on-log-stream-closed.js.map +1 -0
  187. package/dist/presentation/subscribers/on-step-attempt-terminated.d.ts +3 -0
  188. package/dist/presentation/subscribers/on-step-attempt-terminated.d.ts.map +1 -0
  189. package/dist/presentation/subscribers/on-step-attempt-terminated.js +14 -0
  190. package/dist/presentation/subscribers/on-step-attempt-terminated.js.map +1 -0
  191. package/dist/temporal/activities/close-abandoned-streams.d.ts +12 -0
  192. package/dist/temporal/activities/close-abandoned-streams.d.ts.map +1 -0
  193. package/dist/temporal/activities/close-abandoned-streams.js +33 -0
  194. package/dist/temporal/activities/close-abandoned-streams.js.map +1 -0
  195. package/dist/temporal/activities/compact-stream.d.ts +33 -0
  196. package/dist/temporal/activities/compact-stream.d.ts.map +1 -0
  197. package/dist/temporal/activities/compact-stream.js +95 -0
  198. package/dist/temporal/activities/compact-stream.js.map +1 -0
  199. package/dist/temporal/activities/compaction-reconcile.d.ts +16 -0
  200. package/dist/temporal/activities/compaction-reconcile.d.ts.map +1 -0
  201. package/dist/temporal/activities/compaction-reconcile.js +52 -0
  202. package/dist/temporal/activities/compaction-reconcile.js.map +1 -0
  203. package/dist/temporal/activities/index.d.ts +14 -0
  204. package/dist/temporal/activities/index.d.ts.map +1 -0
  205. package/dist/temporal/activities/index.js +16 -0
  206. package/dist/temporal/activities/index.js.map +1 -0
  207. package/dist/temporal/activities/reap-stale-open-streams.d.ts +4 -0
  208. package/dist/temporal/activities/reap-stale-open-streams.d.ts.map +1 -0
  209. package/dist/temporal/activities/reap-stale-open-streams.js +11 -0
  210. package/dist/temporal/activities/reap-stale-open-streams.js.map +1 -0
  211. package/dist/temporal/activities/retention-sweep.d.ts +7 -0
  212. package/dist/temporal/activities/retention-sweep.d.ts.map +1 -0
  213. package/dist/temporal/activities/retention-sweep.js +19 -0
  214. package/dist/temporal/activities/retention-sweep.js.map +1 -0
  215. package/dist/temporal/constants.d.ts +7 -0
  216. package/dist/temporal/constants.d.ts.map +1 -0
  217. package/dist/temporal/constants.js +13 -0
  218. package/dist/temporal/constants.js.map +1 -0
  219. package/dist/temporal/workflows/close-abandoned-streams.d.ts +11 -0
  220. package/dist/temporal/workflows/close-abandoned-streams.d.ts.map +1 -0
  221. package/dist/temporal/workflows/close-abandoned-streams.js +22 -0
  222. package/dist/temporal/workflows/close-abandoned-streams.js.map +1 -0
  223. package/dist/temporal/workflows/compact-stream.d.ts +6 -0
  224. package/dist/temporal/workflows/compact-stream.d.ts.map +1 -0
  225. package/dist/temporal/workflows/compact-stream.js +25 -0
  226. package/dist/temporal/workflows/compact-stream.js.map +1 -0
  227. package/dist/temporal/workflows/compaction-reconcile-cron.d.ts +3 -0
  228. package/dist/temporal/workflows/compaction-reconcile-cron.d.ts.map +1 -0
  229. package/dist/temporal/workflows/compaction-reconcile-cron.js +15 -0
  230. package/dist/temporal/workflows/compaction-reconcile-cron.js.map +1 -0
  231. package/dist/temporal/workflows/index.d.ts +6 -0
  232. package/dist/temporal/workflows/index.d.ts.map +1 -0
  233. package/dist/temporal/workflows/index.js +7 -0
  234. package/dist/temporal/workflows/index.js.map +1 -0
  235. package/dist/temporal/workflows/reap-stale-open-streams-cron.d.ts +2 -0
  236. package/dist/temporal/workflows/reap-stale-open-streams-cron.d.ts.map +1 -0
  237. package/dist/temporal/workflows/reap-stale-open-streams-cron.js +15 -0
  238. package/dist/temporal/workflows/reap-stale-open-streams-cron.js.map +1 -0
  239. package/dist/temporal/workflows/retention-sweep-cron.d.ts +2 -0
  240. package/dist/temporal/workflows/retention-sweep-cron.d.ts.map +1 -0
  241. package/dist/temporal/workflows/retention-sweep-cron.js +23 -0
  242. package/dist/temporal/workflows/retention-sweep-cron.js.map +1 -0
  243. package/dist/tsconfig.test.tsbuildinfo +1 -0
  244. package/drizzle/0000_initial.sql +65 -0
  245. package/drizzle/meta/0000_snapshot.json +529 -0
  246. package/drizzle/meta/_journal.json +13 -0
  247. package/drizzle.config.ts +7 -0
  248. package/package.json +84 -0
  249. package/src/api/object-storage-credentials.test.ts +70 -0
  250. package/src/api/object-storage.test.ts +64 -0
  251. package/src/api/object-storage.ts +226 -0
  252. package/src/config.test.ts +118 -0
  253. package/src/config.ts +130 -0
  254. package/src/core/append-logs.test.ts +563 -0
  255. package/src/core/append-logs.ts +384 -0
  256. package/src/core/budget.test.ts +30 -0
  257. package/src/core/budget.ts +19 -0
  258. package/src/core/close-stream.ts +72 -0
  259. package/src/core/compaction.ts +68 -0
  260. package/src/core/entities/attempt-stream.ts +38 -0
  261. package/src/core/entities/job-accounting.ts +11 -0
  262. package/src/core/entities/log-object.test.ts +25 -0
  263. package/src/core/entities/log-object.ts +18 -0
  264. package/src/core/errors.ts +37 -0
  265. package/src/core/finalize-attempt-stream.test.ts +90 -0
  266. package/src/core/finalize-attempt-stream.ts +59 -0
  267. package/src/core/read-logs.ts +77 -0
  268. package/src/core/reap-stale-open-streams.test.ts +137 -0
  269. package/src/core/reap-stale-open-streams.ts +60 -0
  270. package/src/core/retention.test.ts +388 -0
  271. package/src/core/retention.ts +184 -0
  272. package/src/core/session/claude/rows.ts +223 -0
  273. package/src/core/session/claude-parser.test.ts +199 -0
  274. package/src/core/session/claude-parser.ts +38 -0
  275. package/src/core/session/entry-schema.ts +82 -0
  276. package/src/core/session/object.ts +57 -0
  277. package/src/core/session/parse-session.ts +23 -0
  278. package/src/core/session/pi/message-rows.ts +211 -0
  279. package/src/core/session/pi/metadata.ts +148 -0
  280. package/src/core/session/pi/text.ts +83 -0
  281. package/src/core/session/pi-parser.test.ts +451 -0
  282. package/src/core/session/pi-parser.ts +107 -0
  283. package/src/core/session/rows.ts +38 -0
  284. package/src/core/session/session-record.ts +4 -0
  285. package/src/db/accounting.ts +81 -0
  286. package/src/db/chunks.ts +143 -0
  287. package/src/db/db.ts +22 -0
  288. package/src/db/index.ts +4 -0
  289. package/src/db/schema/attempt-streams.ts +106 -0
  290. package/src/db/schema/chunks.ts +43 -0
  291. package/src/db/schema/common.ts +12 -0
  292. package/src/db/schema/job-accounting.ts +38 -0
  293. package/src/db/schema/outbox.ts +4 -0
  294. package/src/db/streams.test.ts +47 -0
  295. package/src/db/streams.ts +368 -0
  296. package/src/index.ts +77 -0
  297. package/src/metrics/index.ts +1 -0
  298. package/src/metrics/instance.ts +33 -0
  299. package/src/metrics/service.ts +23 -0
  300. package/src/presentation/dto/read-logs.ts +24 -0
  301. package/src/presentation/routes/append-logs.test.ts +279 -0
  302. package/src/presentation/routes/append-logs.ts +66 -0
  303. package/src/presentation/routes/index.ts +27 -0
  304. package/src/presentation/routes/read-logs.test.ts +504 -0
  305. package/src/presentation/routes/read-logs.ts +38 -0
  306. package/src/presentation/subscribers/on-job-terminated.test.ts +64 -0
  307. package/src/presentation/subscribers/on-job-terminated.ts +26 -0
  308. package/src/presentation/subscribers/on-log-stream-closed.test.ts +63 -0
  309. package/src/presentation/subscribers/on-log-stream-closed.ts +26 -0
  310. package/src/presentation/subscribers/on-step-attempt-terminated.ts +16 -0
  311. package/src/temporal/activities/close-abandoned-streams.test.ts +84 -0
  312. package/src/temporal/activities/close-abandoned-streams.ts +30 -0
  313. package/src/temporal/activities/compact-stream.test.ts +221 -0
  314. package/src/temporal/activities/compact-stream.ts +123 -0
  315. package/src/temporal/activities/compaction-reconcile.test.ts +143 -0
  316. package/src/temporal/activities/compaction-reconcile.ts +48 -0
  317. package/src/temporal/activities/index.ts +15 -0
  318. package/src/temporal/activities/reap-stale-open-streams.test.ts +48 -0
  319. package/src/temporal/activities/reap-stale-open-streams.ts +14 -0
  320. package/src/temporal/activities/retention-sweep.test.ts +38 -0
  321. package/src/temporal/activities/retention-sweep.ts +23 -0
  322. package/src/temporal/constants.ts +14 -0
  323. package/src/temporal/workflows/close-abandoned-streams.ts +25 -0
  324. package/src/temporal/workflows/compact-stream.ts +26 -0
  325. package/src/temporal/workflows/compaction-reconcile-cron.ts +14 -0
  326. package/src/temporal/workflows/index.ts +5 -0
  327. package/src/temporal/workflows/reap-stale-open-streams-cron.ts +13 -0
  328. package/src/temporal/workflows/retention-sweep-cron.ts +23 -0
  329. package/test/env.ts +37 -0
  330. package/test/factories/job-accounting.ts +38 -0
  331. package/test/fixtures/closed-stream.ts +53 -0
  332. package/test/fixtures/lease-token.ts +42 -0
  333. package/test/fixtures/ndjson.ts +34 -0
  334. package/test/globalSetup.ts +44 -0
  335. package/test/index.ts +9 -0
  336. package/test/queries.ts +54 -0
  337. package/test/setup.ts +17 -0
  338. package/tsconfig.build.json +9 -0
  339. package/tsconfig.build.tsbuildinfo +1 -0
  340. package/tsconfig.json +3 -0
  341. package/tsconfig.test.json +8 -0
  342. package/vitest.config.ts +13 -0
@@ -0,0 +1,197 @@
1
+ import { and, asc, eq, getTableColumns, isNull, lt, notInArray, sql } from 'drizzle-orm';
2
+ import { LeaseStreamMismatchError } from '#core/errors.js';
3
+ import { db } from './db.js';
4
+ import { attemptStreams, toAttemptStream } from './schema/attempt-streams.js';
5
+ import { logChunks } from './schema/chunks.js';
6
+ /**
7
+ * Reads the stream for `(job, step, attempt)` without creating it. Used by the
8
+ * empty-body heartbeat path so a runner cannot mint rows with empty appends.
9
+ */ export async function getAttemptStream(tx, lookup) {
10
+ const [row] = await tx.select().from(attemptStreams).where(and(eq(attemptStreams.jobId, lookup.jobId), eq(attemptStreams.stepId, lookup.stepId), eq(attemptStreams.attempt, lookup.attempt)));
11
+ return row ? toAttemptStream(row) : null;
12
+ }
13
+ /**
14
+ * Reads the stream for `(step, attempt)` for the session read path, which holds no lease
15
+ * and so has no `jobId`. `stepId` is a globally unique uuidv7, so `(stepId, attempt)`
16
+ * selects at most one row even though stream identity is `(jobId, stepId, attempt)`. The
17
+ * caller authorizes against the row's denormalized `workspaceId`.
18
+ */ export async function getStreamByStepAttempt(lookup) {
19
+ const [row] = await db().select().from(attemptStreams).where(and(eq(attemptStreams.stepId, lookup.stepId), eq(attemptStreams.attempt, lookup.attempt)));
20
+ return row ? toAttemptStream(row) : null;
21
+ }
22
+ /**
23
+ * Loads the stream for `(job, step, attempt)`, creating it on first append.
24
+ * Scoped by `jobId` from the lease, so a lease never reaches another job's stream.
25
+ * The create path reports whether the row was inserted so callers can record
26
+ * first-open side effects only after their enclosing transaction commits.
27
+ */ export async function getOrCreateAttemptStreamWithStatus(tx, identity) {
28
+ const [row] = await tx.insert(attemptStreams).values(identity).onConflictDoUpdate({
29
+ target: [
30
+ attemptStreams.jobId,
31
+ attemptStreams.stepId,
32
+ attemptStreams.attempt
33
+ ],
34
+ set: {
35
+ updatedAt: sql`now()`
36
+ }
37
+ })// Postgres sets `xmax` only on the conflict-update path, so this keeps
38
+ // first-open detection in the single upsert round trip.
39
+ .returning({
40
+ ...getTableColumns(attemptStreams),
41
+ created: sql`xmax = 0`
42
+ });
43
+ if (!row) throw new Error('attempt stream missing after get-or-create');
44
+ if (row.workspaceId !== identity.workspaceId || row.projectId !== identity.projectId || row.workflowRunAttemptId !== identity.workflowRunAttemptId) {
45
+ throw new LeaseStreamMismatchError();
46
+ }
47
+ return {
48
+ stream: toAttemptStream(row),
49
+ created: row.created
50
+ };
51
+ }
52
+ export async function getOrCreateAttemptStream(tx, identity) {
53
+ const result = await getOrCreateAttemptStreamWithStatus(tx, identity);
54
+ return result.stream;
55
+ }
56
+ /**
57
+ * Offset-CAS extend: advances `committed_length` by `byteLen` iff it currently
58
+ * equals `offset`. On mismatch, re-reads the committed length and classifies the
59
+ * caller's chunk: a retry (fully already applied) only when `offset + byteLen`
60
+ * does not run past the committed length; otherwise a gap. A chunk that straddles
61
+ * the committed point (offset behind it, but extending past it) is a gap too, so
62
+ * the runner rewinds rather than getting a 200 that silently drops its tail.
63
+ */ export async function casExtendCommittedLength(tx, params) {
64
+ const extended = await tx.update(attemptStreams).set({
65
+ committedLength: sql`${attemptStreams.committedLength} + ${params.byteLen}`,
66
+ updatedAt: sql`now()`
67
+ }).where(and(eq(attemptStreams.id, params.streamId), eq(attemptStreams.committedLength, params.offset))).returning({
68
+ committedLength: attemptStreams.committedLength
69
+ });
70
+ if (extended[0]) return {
71
+ outcome: 'extended',
72
+ committedLength: extended[0].committedLength
73
+ };
74
+ const [current] = await tx.select({
75
+ committedLength: attemptStreams.committedLength
76
+ }).from(attemptStreams).where(eq(attemptStreams.id, params.streamId));
77
+ const committedLength = current?.committedLength ?? 0;
78
+ const fullyApplied = params.offset + params.byteLen <= committedLength;
79
+ return {
80
+ outcome: fullyApplied ? 'retry' : 'gap',
81
+ committedLength
82
+ };
83
+ }
84
+ export async function setDeclaredTotalBytes(tx, params) {
85
+ await tx.update(attemptStreams).set({
86
+ declaredTotalBytes: params.declaredTotalBytes,
87
+ updatedAt: sql`now()`
88
+ }).where(eq(attemptStreams.id, params.streamId));
89
+ }
90
+ /**
91
+ * Guarded close: flips `open → closed` and stamps `closed_at` iff the stream is
92
+ * still open, returning the closed row (or null when another path already closed
93
+ * it). The `WHERE state='open'` predicate is both the row lock and the
94
+ * exactly-once gate. `committed_length` is left untouched; it stays equal to the
95
+ * runner spool bytes. `truncated` is set only on the timeout path.
96
+ */ export async function markStreamClosed(tx, params) {
97
+ const [row] = await tx.update(attemptStreams).set({
98
+ state: 'closed',
99
+ closeReason: params.reason,
100
+ closedAt: sql`now()`,
101
+ updatedAt: sql`now()`,
102
+ ...params.markTruncated ? {
103
+ truncated: true
104
+ } : {}
105
+ }).where(and(eq(attemptStreams.id, params.streamId), eq(attemptStreams.state, 'open'))).returning();
106
+ return row ? toAttemptStream(row) : null;
107
+ }
108
+ /** Open streams of a job, for the timeout sweep to force-close after the grace period. */ export async function listOpenStreamsByJob(jobId) {
109
+ const rows = await db().select().from(attemptStreams).where(and(eq(attemptStreams.jobId, jobId), eq(attemptStreams.state, 'open')));
110
+ return rows.map(toAttemptStream);
111
+ }
112
+ /** For compaction, which is driven by the closed event that carries the stream id. */ export async function getAttemptStreamById(streamId) {
113
+ const [row] = await db().select().from(attemptStreams).where(eq(attemptStreams.id, streamId));
114
+ return row ? toAttemptStream(row) : null;
115
+ }
116
+ export async function getAttemptStreamByIdInTransaction(tx, streamId) {
117
+ const [row] = await tx.select().from(attemptStreams).where(eq(attemptStreams.id, streamId));
118
+ return row ? toAttemptStream(row) : null;
119
+ }
120
+ /**
121
+ * Final compaction step: records the object key and deletes the now-cold chunk rows in one
122
+ * transaction. The guard is `state='closed' AND object_key IS NULL`, so the publish is a
123
+ * single winner even when two compaction attempts (e.g. a heartbeat-timed-out run and its
124
+ * retry) race: each uploads to its own key, and only the first to land here writes a key and
125
+ * drops the chunks. A 0-row result means the row was either hard-deleted by retention or
126
+ * already published by another attempt; the caller re-reads to tell those apart and deletes
127
+ * its own now-orphaned upload either way. Returns whether this attempt won the publish.
128
+ */ export async function setObjectKeyAndDeleteChunks(tx, params) {
129
+ const updated = await tx.update(attemptStreams).set({
130
+ objectKey: params.objectKey,
131
+ updatedAt: sql`now()`
132
+ }).where(and(eq(attemptStreams.id, params.streamId), eq(attemptStreams.state, 'closed'), isNull(attemptStreams.objectKey))).returning({
133
+ id: attemptStreams.id
134
+ });
135
+ if (updated.length === 0) return {
136
+ updated: false
137
+ };
138
+ await tx.delete(logChunks).where(eq(logChunks.streamId, params.streamId));
139
+ return {
140
+ updated: true
141
+ };
142
+ }
143
+ /**
144
+ * Closed streams that never got an object key and have sat that way past the stale
145
+ * window: the reconcile cron re-drives compaction for these. Hits the partial index
146
+ * `logs_attempt_streams_uncompacted_idx`; ordered by `closed_at` so the oldest backlog
147
+ * drains first, bounded by `limit` per tick.
148
+ */ export async function listStaleUncompactedStreams(params) {
149
+ const rows = await db().select().from(attemptStreams).where(and(eq(attemptStreams.state, 'closed'), isNull(attemptStreams.objectKey), lt(attemptStreams.closedAt, sql`now() - make_interval(secs => ${params.olderThanSeconds})`))).orderBy(asc(attemptStreams.closedAt)).limit(params.limit);
150
+ return rows.map(toAttemptStream);
151
+ }
152
+ /**
153
+ * Lists open streams old enough to force-close. The age check is safe only when
154
+ * `olderThanSeconds` exceeds the lease TTL: leases are minted before a stream can
155
+ * be created, so no valid append can arrive after `created_at + leaseTTL`.
156
+ */ export async function listStaleOpenStreams(params) {
157
+ const rows = await db().select().from(attemptStreams).where(and(eq(attemptStreams.state, 'open'), lt(attemptStreams.createdAt, sql`now() - make_interval(secs => ${params.olderThanSeconds})`))).orderBy(asc(attemptStreams.createdAt)).limit(params.limit);
158
+ return rows.map(toAttemptStream);
159
+ }
160
+ export async function getOpenStreamCount() {
161
+ const [row] = await db().select({
162
+ value: sql`count(*)`.mapWith(BigInt)
163
+ }).from(attemptStreams).where(eq(attemptStreams.state, 'open'));
164
+ return row?.value ?? 0n;
165
+ }
166
+ /**
167
+ * Lists expired closed streams for retention. `excludeIds` keeps failed or raced rows from
168
+ * starving younger rows in the same run, and avoids cursoring on `closed_at`, whose microsecond
169
+ * precision would be lost through JS `Date`.
170
+ *
171
+ * No `object_key` filter: retention deletes compacted and never-compacted streams alike.
172
+ */ export async function listExpiredClosedStreams(params) {
173
+ const rows = await db().select().from(attemptStreams).where(and(eq(attemptStreams.state, 'closed'), lt(attemptStreams.closedAt, sql`now() - make_interval(days => ${params.retentionDays})`), params.excludeIds && params.excludeIds.length > 0 ? notInArray(attemptStreams.id, params.excludeIds) : undefined)).orderBy(asc(attemptStreams.closedAt), asc(attemptStreams.id)).limit(params.limit).for('update', {
174
+ skipLocked: true
175
+ });
176
+ return rows.map(toAttemptStream);
177
+ }
178
+ /**
179
+ * Hard-deletes one expired stream row, guarded on the observed `object_key` so a racing
180
+ * compaction publish leaves the row for the next sweep.
181
+ */ export async function deleteExpiredStream(tx, params) {
182
+ const [row] = await tx.delete(attemptStreams).where(and(eq(attemptStreams.id, params.streamId), params.observedObjectKey === null ? isNull(attemptStreams.objectKey) : eq(attemptStreams.objectKey, params.observedObjectKey))).returning({
183
+ jobId: attemptStreams.jobId
184
+ });
185
+ return {
186
+ deleted: Boolean(row),
187
+ jobId: row?.jobId ?? null
188
+ };
189
+ }
190
+ export async function accountingHasNoStreams(tx, jobId) {
191
+ const [row] = await tx.select({
192
+ id: attemptStreams.id
193
+ }).from(attemptStreams).where(eq(attemptStreams.jobId, jobId)).limit(1);
194
+ return !row;
195
+ }
196
+
197
+ //# sourceMappingURL=streams.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/db/streams.ts"],"sourcesContent":["import {and, asc, eq, getTableColumns, isNull, lt, notInArray, sql} from 'drizzle-orm';\nimport type {AttemptStream, StreamCloseReason} from '#core/entities/attempt-stream.js';\nimport {LeaseStreamMismatchError} from '#core/errors.js';\nimport {db, type Transaction} from './db.js';\nimport {attemptStreams, toAttemptStream} from './schema/attempt-streams.js';\nimport {logChunks} from './schema/chunks.js';\n\nexport interface AttemptStreamIdentity {\n jobId: string;\n stepId: string;\n attempt: number;\n workspaceId: string;\n projectId: string;\n workflowRunAttemptId: string;\n}\n\nexport interface AttemptStreamLookup {\n jobId: string;\n stepId: string;\n attempt: number;\n}\n\n/**\n * Reads the stream for `(job, step, attempt)` without creating it. Used by the\n * empty-body heartbeat path so a runner cannot mint rows with empty appends.\n */\nexport async function getAttemptStream(\n tx: Transaction,\n lookup: AttemptStreamLookup,\n): Promise<AttemptStream | null> {\n const [row] = await tx\n .select()\n .from(attemptStreams)\n .where(\n and(\n eq(attemptStreams.jobId, lookup.jobId),\n eq(attemptStreams.stepId, lookup.stepId),\n eq(attemptStreams.attempt, lookup.attempt),\n ),\n );\n return row ? toAttemptStream(row) : null;\n}\n\n/**\n * Reads the stream for `(step, attempt)` for the session read path, which holds no lease\n * and so has no `jobId`. `stepId` is a globally unique uuidv7, so `(stepId, attempt)`\n * selects at most one row even though stream identity is `(jobId, stepId, attempt)`. The\n * caller authorizes against the row's denormalized `workspaceId`.\n */\nexport async function getStreamByStepAttempt(lookup: {\n stepId: string;\n attempt: number;\n}): Promise<AttemptStream | null> {\n const [row] = await db()\n .select()\n .from(attemptStreams)\n .where(\n and(eq(attemptStreams.stepId, lookup.stepId), eq(attemptStreams.attempt, lookup.attempt)),\n );\n return row ? toAttemptStream(row) : null;\n}\n\nexport interface GetOrCreateAttemptStreamResult {\n stream: AttemptStream;\n created: boolean;\n}\n\n/**\n * Loads the stream for `(job, step, attempt)`, creating it on first append.\n * Scoped by `jobId` from the lease, so a lease never reaches another job's stream.\n * The create path reports whether the row was inserted so callers can record\n * first-open side effects only after their enclosing transaction commits.\n */\nexport async function getOrCreateAttemptStreamWithStatus(\n tx: Transaction,\n identity: AttemptStreamIdentity,\n): Promise<GetOrCreateAttemptStreamResult> {\n const [row] = await tx\n .insert(attemptStreams)\n .values(identity)\n .onConflictDoUpdate({\n target: [attemptStreams.jobId, attemptStreams.stepId, attemptStreams.attempt],\n set: {updatedAt: sql`now()`},\n })\n // Postgres sets `xmax` only on the conflict-update path, so this keeps\n // first-open detection in the single upsert round trip.\n .returning({...getTableColumns(attemptStreams), created: sql<boolean>`xmax = 0`});\n\n if (!row) throw new Error('attempt stream missing after get-or-create');\n if (\n row.workspaceId !== identity.workspaceId ||\n row.projectId !== identity.projectId ||\n row.workflowRunAttemptId !== identity.workflowRunAttemptId\n ) {\n throw new LeaseStreamMismatchError();\n }\n return {stream: toAttemptStream(row), created: row.created};\n}\n\nexport async function getOrCreateAttemptStream(\n tx: Transaction,\n identity: AttemptStreamIdentity,\n): Promise<AttemptStream> {\n const result = await getOrCreateAttemptStreamWithStatus(tx, identity);\n return result.stream;\n}\n\nexport type CasOutcome = 'extended' | 'retry' | 'gap';\n\nexport interface CasResult {\n outcome: CasOutcome;\n committedLength: number;\n}\n\n/**\n * Offset-CAS extend: advances `committed_length` by `byteLen` iff it currently\n * equals `offset`. On mismatch, re-reads the committed length and classifies the\n * caller's chunk: a retry (fully already applied) only when `offset + byteLen`\n * does not run past the committed length; otherwise a gap. A chunk that straddles\n * the committed point (offset behind it, but extending past it) is a gap too, so\n * the runner rewinds rather than getting a 200 that silently drops its tail.\n */\nexport async function casExtendCommittedLength(\n tx: Transaction,\n params: {streamId: string; offset: number; byteLen: number},\n): Promise<CasResult> {\n const extended = await tx\n .update(attemptStreams)\n .set({\n committedLength: sql`${attemptStreams.committedLength} + ${params.byteLen}`,\n updatedAt: sql`now()`,\n })\n .where(\n and(\n eq(attemptStreams.id, params.streamId),\n eq(attemptStreams.committedLength, params.offset),\n ),\n )\n .returning({committedLength: attemptStreams.committedLength});\n\n if (extended[0]) return {outcome: 'extended', committedLength: extended[0].committedLength};\n\n const [current] = await tx\n .select({committedLength: attemptStreams.committedLength})\n .from(attemptStreams)\n .where(eq(attemptStreams.id, params.streamId));\n const committedLength = current?.committedLength ?? 0;\n\n const fullyApplied = params.offset + params.byteLen <= committedLength;\n return {outcome: fullyApplied ? 'retry' : 'gap', committedLength};\n}\n\nexport async function setDeclaredTotalBytes(\n tx: Transaction,\n params: {streamId: string; declaredTotalBytes: number},\n): Promise<void> {\n await tx\n .update(attemptStreams)\n .set({declaredTotalBytes: params.declaredTotalBytes, updatedAt: sql`now()`})\n .where(eq(attemptStreams.id, params.streamId));\n}\n\n/**\n * Guarded close: flips `open → closed` and stamps `closed_at` iff the stream is\n * still open, returning the closed row (or null when another path already closed\n * it). The `WHERE state='open'` predicate is both the row lock and the\n * exactly-once gate. `committed_length` is left untouched; it stays equal to the\n * runner spool bytes. `truncated` is set only on the timeout path.\n */\nexport async function markStreamClosed(\n tx: Transaction,\n params: {streamId: string; reason: StreamCloseReason; markTruncated: boolean},\n): Promise<AttemptStream | null> {\n const [row] = await tx\n .update(attemptStreams)\n .set({\n state: 'closed',\n closeReason: params.reason,\n closedAt: sql`now()`,\n updatedAt: sql`now()`,\n ...(params.markTruncated ? {truncated: true} : {}),\n })\n .where(and(eq(attemptStreams.id, params.streamId), eq(attemptStreams.state, 'open')))\n .returning();\n\n return row ? toAttemptStream(row) : null;\n}\n\n/** Open streams of a job, for the timeout sweep to force-close after the grace period. */\nexport async function listOpenStreamsByJob(jobId: string): Promise<AttemptStream[]> {\n const rows = await db()\n .select()\n .from(attemptStreams)\n .where(and(eq(attemptStreams.jobId, jobId), eq(attemptStreams.state, 'open')));\n\n return rows.map(toAttemptStream);\n}\n\n/** For compaction, which is driven by the closed event that carries the stream id. */\nexport async function getAttemptStreamById(streamId: string): Promise<AttemptStream | null> {\n const [row] = await db().select().from(attemptStreams).where(eq(attemptStreams.id, streamId));\n return row ? toAttemptStream(row) : null;\n}\n\nexport async function getAttemptStreamByIdInTransaction(\n tx: Transaction,\n streamId: string,\n): Promise<AttemptStream | null> {\n const [row] = await tx.select().from(attemptStreams).where(eq(attemptStreams.id, streamId));\n return row ? toAttemptStream(row) : null;\n}\n\n/**\n * Final compaction step: records the object key and deletes the now-cold chunk rows in one\n * transaction. The guard is `state='closed' AND object_key IS NULL`, so the publish is a\n * single winner even when two compaction attempts (e.g. a heartbeat-timed-out run and its\n * retry) race: each uploads to its own key, and only the first to land here writes a key and\n * drops the chunks. A 0-row result means the row was either hard-deleted by retention or\n * already published by another attempt; the caller re-reads to tell those apart and deletes\n * its own now-orphaned upload either way. Returns whether this attempt won the publish.\n */\nexport async function setObjectKeyAndDeleteChunks(\n tx: Transaction,\n params: {streamId: string; objectKey: string},\n): Promise<{updated: boolean}> {\n const updated = await tx\n .update(attemptStreams)\n .set({objectKey: params.objectKey, updatedAt: sql`now()`})\n .where(\n and(\n eq(attemptStreams.id, params.streamId),\n eq(attemptStreams.state, 'closed'),\n isNull(attemptStreams.objectKey),\n ),\n )\n .returning({id: attemptStreams.id});\n\n if (updated.length === 0) return {updated: false};\n\n await tx.delete(logChunks).where(eq(logChunks.streamId, params.streamId));\n return {updated: true};\n}\n\n/**\n * Closed streams that never got an object key and have sat that way past the stale\n * window: the reconcile cron re-drives compaction for these. Hits the partial index\n * `logs_attempt_streams_uncompacted_idx`; ordered by `closed_at` so the oldest backlog\n * drains first, bounded by `limit` per tick.\n */\nexport async function listStaleUncompactedStreams(params: {\n olderThanSeconds: number;\n limit: number;\n}): Promise<AttemptStream[]> {\n const rows = await db()\n .select()\n .from(attemptStreams)\n .where(\n and(\n eq(attemptStreams.state, 'closed'),\n isNull(attemptStreams.objectKey),\n lt(attemptStreams.closedAt, sql`now() - make_interval(secs => ${params.olderThanSeconds})`),\n ),\n )\n .orderBy(asc(attemptStreams.closedAt))\n .limit(params.limit);\n\n return rows.map(toAttemptStream);\n}\n\n/**\n * Lists open streams old enough to force-close. The age check is safe only when\n * `olderThanSeconds` exceeds the lease TTL: leases are minted before a stream can\n * be created, so no valid append can arrive after `created_at + leaseTTL`.\n */\nexport async function listStaleOpenStreams(params: {\n olderThanSeconds: number;\n limit: number;\n}): Promise<AttemptStream[]> {\n const rows = await db()\n .select()\n .from(attemptStreams)\n .where(\n and(\n eq(attemptStreams.state, 'open'),\n lt(\n attemptStreams.createdAt,\n sql`now() - make_interval(secs => ${params.olderThanSeconds})`,\n ),\n ),\n )\n .orderBy(asc(attemptStreams.createdAt))\n .limit(params.limit);\n\n return rows.map(toAttemptStream);\n}\n\nexport async function getOpenStreamCount(): Promise<bigint> {\n const [row] = await db()\n .select({value: sql<bigint>`count(*)`.mapWith(BigInt)})\n .from(attemptStreams)\n .where(eq(attemptStreams.state, 'open'));\n\n return row?.value ?? 0n;\n}\n\n/**\n * Lists expired closed streams for retention. `excludeIds` keeps failed or raced rows from\n * starving younger rows in the same run, and avoids cursoring on `closed_at`, whose microsecond\n * precision would be lost through JS `Date`.\n *\n * No `object_key` filter: retention deletes compacted and never-compacted streams alike.\n */\nexport async function listExpiredClosedStreams(params: {\n retentionDays: number;\n limit: number;\n excludeIds?: string[] | undefined;\n}): Promise<AttemptStream[]> {\n const rows = await db()\n .select()\n .from(attemptStreams)\n .where(\n and(\n eq(attemptStreams.state, 'closed'),\n lt(attemptStreams.closedAt, sql`now() - make_interval(days => ${params.retentionDays})`),\n params.excludeIds && params.excludeIds.length > 0\n ? notInArray(attemptStreams.id, params.excludeIds)\n : undefined,\n ),\n )\n .orderBy(asc(attemptStreams.closedAt), asc(attemptStreams.id))\n .limit(params.limit)\n .for('update', {skipLocked: true});\n\n return rows.map(toAttemptStream);\n}\n\n/**\n * Hard-deletes one expired stream row, guarded on the observed `object_key` so a racing\n * compaction publish leaves the row for the next sweep.\n */\nexport async function deleteExpiredStream(\n tx: Transaction,\n params: {streamId: string; observedObjectKey: string | null},\n): Promise<{deleted: boolean; jobId: string | null}> {\n const [row] = await tx\n .delete(attemptStreams)\n .where(\n and(\n eq(attemptStreams.id, params.streamId),\n params.observedObjectKey === null\n ? isNull(attemptStreams.objectKey)\n : eq(attemptStreams.objectKey, params.observedObjectKey),\n ),\n )\n .returning({jobId: attemptStreams.jobId});\n\n return {deleted: Boolean(row), jobId: row?.jobId ?? null};\n}\n\nexport async function accountingHasNoStreams(tx: Transaction, jobId: string): Promise<boolean> {\n const [row] = await tx\n .select({id: attemptStreams.id})\n .from(attemptStreams)\n .where(eq(attemptStreams.jobId, jobId))\n .limit(1);\n\n return !row;\n}\n"],"names":["and","asc","eq","getTableColumns","isNull","lt","notInArray","sql","LeaseStreamMismatchError","db","attemptStreams","toAttemptStream","logChunks","getAttemptStream","tx","lookup","row","select","from","where","jobId","stepId","attempt","getStreamByStepAttempt","getOrCreateAttemptStreamWithStatus","identity","insert","values","onConflictDoUpdate","target","set","updatedAt","returning","created","Error","workspaceId","projectId","workflowRunAttemptId","stream","getOrCreateAttemptStream","result","casExtendCommittedLength","params","extended","update","committedLength","byteLen","id","streamId","offset","outcome","current","fullyApplied","setDeclaredTotalBytes","declaredTotalBytes","markStreamClosed","state","closeReason","reason","closedAt","markTruncated","truncated","listOpenStreamsByJob","rows","map","getAttemptStreamById","getAttemptStreamByIdInTransaction","setObjectKeyAndDeleteChunks","updated","objectKey","length","delete","listStaleUncompactedStreams","olderThanSeconds","orderBy","limit","listStaleOpenStreams","createdAt","getOpenStreamCount","value","mapWith","BigInt","listExpiredClosedStreams","retentionDays","excludeIds","undefined","for","skipLocked","deleteExpiredStream","observedObjectKey","deleted","Boolean","accountingHasNoStreams"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { type ShipfoxModule } from '@shipfox/node-module';
2
+ export { checkBucketReachable } from '#api/object-storage.js';
3
+ export declare const logsModule: ShipfoxModule;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAC,KAAK,aAAa,EAAoB,MAAM,sBAAsB,CAAC;AAY3E,OAAO,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAO5D,eAAO,MAAM,UAAU,EAAE,aAiDxB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,81 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { LOG_STREAM_CLOSED, logsEventSchemas } from '@shipfox/api-logs-dto';
4
+ import { WORKFLOWS_JOB_TERMINATED, WORKFLOWS_STEP_ATTEMPT_TERMINATED } from '@shipfox/api-workflows-dto';
5
+ import { subscriberFactory } from '@shipfox/node-module';
6
+ import { db } from '#db/db.js';
7
+ import { migrationsPath } from '#db/index.js';
8
+ import { logsOutbox } from '#db/schema/outbox.js';
9
+ import { registerLogsServiceMetrics } from '#metrics/service.js';
10
+ import { logsRoutes } from '#presentation/routes/index.js';
11
+ import { onJobTerminated } from '#presentation/subscribers/on-job-terminated.js';
12
+ import { onLogStreamClosed } from '#presentation/subscribers/on-log-stream-closed.js';
13
+ import { onStepAttemptTerminated } from '#presentation/subscribers/on-step-attempt-terminated.js';
14
+ import { createLogsActivities } from '#temporal/activities/index.js';
15
+ import { LOGS_COMPACTION_TASK_QUEUE, LOGS_LIFECYCLE_TASK_QUEUE } from '#temporal/constants.js';
16
+ export { checkBucketReachable } from '#api/object-storage.js';
17
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
18
+ const workflowsPath = resolve(packageRoot, 'dist/temporal/workflows/index.js');
19
+ const subscriber = subscriberFactory();
20
+ export const logsModule = {
21
+ name: 'logs',
22
+ database: {
23
+ db,
24
+ migrationsPath
25
+ },
26
+ routes: logsRoutes,
27
+ metrics: registerLogsServiceMetrics,
28
+ // `logs.stream.closed` is written by the close paths: the job-terminated subscriber
29
+ // force-closes streams the runner never ended, and the closed event drives compaction.
30
+ publishers: [
31
+ {
32
+ name: 'logs',
33
+ table: logsOutbox,
34
+ db,
35
+ eventSchemas: logsEventSchemas
36
+ }
37
+ ],
38
+ subscribers: [
39
+ subscriber(WORKFLOWS_STEP_ATTEMPT_TERMINATED, onStepAttemptTerminated),
40
+ subscriber(WORKFLOWS_JOB_TERMINATED, onJobTerminated),
41
+ subscriber(LOG_STREAM_CLOSED, onLogStreamClosed)
42
+ ],
43
+ workers: [
44
+ {
45
+ taskQueue: LOGS_LIFECYCLE_TASK_QUEUE,
46
+ workflowsPath,
47
+ activities: createLogsActivities,
48
+ // Temporal cron schedules are fixed after creation, so making cadence configurable would
49
+ // look adjustable while keeping the old schedule. Tune LOG_RETENTION_DAYS for the horizon.
50
+ workflows: [
51
+ {
52
+ name: 'retentionSweepCron',
53
+ id: 'logs-retention-sweep',
54
+ cronSchedule: '0 * * * *'
55
+ },
56
+ // Offset from retention's top-of-hour sweep; stale-stream age is governed by
57
+ // LOG_STREAM_REAP_AFTER_SECONDS.
58
+ {
59
+ name: 'reapStaleOpenStreamsCron',
60
+ id: 'logs-reap-stale-open-streams',
61
+ cronSchedule: '5,15,25,35,45,55 * * * *'
62
+ }
63
+ ]
64
+ },
65
+ // Compaction runs on its own queue so long uploads cannot starve the lifecycle sweep.
66
+ {
67
+ taskQueue: LOGS_COMPACTION_TASK_QUEUE,
68
+ workflowsPath,
69
+ activities: createLogsActivities,
70
+ workflows: [
71
+ {
72
+ name: 'compactionReconcileCron',
73
+ id: 'logs-compaction-reconcile',
74
+ cronSchedule: '*/10 * * * *'
75
+ }
76
+ ]
77
+ }
78
+ ]
79
+ };
80
+
81
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {dirname, resolve} from 'node:path';\nimport {fileURLToPath} from 'node:url';\nimport {LOG_STREAM_CLOSED, type LogsEventMap, logsEventSchemas} from '@shipfox/api-logs-dto';\nimport {\n WORKFLOWS_JOB_TERMINATED,\n WORKFLOWS_STEP_ATTEMPT_TERMINATED,\n type WorkflowsEventMapDto,\n} from '@shipfox/api-workflows-dto';\nimport {type ShipfoxModule, subscriberFactory} from '@shipfox/node-module';\nimport {db} from '#db/db.js';\nimport {migrationsPath} from '#db/index.js';\nimport {logsOutbox} from '#db/schema/outbox.js';\nimport {registerLogsServiceMetrics} from '#metrics/service.js';\nimport {logsRoutes} from '#presentation/routes/index.js';\nimport {onJobTerminated} from '#presentation/subscribers/on-job-terminated.js';\nimport {onLogStreamClosed} from '#presentation/subscribers/on-log-stream-closed.js';\nimport {onStepAttemptTerminated} from '#presentation/subscribers/on-step-attempt-terminated.js';\nimport {createLogsActivities} from '#temporal/activities/index.js';\nimport {LOGS_COMPACTION_TASK_QUEUE, LOGS_LIFECYCLE_TASK_QUEUE} from '#temporal/constants.js';\n\nexport {checkBucketReachable} from '#api/object-storage.js';\n\nconst packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');\nconst workflowsPath = resolve(packageRoot, 'dist/temporal/workflows/index.js');\n\nconst subscriber = subscriberFactory<WorkflowsEventMapDto & LogsEventMap>();\n\nexport const logsModule: ShipfoxModule = {\n name: 'logs',\n database: {db, migrationsPath},\n routes: logsRoutes,\n metrics: registerLogsServiceMetrics,\n // `logs.stream.closed` is written by the close paths: the job-terminated subscriber\n // force-closes streams the runner never ended, and the closed event drives compaction.\n publishers: [{name: 'logs', table: logsOutbox, db, eventSchemas: logsEventSchemas}],\n subscribers: [\n subscriber(WORKFLOWS_STEP_ATTEMPT_TERMINATED, onStepAttemptTerminated),\n subscriber(WORKFLOWS_JOB_TERMINATED, onJobTerminated),\n subscriber(LOG_STREAM_CLOSED, onLogStreamClosed),\n ],\n workers: [\n {\n taskQueue: LOGS_LIFECYCLE_TASK_QUEUE,\n workflowsPath,\n activities: createLogsActivities,\n // Temporal cron schedules are fixed after creation, so making cadence configurable would\n // look adjustable while keeping the old schedule. Tune LOG_RETENTION_DAYS for the horizon.\n workflows: [\n {\n name: 'retentionSweepCron',\n id: 'logs-retention-sweep',\n cronSchedule: '0 * * * *',\n },\n // Offset from retention's top-of-hour sweep; stale-stream age is governed by\n // LOG_STREAM_REAP_AFTER_SECONDS.\n {\n name: 'reapStaleOpenStreamsCron',\n id: 'logs-reap-stale-open-streams',\n cronSchedule: '5,15,25,35,45,55 * * * *',\n },\n ],\n },\n // Compaction runs on its own queue so long uploads cannot starve the lifecycle sweep.\n {\n taskQueue: LOGS_COMPACTION_TASK_QUEUE,\n workflowsPath,\n activities: createLogsActivities,\n workflows: [\n {\n name: 'compactionReconcileCron',\n id: 'logs-compaction-reconcile',\n cronSchedule: '*/10 * * * *',\n },\n ],\n },\n ],\n};\n"],"names":["dirname","resolve","fileURLToPath","LOG_STREAM_CLOSED","logsEventSchemas","WORKFLOWS_JOB_TERMINATED","WORKFLOWS_STEP_ATTEMPT_TERMINATED","subscriberFactory","db","migrationsPath","logsOutbox","registerLogsServiceMetrics","logsRoutes","onJobTerminated","onLogStreamClosed","onStepAttemptTerminated","createLogsActivities","LOGS_COMPACTION_TASK_QUEUE","LOGS_LIFECYCLE_TASK_QUEUE","checkBucketReachable","packageRoot","url","workflowsPath","subscriber","logsModule","name","database","routes","metrics","publishers","table","eventSchemas","subscribers","workers","taskQueue","activities","workflows","id","cronSchedule"],"mappings":"AAAA,SAAQA,OAAO,EAAEC,OAAO,QAAO,YAAY;AAC3C,SAAQC,aAAa,QAAO,WAAW;AACvC,SAAQC,iBAAiB,EAAqBC,gBAAgB,QAAO,wBAAwB;AAC7F,SACEC,wBAAwB,EACxBC,iCAAiC,QAE5B,6BAA6B;AACpC,SAA4BC,iBAAiB,QAAO,uBAAuB;AAC3E,SAAQC,EAAE,QAAO,YAAY;AAC7B,SAAQC,cAAc,QAAO,eAAe;AAC5C,SAAQC,UAAU,QAAO,uBAAuB;AAChD,SAAQC,0BAA0B,QAAO,sBAAsB;AAC/D,SAAQC,UAAU,QAAO,gCAAgC;AACzD,SAAQC,eAAe,QAAO,iDAAiD;AAC/E,SAAQC,iBAAiB,QAAO,oDAAoD;AACpF,SAAQC,uBAAuB,QAAO,0DAA0D;AAChG,SAAQC,oBAAoB,QAAO,gCAAgC;AACnE,SAAQC,0BAA0B,EAAEC,yBAAyB,QAAO,yBAAyB;AAE7F,SAAQC,oBAAoB,QAAO,yBAAyB;AAE5D,MAAMC,cAAcnB,QAAQD,QAAQE,cAAc,YAAYmB,GAAG,IAAI;AACrE,MAAMC,gBAAgBrB,QAAQmB,aAAa;AAE3C,MAAMG,aAAahB;AAEnB,OAAO,MAAMiB,aAA4B;IACvCC,MAAM;IACNC,UAAU;QAAClB;QAAIC;IAAc;IAC7BkB,QAAQf;IACRgB,SAASjB;IACT,oFAAoF;IACpF,uFAAuF;IACvFkB,YAAY;QAAC;YAACJ,MAAM;YAAQK,OAAOpB;YAAYF;YAAIuB,cAAc3B;QAAgB;KAAE;IACnF4B,aAAa;QACXT,WAAWjB,mCAAmCS;QAC9CQ,WAAWlB,0BAA0BQ;QACrCU,WAAWpB,mBAAmBW;KAC/B;IACDmB,SAAS;QACP;YACEC,WAAWhB;YACXI;YACAa,YAAYnB;YACZ,yFAAyF;YACzF,2FAA2F;YAC3FoB,WAAW;gBACT;oBACEX,MAAM;oBACNY,IAAI;oBACJC,cAAc;gBAChB;gBACA,6EAA6E;gBAC7E,iCAAiC;gBACjC;oBACEb,MAAM;oBACNY,IAAI;oBACJC,cAAc;gBAChB;aACD;QACH;QACA,sFAAsF;QACtF;YACEJ,WAAWjB;YACXK;YACAa,YAAYnB;YACZoB,WAAW;gBACT;oBACEX,MAAM;oBACNY,IAAI;oBACJC,cAAc;gBAChB;aACD;QACH;KACD;AACH,EAAE"}
@@ -0,0 +1,2 @@
1
+ export { registerLogsServiceMetrics } from './service.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/metrics/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,0BAA0B,EAAC,MAAM,cAAc,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { registerLogsServiceMetrics } from './service.js';
2
+
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/metrics/index.ts"],"sourcesContent":["export {registerLogsServiceMetrics} from './service.js';\n"],"names":["registerLogsServiceMetrics"],"mappings":"AAAA,SAAQA,0BAA0B,QAAO,eAAe"}
@@ -0,0 +1,14 @@
1
+ import type { LogRecord } from '@shipfox/api-logs-dto';
2
+ export type LogRecordMetricKind = LogRecord['type'];
3
+ export declare const recordAppendedCount: import("@shipfox/node-opentelemetry").Counter<{
4
+ kind: LogRecordMetricKind;
5
+ }>;
6
+ export declare const streamOpenedCount: import("@shipfox/node-opentelemetry").Counter<Record<string, never>>;
7
+ export declare const streamClosedCount: import("@shipfox/node-opentelemetry").Counter<{
8
+ reason: "declared" | "timeout";
9
+ }>;
10
+ export type CompactionMetricOutcome = 'already-compacted' | 'compacted' | 'failed' | 'gone' | 'retention-raced' | 'superseded';
11
+ export declare const compactionCount: import("@shipfox/node-opentelemetry").Counter<{
12
+ outcome: CompactionMetricOutcome;
13
+ }>;
14
+ //# sourceMappingURL=instance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/metrics/instance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAKrD,MAAM,MAAM,mBAAmB,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;AAEpD,eAAO,MAAM,mBAAmB;UAA8B,mBAAmB;EAGhF,CAAC;AAEF,eAAO,MAAM,iBAAiB,sEAE5B,CAAC;AAEH,eAAO,MAAM,iBAAiB;YAAgC,UAAU,GAAG,SAAS;EAGnF,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAC/B,mBAAmB,GACnB,WAAW,GACX,QAAQ,GACR,MAAM,GACN,iBAAiB,GACjB,YAAY,CAAC;AAEjB,eAAO,MAAM,eAAe;aAAiC,uBAAuB;EAGnF,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { instanceMetrics } from '@shipfox/node-opentelemetry';
2
+ const meter = instanceMetrics.getMeter('logs');
3
+ export const recordAppendedCount = meter.createCounter('logs_record_appended', {
4
+ description: 'Log records appended to durable stream storage by record type'
5
+ });
6
+ export const streamOpenedCount = meter.createCounter('logs_stream_opened', {
7
+ description: 'Log streams opened by first append'
8
+ });
9
+ export const streamClosedCount = meter.createCounter('logs_stream_closed', {
10
+ description: 'Log streams closed by reason'
11
+ });
12
+ export const compactionCount = meter.createCounter('logs_compaction', {
13
+ description: 'Log stream compaction attempts by outcome'
14
+ });
15
+
16
+ //# sourceMappingURL=instance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/metrics/instance.ts"],"sourcesContent":["import type {LogRecord} from '@shipfox/api-logs-dto';\nimport {instanceMetrics} from '@shipfox/node-opentelemetry';\n\nconst meter = instanceMetrics.getMeter('logs');\n\nexport type LogRecordMetricKind = LogRecord['type'];\n\nexport const recordAppendedCount = meter.createCounter<{kind: LogRecordMetricKind}>(\n 'logs_record_appended',\n {description: 'Log records appended to durable stream storage by record type'},\n);\n\nexport const streamOpenedCount = meter.createCounter<Record<string, never>>('logs_stream_opened', {\n description: 'Log streams opened by first append',\n});\n\nexport const streamClosedCount = meter.createCounter<{reason: 'declared' | 'timeout'}>(\n 'logs_stream_closed',\n {description: 'Log streams closed by reason'},\n);\n\nexport type CompactionMetricOutcome =\n | 'already-compacted'\n | 'compacted'\n | 'failed'\n | 'gone'\n | 'retention-raced'\n | 'superseded';\n\nexport const compactionCount = meter.createCounter<{outcome: CompactionMetricOutcome}>(\n 'logs_compaction',\n {description: 'Log stream compaction attempts by outcome'},\n);\n"],"names":["instanceMetrics","meter","getMeter","recordAppendedCount","createCounter","description","streamOpenedCount","streamClosedCount","compactionCount"],"mappings":"AACA,SAAQA,eAAe,QAAO,8BAA8B;AAE5D,MAAMC,QAAQD,gBAAgBE,QAAQ,CAAC;AAIvC,OAAO,MAAMC,sBAAsBF,MAAMG,aAAa,CACpD,wBACA;IAACC,aAAa;AAA+D,GAC7E;AAEF,OAAO,MAAMC,oBAAoBL,MAAMG,aAAa,CAAwB,sBAAsB;IAChGC,aAAa;AACf,GAAG;AAEH,OAAO,MAAME,oBAAoBN,MAAMG,aAAa,CAClD,sBACA;IAACC,aAAa;AAA8B,GAC5C;AAUF,OAAO,MAAMG,kBAAkBP,MAAMG,aAAa,CAChD,mBACA;IAACC,aAAa;AAA2C,GACzD"}
@@ -0,0 +1,2 @@
1
+ export declare function registerLogsServiceMetrics(): void;
2
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/metrics/service.ts"],"names":[],"mappings":"AAGA,wBAAgB,0BAA0B,IAAI,IAAI,CAajD"}
@@ -0,0 +1,20 @@
1
+ import { getServiceMetricsProvider } from '@shipfox/node-opentelemetry';
2
+ import { getOpenStreamCount } from '#db/streams.js';
3
+ export function registerLogsServiceMetrics() {
4
+ const meter = getServiceMetricsProvider().getMeter('logs');
5
+ const openStreams = meter.createObservableGauge('logs_open_streams', {
6
+ description: 'Log streams currently open for appends'
7
+ });
8
+ meter.addBatchObservableCallback(async (observer)=>{
9
+ observer.observe(openStreams, toSafeGaugeNumber(await getOpenStreamCount()));
10
+ }, [
11
+ openStreams
12
+ ]);
13
+ }
14
+ function toSafeGaugeNumber(value) {
15
+ // OpenTelemetry gauges accept numbers; clamp unrepresentable DB counts rather than round them.
16
+ if (value <= BigInt(Number.MAX_SAFE_INTEGER)) return Number(value);
17
+ return Number.MAX_SAFE_INTEGER;
18
+ }
19
+
20
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/metrics/service.ts"],"sourcesContent":["import {getServiceMetricsProvider} from '@shipfox/node-opentelemetry';\nimport {getOpenStreamCount} from '#db/streams.js';\n\nexport function registerLogsServiceMetrics(): void {\n const meter = getServiceMetricsProvider().getMeter('logs');\n\n const openStreams = meter.createObservableGauge('logs_open_streams', {\n description: 'Log streams currently open for appends',\n });\n\n meter.addBatchObservableCallback(\n async (observer) => {\n observer.observe(openStreams, toSafeGaugeNumber(await getOpenStreamCount()));\n },\n [openStreams],\n );\n}\n\nfunction toSafeGaugeNumber(value: bigint): number {\n // OpenTelemetry gauges accept numbers; clamp unrepresentable DB counts rather than round them.\n if (value <= BigInt(Number.MAX_SAFE_INTEGER)) return Number(value);\n return Number.MAX_SAFE_INTEGER;\n}\n"],"names":["getServiceMetricsProvider","getOpenStreamCount","registerLogsServiceMetrics","meter","getMeter","openStreams","createObservableGauge","description","addBatchObservableCallback","observer","observe","toSafeGaugeNumber","value","BigInt","Number","MAX_SAFE_INTEGER"],"mappings":"AAAA,SAAQA,yBAAyB,QAAO,8BAA8B;AACtE,SAAQC,kBAAkB,QAAO,iBAAiB;AAElD,OAAO,SAASC;IACd,MAAMC,QAAQH,4BAA4BI,QAAQ,CAAC;IAEnD,MAAMC,cAAcF,MAAMG,qBAAqB,CAAC,qBAAqB;QACnEC,aAAa;IACf;IAEAJ,MAAMK,0BAA0B,CAC9B,OAAOC;QACLA,SAASC,OAAO,CAACL,aAAaM,kBAAkB,MAAMV;IACxD,GACA;QAACI;KAAY;AAEjB;AAEA,SAASM,kBAAkBC,KAAa;IACtC,+FAA+F;IAC/F,IAAIA,SAASC,OAAOC,OAAOC,gBAAgB,GAAG,OAAOD,OAAOF;IAC5D,OAAOE,OAAOC,gBAAgB;AAChC"}
@@ -0,0 +1,4 @@
1
+ import type { ReadLogsResponseDto } from '@shipfox/api-logs-dto';
2
+ import type { LogReadResult } from '#core/read-logs.js';
3
+ export declare function toReadLogsDto(result: LogReadResult): ReadLogsResponseDto;
4
+ //# sourceMappingURL=read-logs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-logs.d.ts","sourceRoot":"","sources":["../../../src/presentation/dto/read-logs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,oBAAoB,CAAC;AAEtD,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,mBAAmB,CAoBxE"}
@@ -0,0 +1,22 @@
1
+ export function toReadLogsDto(result) {
2
+ if (result.mode === 'presigned') {
3
+ return {
4
+ mode: 'presigned',
5
+ url: result.url,
6
+ state: result.state,
7
+ expires_at: result.expiresAt.toISOString(),
8
+ total_bytes: result.totalBytes,
9
+ truncated: result.truncated
10
+ };
11
+ }
12
+ return {
13
+ mode: 'inline',
14
+ ndjson: result.ndjson,
15
+ next_cursor: result.nextCursor,
16
+ has_more: result.hasMore,
17
+ state: result.state,
18
+ truncated: result.truncated
19
+ };
20
+ }
21
+
22
+ //# sourceMappingURL=read-logs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/presentation/dto/read-logs.ts"],"sourcesContent":["import type {ReadLogsResponseDto} from '@shipfox/api-logs-dto';\nimport type {LogReadResult} from '#core/read-logs.js';\n\nexport function toReadLogsDto(result: LogReadResult): ReadLogsResponseDto {\n if (result.mode === 'presigned') {\n return {\n mode: 'presigned',\n url: result.url,\n state: result.state,\n expires_at: result.expiresAt.toISOString(),\n total_bytes: result.totalBytes,\n truncated: result.truncated,\n };\n }\n\n return {\n mode: 'inline',\n ndjson: result.ndjson,\n next_cursor: result.nextCursor,\n has_more: result.hasMore,\n state: result.state,\n truncated: result.truncated,\n };\n}\n"],"names":["toReadLogsDto","result","mode","url","state","expires_at","expiresAt","toISOString","total_bytes","totalBytes","truncated","ndjson","next_cursor","nextCursor","has_more","hasMore"],"mappings":"AAGA,OAAO,SAASA,cAAcC,MAAqB;IACjD,IAAIA,OAAOC,IAAI,KAAK,aAAa;QAC/B,OAAO;YACLA,MAAM;YACNC,KAAKF,OAAOE,GAAG;YACfC,OAAOH,OAAOG,KAAK;YACnBC,YAAYJ,OAAOK,SAAS,CAACC,WAAW;YACxCC,aAAaP,OAAOQ,UAAU;YAC9BC,WAAWT,OAAOS,SAAS;QAC7B;IACF;IAEA,OAAO;QACLR,MAAM;QACNS,QAAQV,OAAOU,MAAM;QACrBC,aAAaX,OAAOY,UAAU;QAC9BC,UAAUb,OAAOc,OAAO;QACxBX,OAAOH,OAAOG,KAAK;QACnBM,WAAWT,OAAOS,SAAS;IAC7B;AACF"}
@@ -0,0 +1,2 @@
1
+ export declare const appendLogsRoute: import("@shipfox/node-fastify").RouteDefinition;
2
+ //# sourceMappingURL=append-logs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"append-logs.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/append-logs.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,eAAe,iDAqD1B,CAAC"}
@@ -0,0 +1,71 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { requireLeasedJobContext } from '@shipfox/api-auth-context';
3
+ import { appendLogsQuerySchema, appendLogsResponseSchema, offsetGapResponseSchema } from '@shipfox/api-logs-dto';
4
+ import { ClientError, defineRoute } from '@shipfox/node-fastify';
5
+ import { z } from 'zod';
6
+ import { appendLogs } from '#core/append-logs.js';
7
+ import { LeaseStreamMismatchError, MalformedLogChunkError, OffsetGapError } from '#core/errors.js';
8
+ export const appendLogsRoute = defineRoute({
9
+ method: 'POST',
10
+ path: '/steps/:stepId/logs',
11
+ description: 'Append a chunk of logs for a step attempt of the leased job.',
12
+ schema: {
13
+ params: z.object({
14
+ stepId: z.string().uuid()
15
+ }),
16
+ querystring: appendLogsQuerySchema,
17
+ response: {
18
+ 200: appendLogsResponseSchema,
19
+ 409: offsetGapResponseSchema
20
+ }
21
+ },
22
+ errorHandler: (error)=>{
23
+ if (error instanceof OffsetGapError) {
24
+ throw new ClientError('Append offset is ahead of the committed length', 'offset-gap', {
25
+ status: 409,
26
+ details: {
27
+ committed_length: error.committedLength
28
+ }
29
+ });
30
+ }
31
+ if (error instanceof MalformedLogChunkError) {
32
+ throw new ClientError(error.message, 'malformed-log-chunk', {
33
+ status: 400
34
+ });
35
+ }
36
+ if (error instanceof LeaseStreamMismatchError) {
37
+ throw new ClientError(error.message, 'lease-stream-mismatch', {
38
+ status: 403
39
+ });
40
+ }
41
+ throw error;
42
+ },
43
+ handler: async (request)=>{
44
+ const leasedJob = requireLeasedJobContext(request);
45
+ const { stepId } = request.params;
46
+ const { attempt, offset } = request.query;
47
+ // The scoped lease proves membership in the dispatched step attempt; active-step
48
+ // completion remains governed by workflow/report state, not the log append route.
49
+ if (leasedJob.currentStepId !== stepId || leasedJob.currentStepAttempt !== attempt) {
50
+ throw new ClientError('Step not found for leased job execution', 'step-not-found', {
51
+ status: 404
52
+ });
53
+ }
54
+ const result = await appendLogs({
55
+ jobId: leasedJob.jobId,
56
+ workspaceId: leasedJob.workspaceId,
57
+ projectId: leasedJob.projectId,
58
+ workflowRunAttemptId: leasedJob.workflowRunAttemptId,
59
+ stepId,
60
+ attempt,
61
+ offset,
62
+ body: request.body ?? Buffer.alloc(0)
63
+ });
64
+ return {
65
+ committed_length: result.committedLength,
66
+ capped: result.capped
67
+ };
68
+ }
69
+ });
70
+
71
+ //# sourceMappingURL=append-logs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/presentation/routes/append-logs.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {requireLeasedJobContext} from '@shipfox/api-auth-context';\nimport {\n appendLogsQuerySchema,\n appendLogsResponseSchema,\n offsetGapResponseSchema,\n} from '@shipfox/api-logs-dto';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {z} from 'zod';\nimport {appendLogs} from '#core/append-logs.js';\nimport {LeaseStreamMismatchError, MalformedLogChunkError, OffsetGapError} from '#core/errors.js';\n\nexport const appendLogsRoute = defineRoute({\n method: 'POST',\n path: '/steps/:stepId/logs',\n description: 'Append a chunk of logs for a step attempt of the leased job.',\n schema: {\n params: z.object({stepId: z.string().uuid()}),\n querystring: appendLogsQuerySchema,\n response: {\n 200: appendLogsResponseSchema,\n 409: offsetGapResponseSchema,\n },\n },\n errorHandler: (error) => {\n if (error instanceof OffsetGapError) {\n throw new ClientError('Append offset is ahead of the committed length', 'offset-gap', {\n status: 409,\n details: {committed_length: error.committedLength},\n });\n }\n if (error instanceof MalformedLogChunkError) {\n throw new ClientError(error.message, 'malformed-log-chunk', {status: 400});\n }\n if (error instanceof LeaseStreamMismatchError) {\n throw new ClientError(error.message, 'lease-stream-mismatch', {status: 403});\n }\n throw error;\n },\n handler: async (request) => {\n const leasedJob = requireLeasedJobContext(request);\n const {stepId} = request.params;\n const {attempt, offset} = request.query;\n\n // The scoped lease proves membership in the dispatched step attempt; active-step\n // completion remains governed by workflow/report state, not the log append route.\n if (leasedJob.currentStepId !== stepId || leasedJob.currentStepAttempt !== attempt) {\n throw new ClientError('Step not found for leased job execution', 'step-not-found', {\n status: 404,\n });\n }\n\n const result = await appendLogs({\n jobId: leasedJob.jobId,\n workspaceId: leasedJob.workspaceId,\n projectId: leasedJob.projectId,\n workflowRunAttemptId: leasedJob.workflowRunAttemptId,\n stepId,\n attempt,\n offset,\n body: (request.body as Buffer | undefined) ?? Buffer.alloc(0),\n });\n\n return {committed_length: result.committedLength, capped: result.capped};\n },\n});\n"],"names":["Buffer","requireLeasedJobContext","appendLogsQuerySchema","appendLogsResponseSchema","offsetGapResponseSchema","ClientError","defineRoute","z","appendLogs","LeaseStreamMismatchError","MalformedLogChunkError","OffsetGapError","appendLogsRoute","method","path","description","schema","params","object","stepId","string","uuid","querystring","response","errorHandler","error","status","details","committed_length","committedLength","message","handler","request","leasedJob","attempt","offset","query","currentStepId","currentStepAttempt","result","jobId","workspaceId","projectId","workflowRunAttemptId","body","alloc","capped"],"mappings":"AAAA,SAAQA,MAAM,QAAO,cAAc;AACnC,SAAQC,uBAAuB,QAAO,4BAA4B;AAClE,SACEC,qBAAqB,EACrBC,wBAAwB,EACxBC,uBAAuB,QAClB,wBAAwB;AAC/B,SAAQC,WAAW,EAAEC,WAAW,QAAO,wBAAwB;AAC/D,SAAQC,CAAC,QAAO,MAAM;AACtB,SAAQC,UAAU,QAAO,uBAAuB;AAChD,SAAQC,wBAAwB,EAAEC,sBAAsB,EAAEC,cAAc,QAAO,kBAAkB;AAEjG,OAAO,MAAMC,kBAAkBN,YAAY;IACzCO,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNC,QAAQV,EAAEW,MAAM,CAAC;YAACC,QAAQZ,EAAEa,MAAM,GAAGC,IAAI;QAAE;QAC3CC,aAAapB;QACbqB,UAAU;YACR,KAAKpB;YACL,KAAKC;QACP;IACF;IACAoB,cAAc,CAACC;QACb,IAAIA,iBAAiBd,gBAAgB;YACnC,MAAM,IAAIN,YAAY,kDAAkD,cAAc;gBACpFqB,QAAQ;gBACRC,SAAS;oBAACC,kBAAkBH,MAAMI,eAAe;gBAAA;YACnD;QACF;QACA,IAAIJ,iBAAiBf,wBAAwB;YAC3C,MAAM,IAAIL,YAAYoB,MAAMK,OAAO,EAAE,uBAAuB;gBAACJ,QAAQ;YAAG;QAC1E;QACA,IAAID,iBAAiBhB,0BAA0B;YAC7C,MAAM,IAAIJ,YAAYoB,MAAMK,OAAO,EAAE,yBAAyB;gBAACJ,QAAQ;YAAG;QAC5E;QACA,MAAMD;IACR;IACAM,SAAS,OAAOC;QACd,MAAMC,YAAYhC,wBAAwB+B;QAC1C,MAAM,EAACb,MAAM,EAAC,GAAGa,QAAQf,MAAM;QAC/B,MAAM,EAACiB,OAAO,EAAEC,MAAM,EAAC,GAAGH,QAAQI,KAAK;QAEvC,iFAAiF;QACjF,kFAAkF;QAClF,IAAIH,UAAUI,aAAa,KAAKlB,UAAUc,UAAUK,kBAAkB,KAAKJ,SAAS;YAClF,MAAM,IAAI7B,YAAY,2CAA2C,kBAAkB;gBACjFqB,QAAQ;YACV;QACF;QAEA,MAAMa,SAAS,MAAM/B,WAAW;YAC9BgC,OAAOP,UAAUO,KAAK;YACtBC,aAAaR,UAAUQ,WAAW;YAClCC,WAAWT,UAAUS,SAAS;YAC9BC,sBAAsBV,UAAUU,oBAAoB;YACpDxB;YACAe;YACAC;YACAS,MAAM,AAACZ,QAAQY,IAAI,IAA2B5C,OAAO6C,KAAK,CAAC;QAC7D;QAEA,OAAO;YAACjB,kBAAkBW,OAAOV,eAAe;YAAEiB,QAAQP,OAAOO,MAAM;QAAA;IACzE;AACF,GAAG"}
@@ -0,0 +1,3 @@
1
+ import { type RouteGroup } from '@shipfox/node-fastify';
2
+ export declare const logsRoutes: RouteGroup[];
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAsB,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AAQ3E,eAAO,MAAM,UAAU,EAAE,UAAU,EAiBlC,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { AUTH_LEASED_JOB, AUTH_USER } from '@shipfox/api-auth-context';
2
+ import { createRawBodyPlugin } from '@shipfox/node-fastify';
3
+ import { config } from '#config.js';
4
+ import { appendLogsRoute } from './append-logs.js';
5
+ import { readLogsRoute } from './read-logs.js';
6
+ // Keep the lease-authed append in its own Fastify scope so the raw NDJSON parser does not
7
+ // disturb the JSON read route (or workflow routes). The body limit also bounds one-append
8
+ // budget overshoot. The read route is session-authed and workspace-scoped via the row.
9
+ export const logsRoutes = [
10
+ {
11
+ prefix: '/runs/jobs/current',
12
+ auth: AUTH_LEASED_JOB,
13
+ plugins: [
14
+ createRawBodyPlugin({
15
+ contentType: 'application/x-ndjson',
16
+ bodyLimit: config.LOG_APPEND_BODY_LIMIT_BYTES
17
+ })
18
+ ],
19
+ routes: [
20
+ appendLogsRoute
21
+ ]
22
+ },
23
+ {
24
+ prefix: '/steps',
25
+ auth: AUTH_USER,
26
+ routes: [
27
+ readLogsRoute
28
+ ]
29
+ }
30
+ ];
31
+
32
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/presentation/routes/index.ts"],"sourcesContent":["import {AUTH_LEASED_JOB, AUTH_USER} from '@shipfox/api-auth-context';\nimport {createRawBodyPlugin, type RouteGroup} from '@shipfox/node-fastify';\nimport {config} from '#config.js';\nimport {appendLogsRoute} from './append-logs.js';\nimport {readLogsRoute} from './read-logs.js';\n\n// Keep the lease-authed append in its own Fastify scope so the raw NDJSON parser does not\n// disturb the JSON read route (or workflow routes). The body limit also bounds one-append\n// budget overshoot. The read route is session-authed and workspace-scoped via the row.\nexport const logsRoutes: RouteGroup[] = [\n {\n prefix: '/runs/jobs/current',\n auth: AUTH_LEASED_JOB,\n plugins: [\n createRawBodyPlugin({\n contentType: 'application/x-ndjson',\n bodyLimit: config.LOG_APPEND_BODY_LIMIT_BYTES,\n }),\n ],\n routes: [appendLogsRoute],\n },\n {\n prefix: '/steps',\n auth: AUTH_USER,\n routes: [readLogsRoute],\n },\n];\n"],"names":["AUTH_LEASED_JOB","AUTH_USER","createRawBodyPlugin","config","appendLogsRoute","readLogsRoute","logsRoutes","prefix","auth","plugins","contentType","bodyLimit","LOG_APPEND_BODY_LIMIT_BYTES","routes"],"mappings":"AAAA,SAAQA,eAAe,EAAEC,SAAS,QAAO,4BAA4B;AACrE,SAAQC,mBAAmB,QAAwB,wBAAwB;AAC3E,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,eAAe,QAAO,mBAAmB;AACjD,SAAQC,aAAa,QAAO,iBAAiB;AAE7C,0FAA0F;AAC1F,0FAA0F;AAC1F,uFAAuF;AACvF,OAAO,MAAMC,aAA2B;IACtC;QACEC,QAAQ;QACRC,MAAMR;QACNS,SAAS;YACPP,oBAAoB;gBAClBQ,aAAa;gBACbC,WAAWR,OAAOS,2BAA2B;YAC/C;SACD;QACDC,QAAQ;YAACT;SAAgB;IAC3B;IACA;QACEG,QAAQ;QACRC,MAAMP;QACNY,QAAQ;YAACR;SAAc;IACzB;CACD,CAAC"}