@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,27 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import type { AttemptStream, StreamCloseReason } from '#core/entities/attempt-stream.js';
3
+ import type { Transaction } from '#db/db.js';
4
+ export type TombstoneKind = 'capped' | 'runner_lost';
5
+ /**
6
+ * Frames a server tombstone as one newline-terminated record. Typed against the
7
+ * shared contract so an envelope change breaks this at compile time. Stored as a
8
+ * `control`-origin chunk; like every server record it does NOT advance
9
+ * `committed_length`, so the CAS axis stays equal to runner spool bytes.
10
+ */
11
+ export declare function controlTombstone(kind: TombstoneKind): Buffer;
12
+ export interface CloseStreamParams {
13
+ streamId: string;
14
+ reason: StreamCloseReason;
15
+ }
16
+ /**
17
+ * The guarded UPDATE (`WHERE state='open'`) in `markStreamClosed` is the lock and
18
+ * the idempotency gate: a stream already closed by the other path (append-time
19
+ * declared close vs the job-terminated timeout sweep) returns null, so no duplicate
20
+ * `LOG_STREAM_CLOSED` is written and no second tombstone lands. A timeout close sets
21
+ * `truncated` and injects a `runner_lost` tombstone in-band (a `capped` tombstone,
22
+ * if any, was injected earlier at the append that tripped the cap).
23
+ *
24
+ * The event drives compaction; it is written in the same transaction as the flip.
25
+ */
26
+ export declare function closeStream(tx: Transaction, params: CloseStreamParams): Promise<AttemptStream | null>;
27
+ //# sourceMappingURL=close-stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"close-stream.d.ts","sourceRoot":"","sources":["../../src/core/close-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAGnC,OAAO,KAAK,EAAC,aAAa,EAAE,iBAAiB,EAAC,MAAM,kCAAkC,CAAC;AAEvF,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,WAAW,CAAC;AAI3C,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,aAAa,CAAC;AAErD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAG5D;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CA+B/B"}
@@ -0,0 +1,59 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { LOG_STREAM_CLOSED } from '@shipfox/api-logs-dto';
3
+ import { writeOutboxEvent } from '@shipfox/node-outbox';
4
+ import { insertChunk } from '#db/chunks.js';
5
+ import { logsOutbox } from '#db/schema/outbox.js';
6
+ import { markStreamClosed } from '#db/streams.js';
7
+ /**
8
+ * Frames a server tombstone as one newline-terminated record. Typed against the
9
+ * shared contract so an envelope change breaks this at compile time. Stored as a
10
+ * `control`-origin chunk; like every server record it does NOT advance
11
+ * `committed_length`, so the CAS axis stays equal to runner spool bytes.
12
+ */ export function controlTombstone(kind) {
13
+ const record = {
14
+ v: 1,
15
+ ts: Date.now(),
16
+ type: kind
17
+ };
18
+ return Buffer.from(`${JSON.stringify(record)}\n`, 'utf8');
19
+ }
20
+ /**
21
+ * The guarded UPDATE (`WHERE state='open'`) in `markStreamClosed` is the lock and
22
+ * the idempotency gate: a stream already closed by the other path (append-time
23
+ * declared close vs the job-terminated timeout sweep) returns null, so no duplicate
24
+ * `LOG_STREAM_CLOSED` is written and no second tombstone lands. A timeout close sets
25
+ * `truncated` and injects a `runner_lost` tombstone in-band (a `capped` tombstone,
26
+ * if any, was injected earlier at the append that tripped the cap).
27
+ *
28
+ * The event drives compaction; it is written in the same transaction as the flip.
29
+ */ export async function closeStream(tx, params) {
30
+ const closed = await markStreamClosed(tx, {
31
+ streamId: params.streamId,
32
+ reason: params.reason,
33
+ markTruncated: params.reason === 'timeout'
34
+ });
35
+ if (!closed) return null;
36
+ if (params.reason === 'timeout') {
37
+ const tombstone = controlTombstone('runner_lost');
38
+ await insertChunk(tx, {
39
+ streamId: closed.id,
40
+ streamOffset: closed.committedLength,
41
+ byteLen: tombstone.length,
42
+ data: tombstone,
43
+ origin: 'control'
44
+ });
45
+ }
46
+ await writeOutboxEvent(tx, logsOutbox, {
47
+ type: LOG_STREAM_CLOSED,
48
+ payload: {
49
+ workspaceId: closed.workspaceId,
50
+ jobId: closed.jobId,
51
+ stepId: closed.stepId,
52
+ attempt: closed.attempt,
53
+ streamId: closed.id
54
+ }
55
+ });
56
+ return closed;
57
+ }
58
+
59
+ //# sourceMappingURL=close-stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/close-stream.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {LOG_STREAM_CLOSED, type LogRecord, type LogsEventMap} from '@shipfox/api-logs-dto';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport type {AttemptStream, StreamCloseReason} from '#core/entities/attempt-stream.js';\nimport {insertChunk} from '#db/chunks.js';\nimport type {Transaction} from '#db/db.js';\nimport {logsOutbox} from '#db/schema/outbox.js';\nimport {markStreamClosed} from '#db/streams.js';\n\nexport type TombstoneKind = 'capped' | 'runner_lost';\n\n/**\n * Frames a server tombstone as one newline-terminated record. Typed against the\n * shared contract so an envelope change breaks this at compile time. Stored as a\n * `control`-origin chunk; like every server record it does NOT advance\n * `committed_length`, so the CAS axis stays equal to runner spool bytes.\n */\nexport function controlTombstone(kind: TombstoneKind): Buffer {\n const record: LogRecord = {v: 1, ts: Date.now(), type: kind};\n return Buffer.from(`${JSON.stringify(record)}\\n`, 'utf8');\n}\n\nexport interface CloseStreamParams {\n streamId: string;\n reason: StreamCloseReason;\n}\n\n/**\n * The guarded UPDATE (`WHERE state='open'`) in `markStreamClosed` is the lock and\n * the idempotency gate: a stream already closed by the other path (append-time\n * declared close vs the job-terminated timeout sweep) returns null, so no duplicate\n * `LOG_STREAM_CLOSED` is written and no second tombstone lands. A timeout close sets\n * `truncated` and injects a `runner_lost` tombstone in-band (a `capped` tombstone,\n * if any, was injected earlier at the append that tripped the cap).\n *\n * The event drives compaction; it is written in the same transaction as the flip.\n */\nexport async function closeStream(\n tx: Transaction,\n params: CloseStreamParams,\n): Promise<AttemptStream | null> {\n const closed = await markStreamClosed(tx, {\n streamId: params.streamId,\n reason: params.reason,\n markTruncated: params.reason === 'timeout',\n });\n if (!closed) return null;\n\n if (params.reason === 'timeout') {\n const tombstone = controlTombstone('runner_lost');\n await insertChunk(tx, {\n streamId: closed.id,\n streamOffset: closed.committedLength,\n byteLen: tombstone.length,\n data: tombstone,\n origin: 'control',\n });\n }\n\n await writeOutboxEvent<LogsEventMap>(tx, logsOutbox, {\n type: LOG_STREAM_CLOSED,\n payload: {\n workspaceId: closed.workspaceId,\n jobId: closed.jobId,\n stepId: closed.stepId,\n attempt: closed.attempt,\n streamId: closed.id,\n },\n });\n\n return closed;\n}\n"],"names":["Buffer","LOG_STREAM_CLOSED","writeOutboxEvent","insertChunk","logsOutbox","markStreamClosed","controlTombstone","kind","record","v","ts","Date","now","type","from","JSON","stringify","closeStream","tx","params","closed","streamId","reason","markTruncated","tombstone","id","streamOffset","committedLength","byteLen","length","data","origin","payload","workspaceId","jobId","stepId","attempt"],"mappings":"AAAA,SAAQA,MAAM,QAAO,cAAc;AACnC,SAAQC,iBAAiB,QAA0C,wBAAwB;AAC3F,SAAQC,gBAAgB,QAAO,uBAAuB;AAEtD,SAAQC,WAAW,QAAO,gBAAgB;AAE1C,SAAQC,UAAU,QAAO,uBAAuB;AAChD,SAAQC,gBAAgB,QAAO,iBAAiB;AAIhD;;;;;CAKC,GACD,OAAO,SAASC,iBAAiBC,IAAmB;IAClD,MAAMC,SAAoB;QAACC,GAAG;QAAGC,IAAIC,KAAKC,GAAG;QAAIC,MAAMN;IAAI;IAC3D,OAAOP,OAAOc,IAAI,CAAC,GAAGC,KAAKC,SAAS,CAACR,QAAQ,EAAE,CAAC,EAAE;AACpD;AAOA;;;;;;;;;CASC,GACD,OAAO,eAAeS,YACpBC,EAAe,EACfC,MAAyB;IAEzB,MAAMC,SAAS,MAAMf,iBAAiBa,IAAI;QACxCG,UAAUF,OAAOE,QAAQ;QACzBC,QAAQH,OAAOG,MAAM;QACrBC,eAAeJ,OAAOG,MAAM,KAAK;IACnC;IACA,IAAI,CAACF,QAAQ,OAAO;IAEpB,IAAID,OAAOG,MAAM,KAAK,WAAW;QAC/B,MAAME,YAAYlB,iBAAiB;QACnC,MAAMH,YAAYe,IAAI;YACpBG,UAAUD,OAAOK,EAAE;YACnBC,cAAcN,OAAOO,eAAe;YACpCC,SAASJ,UAAUK,MAAM;YACzBC,MAAMN;YACNO,QAAQ;QACV;IACF;IAEA,MAAM7B,iBAA+BgB,IAAId,YAAY;QACnDS,MAAMZ;QACN+B,SAAS;YACPC,aAAab,OAAOa,WAAW;YAC/BC,OAAOd,OAAOc,KAAK;YACnBC,QAAQf,OAAOe,MAAM;YACrBC,SAAShB,OAAOgB,OAAO;YACvBf,UAAUD,OAAOK,EAAE;QACrB;IACF;IAEA,OAAOL;AACT"}
@@ -0,0 +1,28 @@
1
+ import { Readable } from 'node:stream';
2
+ export interface CompactionStreamStats {
3
+ chunkCount: number;
4
+ lastSeq: number;
5
+ uncompressedBytes: number;
6
+ }
7
+ export interface CompactedGzipStream {
8
+ body: Readable;
9
+ stats: CompactionStreamStats;
10
+ }
11
+ export interface CompactedGzipStreamParams {
12
+ streamId: string;
13
+ /**
14
+ * Invoked once per fetched chunk page (after the read, before the page's bytes flow on).
15
+ * Lets the caller heartbeat off read progress, so the pre-upload phase and a slow first
16
+ * part still emit liveness instead of waiting on the upload's per-part progress event.
17
+ */
18
+ onPage?: () => void;
19
+ }
20
+ /**
21
+ * Builds a gzip stream of a closed stream's chunk bytes in `seq` order. Keyset-paginates
22
+ * so memory stays flat at any stream size, and fills `stats` as bytes flow so the caller
23
+ * can check the streamed totals against the table before deleting the source rows. A
24
+ * source error is forwarded to the gzip stream, so the upload consuming it rejects rather
25
+ * than silently truncating.
26
+ */
27
+ export declare function compactedGzipStream(params: CompactedGzipStreamParams): CompactedGzipStream;
28
+ //# sourceMappingURL=compaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/core/compaction.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,QAAQ,EAAC,MAAM,aAAa,CAAC;AAQrC,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,qBAAqB,CAAC;CAC9B;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,yBAAyB,GAAG,mBAAmB,CA8B1F"}
@@ -0,0 +1,49 @@
1
+ import { Readable } from 'node:stream';
2
+ import { createGzip } from 'node:zlib';
3
+ import { readChunksKeyset } from '#db/chunks.js';
4
+ // Chunks are capped at ~256KB each by the runner flush, so a small fixed page count gives a
5
+ // predictable memory bound (page x ~256KB) without a byte-budgeted query.
6
+ const CHUNK_PAGE_SIZE = 64;
7
+ /**
8
+ * Builds a gzip stream of a closed stream's chunk bytes in `seq` order. Keyset-paginates
9
+ * so memory stays flat at any stream size, and fills `stats` as bytes flow so the caller
10
+ * can check the streamed totals against the table before deleting the source rows. A
11
+ * source error is forwarded to the gzip stream, so the upload consuming it rejects rather
12
+ * than silently truncating.
13
+ */ export function compactedGzipStream(params) {
14
+ const stats = {
15
+ chunkCount: 0,
16
+ lastSeq: 0,
17
+ uncompressedBytes: 0
18
+ };
19
+ async function* chunks() {
20
+ let afterSeq = 0;
21
+ while(true){
22
+ const page = await readChunksKeyset({
23
+ streamId: params.streamId,
24
+ afterSeq,
25
+ limit: CHUNK_PAGE_SIZE
26
+ });
27
+ if (page.length === 0) break;
28
+ params.onPage?.();
29
+ for (const row of page){
30
+ stats.chunkCount += 1;
31
+ stats.lastSeq = row.seq;
32
+ stats.uncompressedBytes += row.data.length;
33
+ yield row.data;
34
+ }
35
+ afterSeq = page.at(-1)?.seq ?? afterSeq;
36
+ if (page.length < CHUNK_PAGE_SIZE) break;
37
+ }
38
+ }
39
+ const source = Readable.from(chunks());
40
+ const gzip = createGzip();
41
+ source.once('error', (error)=>gzip.destroy(error));
42
+ source.pipe(gzip);
43
+ return {
44
+ body: gzip,
45
+ stats
46
+ };
47
+ }
48
+
49
+ //# sourceMappingURL=compaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/compaction.ts"],"sourcesContent":["import type {Buffer} from 'node:buffer';\nimport {Readable} from 'node:stream';\nimport {createGzip} from 'node:zlib';\nimport {readChunksKeyset} from '#db/chunks.js';\n\n// Chunks are capped at ~256KB each by the runner flush, so a small fixed page count gives a\n// predictable memory bound (page x ~256KB) without a byte-budgeted query.\nconst CHUNK_PAGE_SIZE = 64;\n\nexport interface CompactionStreamStats {\n chunkCount: number;\n lastSeq: number;\n uncompressedBytes: number;\n}\n\nexport interface CompactedGzipStream {\n body: Readable;\n stats: CompactionStreamStats;\n}\n\nexport interface CompactedGzipStreamParams {\n streamId: string;\n /**\n * Invoked once per fetched chunk page (after the read, before the page's bytes flow on).\n * Lets the caller heartbeat off read progress, so the pre-upload phase and a slow first\n * part still emit liveness instead of waiting on the upload's per-part progress event.\n */\n onPage?: () => void;\n}\n\n/**\n * Builds a gzip stream of a closed stream's chunk bytes in `seq` order. Keyset-paginates\n * so memory stays flat at any stream size, and fills `stats` as bytes flow so the caller\n * can check the streamed totals against the table before deleting the source rows. A\n * source error is forwarded to the gzip stream, so the upload consuming it rejects rather\n * than silently truncating.\n */\nexport function compactedGzipStream(params: CompactedGzipStreamParams): CompactedGzipStream {\n const stats: CompactionStreamStats = {chunkCount: 0, lastSeq: 0, uncompressedBytes: 0};\n\n async function* chunks(): AsyncGenerator<Buffer> {\n let afterSeq = 0;\n while (true) {\n const page = await readChunksKeyset({\n streamId: params.streamId,\n afterSeq,\n limit: CHUNK_PAGE_SIZE,\n });\n if (page.length === 0) break;\n params.onPage?.();\n for (const row of page) {\n stats.chunkCount += 1;\n stats.lastSeq = row.seq;\n stats.uncompressedBytes += row.data.length;\n yield row.data;\n }\n afterSeq = page.at(-1)?.seq ?? afterSeq;\n if (page.length < CHUNK_PAGE_SIZE) break;\n }\n }\n\n const source = Readable.from(chunks());\n const gzip = createGzip();\n source.once('error', (error) => gzip.destroy(error));\n source.pipe(gzip);\n\n return {body: gzip, stats};\n}\n"],"names":["Readable","createGzip","readChunksKeyset","CHUNK_PAGE_SIZE","compactedGzipStream","params","stats","chunkCount","lastSeq","uncompressedBytes","chunks","afterSeq","page","streamId","limit","length","onPage","row","seq","data","at","source","from","gzip","once","error","destroy","pipe","body"],"mappings":"AACA,SAAQA,QAAQ,QAAO,cAAc;AACrC,SAAQC,UAAU,QAAO,YAAY;AACrC,SAAQC,gBAAgB,QAAO,gBAAgB;AAE/C,4FAA4F;AAC5F,0EAA0E;AAC1E,MAAMC,kBAAkB;AAuBxB;;;;;;CAMC,GACD,OAAO,SAASC,oBAAoBC,MAAiC;IACnE,MAAMC,QAA+B;QAACC,YAAY;QAAGC,SAAS;QAAGC,mBAAmB;IAAC;IAErF,gBAAgBC;QACd,IAAIC,WAAW;QACf,MAAO,KAAM;YACX,MAAMC,OAAO,MAAMV,iBAAiB;gBAClCW,UAAUR,OAAOQ,QAAQ;gBACzBF;gBACAG,OAAOX;YACT;YACA,IAAIS,KAAKG,MAAM,KAAK,GAAG;YACvBV,OAAOW,MAAM;YACb,KAAK,MAAMC,OAAOL,KAAM;gBACtBN,MAAMC,UAAU,IAAI;gBACpBD,MAAME,OAAO,GAAGS,IAAIC,GAAG;gBACvBZ,MAAMG,iBAAiB,IAAIQ,IAAIE,IAAI,CAACJ,MAAM;gBAC1C,MAAME,IAAIE,IAAI;YAChB;YACAR,WAAWC,KAAKQ,EAAE,CAAC,CAAC,IAAIF,OAAOP;YAC/B,IAAIC,KAAKG,MAAM,GAAGZ,iBAAiB;QACrC;IACF;IAEA,MAAMkB,SAASrB,SAASsB,IAAI,CAACZ;IAC7B,MAAMa,OAAOtB;IACboB,OAAOG,IAAI,CAAC,SAAS,CAACC,QAAUF,KAAKG,OAAO,CAACD;IAC7CJ,OAAOM,IAAI,CAACJ;IAEZ,OAAO;QAACK,MAAML;QAAMjB;IAAK;AAC3B"}
@@ -0,0 +1,37 @@
1
+ /** Lifecycle state of an attempt's log stream. */
2
+ export type StreamState = 'open' | 'closed';
3
+ /** Why a stream was closed, once it is. */
4
+ export type StreamCloseReason = 'declared' | 'timeout';
5
+ /**
6
+ * A single `(job, step, attempt)` log stream. Identity is scoped to the lease's
7
+ * job. `workspaceId`, `projectId`, and `workflowRunAttemptId` are stamped from the lease on
8
+ * stream creation and asserted on subsequent appends, so reads can authorize
9
+ * per-project without joining back to workflows. `committedLength` is the
10
+ * offset-CAS axis: raw NDJSON spool bytes the server has durably accepted from
11
+ * the runner.
12
+ *
13
+ * `truncated` is an out-of-band terminal flag set when the timeout sweep
14
+ * force-closes a stream the runner never ended. Budget exhaustion ("capped") is a
15
+ * job-level signal recorded in `job_accounting` and surfaced in-band by a `capped`
16
+ * tombstone record, not on the stream row.
17
+ */
18
+ export interface AttemptStream {
19
+ id: string;
20
+ jobId: string;
21
+ stepId: string;
22
+ attempt: number;
23
+ workspaceId: string;
24
+ projectId: string;
25
+ workflowRunAttemptId: string;
26
+ committedLength: number;
27
+ state: StreamState;
28
+ closeReason: StreamCloseReason | null;
29
+ declaredTotalBytes: number | null;
30
+ truncated: boolean;
31
+ objectKey: string | null;
32
+ createdAt: Date;
33
+ updatedAt: Date;
34
+ /** When the stream was closed (either close path); null while open. Retention anchor. */
35
+ closedAt: Date | null;
36
+ }
37
+ //# sourceMappingURL=attempt-stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attempt-stream.d.ts","sourceRoot":"","sources":["../../../src/core/entities/attempt-stream.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE5C,2CAA2C;AAC3C,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,SAAS,CAAC;AAEvD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,WAAW,CAAC;IACnB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACtC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,yFAAyF;IACzF,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;CACvB"}
@@ -0,0 +1,15 @@
1
+ /** Lifecycle state of an attempt's log stream. */ /**
2
+ * A single `(job, step, attempt)` log stream. Identity is scoped to the lease's
3
+ * job. `workspaceId`, `projectId`, and `workflowRunAttemptId` are stamped from the lease on
4
+ * stream creation and asserted on subsequent appends, so reads can authorize
5
+ * per-project without joining back to workflows. `committedLength` is the
6
+ * offset-CAS axis: raw NDJSON spool bytes the server has durably accepted from
7
+ * the runner.
8
+ *
9
+ * `truncated` is an out-of-band terminal flag set when the timeout sweep
10
+ * force-closes a stream the runner never ended. Budget exhaustion ("capped") is a
11
+ * job-level signal recorded in `job_accounting` and surfaced in-band by a `capped`
12
+ * tombstone record, not on the stream row.
13
+ */ export { };
14
+
15
+ //# sourceMappingURL=attempt-stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/core/entities/attempt-stream.ts"],"sourcesContent":["/** Lifecycle state of an attempt's log stream. */\nexport type StreamState = 'open' | 'closed';\n\n/** Why a stream was closed, once it is. */\nexport type StreamCloseReason = 'declared' | 'timeout';\n\n/**\n * A single `(job, step, attempt)` log stream. Identity is scoped to the lease's\n * job. `workspaceId`, `projectId`, and `workflowRunAttemptId` are stamped from the lease on\n * stream creation and asserted on subsequent appends, so reads can authorize\n * per-project without joining back to workflows. `committedLength` is the\n * offset-CAS axis: raw NDJSON spool bytes the server has durably accepted from\n * the runner.\n *\n * `truncated` is an out-of-band terminal flag set when the timeout sweep\n * force-closes a stream the runner never ended. Budget exhaustion (\"capped\") is a\n * job-level signal recorded in `job_accounting` and surfaced in-band by a `capped`\n * tombstone record, not on the stream row.\n */\nexport interface AttemptStream {\n id: string;\n jobId: string;\n stepId: string;\n attempt: number;\n workspaceId: string;\n projectId: string;\n workflowRunAttemptId: string;\n committedLength: number;\n state: StreamState;\n closeReason: StreamCloseReason | null;\n declaredTotalBytes: number | null;\n truncated: boolean;\n objectKey: string | null;\n createdAt: Date;\n updatedAt: Date;\n /** When the stream was closed (either close path); null while open. Retention anchor. */\n closedAt: Date | null;\n}\n"],"names":[],"mappings":"AAAA,gDAAgD,GAMhD;;;;;;;;;;;;CAYC,GACD,WAkBC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Per-job accrual-budget state: how many stored bytes a job has used, the budget
3
+ * clock origin, and whether it has been capped.
4
+ */
5
+ export interface JobAccounting {
6
+ jobId: string;
7
+ workspaceId: string;
8
+ storedBytesUsed: number;
9
+ startedAt: Date;
10
+ cappedAt: Date | null;
11
+ }
12
+ //# sourceMappingURL=job-accounting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-accounting.d.ts","sourceRoot":"","sources":["../../../src/core/entities/job-accounting.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;CACvB"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Per-job accrual-budget state: how many stored bytes a job has used, the budget
3
+ * clock origin, and whether it has been capped.
4
+ */ export { };
5
+
6
+ //# sourceMappingURL=job-accounting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/core/entities/job-accounting.ts"],"sourcesContent":["/**\n * Per-job accrual-budget state: how many stored bytes a job has used, the budget\n * clock origin, and whether it has been capped.\n */\nexport interface JobAccounting {\n jobId: string;\n workspaceId: string;\n storedBytesUsed: number;\n startedAt: Date;\n cappedAt: Date | null;\n}\n"],"names":[],"mappings":"AAAA;;;CAGC,GACD,WAMC"}
@@ -0,0 +1,13 @@
1
+ export interface LogObjectKeyParams {
2
+ workspaceId: string;
3
+ jobId: string;
4
+ stepId: string;
5
+ attempt: number;
6
+ }
7
+ /**
8
+ * Object layout `{prefix}/{workspace}/{job}/{step}/{attempt}` so retention and workspace
9
+ * deletion are prefix operations. `prefix` is the configurable bucket prefix
10
+ * (`LOG_STORAGE_S3_PREFIX`, default `logs`), so one bucket can host several modules.
11
+ */
12
+ export declare function logObjectKey(prefix: string, { workspaceId, jobId, stepId, attempt }: LogObjectKeyParams): string;
13
+ //# sourceMappingURL=log-object.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-object.d.ts","sourceRoot":"","sources":["../../../src/core/entities/log-object.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,EAAC,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAC,EAAE,kBAAkB,GACxD,MAAM,CAER"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Object layout `{prefix}/{workspace}/{job}/{step}/{attempt}` so retention and workspace
3
+ * deletion are prefix operations. `prefix` is the configurable bucket prefix
4
+ * (`LOG_STORAGE_S3_PREFIX`, default `logs`), so one bucket can host several modules.
5
+ */ export function logObjectKey(prefix, { workspaceId, jobId, stepId, attempt }) {
6
+ return `${prefix}/${workspaceId}/${jobId}/${stepId}/${attempt}`;
7
+ }
8
+
9
+ //# sourceMappingURL=log-object.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/core/entities/log-object.ts"],"sourcesContent":["export interface LogObjectKeyParams {\n workspaceId: string;\n jobId: string;\n stepId: string;\n attempt: number;\n}\n\n/**\n * Object layout `{prefix}/{workspace}/{job}/{step}/{attempt}` so retention and workspace\n * deletion are prefix operations. `prefix` is the configurable bucket prefix\n * (`LOG_STORAGE_S3_PREFIX`, default `logs`), so one bucket can host several modules.\n */\nexport function logObjectKey(\n prefix: string,\n {workspaceId, jobId, stepId, attempt}: LogObjectKeyParams,\n): string {\n return `${prefix}/${workspaceId}/${jobId}/${stepId}/${attempt}`;\n}\n"],"names":["logObjectKey","prefix","workspaceId","jobId","stepId","attempt"],"mappings":"AAOA;;;;CAIC,GACD,OAAO,SAASA,aACdC,MAAc,EACd,EAACC,WAAW,EAAEC,KAAK,EAAEC,MAAM,EAAEC,OAAO,EAAqB;IAEzD,OAAO,GAAGJ,OAAO,CAAC,EAAEC,YAAY,CAAC,EAAEC,MAAM,CAAC,EAAEC,OAAO,CAAC,EAAEC,SAAS;AACjE"}
@@ -0,0 +1,26 @@
1
+ /** The append offset is ahead of the committed length: the runner must rewind its spool cursor. */
2
+ export declare class OffsetGapError extends Error {
3
+ readonly committedLength: number;
4
+ constructor(committedLength: number);
5
+ }
6
+ /**
7
+ * The append body is not whole, newline-terminated records of the raw log
8
+ * record contract. `forgedType` is set only for the detectable forgery case — a
9
+ * line that is a valid server-only record (`capped`/`runner_lost`) under the read
10
+ * union but is not valid on the raw write path — so the append path can emit a narrowed audit warn
11
+ * without logging the payload.
12
+ */
13
+ export declare class MalformedLogChunkError extends Error {
14
+ readonly forgedType?: string | undefined;
15
+ constructor(message: string, forgedType?: string | undefined);
16
+ }
17
+ /**
18
+ * The lease's `(workspaceId, projectId, workflowRunAttemptId)` does not match the values
19
+ * stamped on the existing stream row. Since these are functionally determined
20
+ * by `jobId` via workflows FKs, a mismatch implies a forged token or a
21
+ * cross-job lease confusion — never a legitimate request.
22
+ */
23
+ export declare class LeaseStreamMismatchError extends Error {
24
+ constructor();
25
+ }
26
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,qBAAa,cAAe,SAAQ,KAAK;aACX,eAAe,EAAE,MAAM;gBAAvB,eAAe,EAAE,MAAM;CAIpD;AAED;;;;;;GAMG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;aAG7B,UAAU,CAAC,EAAE,MAAM;gBADnC,OAAO,EAAE,MAAM,EACC,UAAU,CAAC,EAAE,MAAM,YAAA;CAKtC;AAED;;;;;GAKG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;;CAKlD"}
@@ -0,0 +1,31 @@
1
+ /** The append offset is ahead of the committed length: the runner must rewind its spool cursor. */ export class OffsetGapError extends Error {
2
+ constructor(committedLength){
3
+ super(`Append offset is ahead of the committed length (${committedLength})`), this.committedLength = committedLength;
4
+ this.name = 'OffsetGapError';
5
+ }
6
+ }
7
+ /**
8
+ * The append body is not whole, newline-terminated records of the raw log
9
+ * record contract. `forgedType` is set only for the detectable forgery case — a
10
+ * line that is a valid server-only record (`capped`/`runner_lost`) under the read
11
+ * union but is not valid on the raw write path — so the append path can emit a narrowed audit warn
12
+ * without logging the payload.
13
+ */ export class MalformedLogChunkError extends Error {
14
+ constructor(message, forgedType){
15
+ super(message), this.forgedType = forgedType;
16
+ this.name = 'MalformedLogChunkError';
17
+ }
18
+ }
19
+ /**
20
+ * The lease's `(workspaceId, projectId, workflowRunAttemptId)` does not match the values
21
+ * stamped on the existing stream row. Since these are functionally determined
22
+ * by `jobId` via workflows FKs, a mismatch implies a forged token or a
23
+ * cross-job lease confusion — never a legitimate request.
24
+ */ export class LeaseStreamMismatchError extends Error {
25
+ constructor(){
26
+ super('Lease identity does not match the existing stream row');
27
+ this.name = 'LeaseStreamMismatchError';
28
+ }
29
+ }
30
+
31
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/errors.ts"],"sourcesContent":["/** The append offset is ahead of the committed length: the runner must rewind its spool cursor. */\nexport class OffsetGapError extends Error {\n constructor(public readonly committedLength: number) {\n super(`Append offset is ahead of the committed length (${committedLength})`);\n this.name = 'OffsetGapError';\n }\n}\n\n/**\n * The append body is not whole, newline-terminated records of the raw log\n * record contract. `forgedType` is set only for the detectable forgery case — a\n * line that is a valid server-only record (`capped`/`runner_lost`) under the read\n * union but is not valid on the raw write path — so the append path can emit a narrowed audit warn\n * without logging the payload.\n */\nexport class MalformedLogChunkError extends Error {\n constructor(\n message: string,\n public readonly forgedType?: string,\n ) {\n super(message);\n this.name = 'MalformedLogChunkError';\n }\n}\n\n/**\n * The lease's `(workspaceId, projectId, workflowRunAttemptId)` does not match the values\n * stamped on the existing stream row. Since these are functionally determined\n * by `jobId` via workflows FKs, a mismatch implies a forged token or a\n * cross-job lease confusion — never a legitimate request.\n */\nexport class LeaseStreamMismatchError extends Error {\n constructor() {\n super('Lease identity does not match the existing stream row');\n this.name = 'LeaseStreamMismatchError';\n }\n}\n"],"names":["OffsetGapError","Error","committedLength","name","MalformedLogChunkError","message","forgedType","LeaseStreamMismatchError"],"mappings":"AAAA,iGAAiG,GACjG,OAAO,MAAMA,uBAAuBC;IAClC,YAAY,AAAgBC,eAAuB,CAAE;QACnD,KAAK,CAAC,CAAC,gDAAgD,EAAEA,gBAAgB,CAAC,CAAC,QADjDA,kBAAAA;QAE1B,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA;;;;;;CAMC,GACD,OAAO,MAAMC,+BAA+BH;IAC1C,YACEI,OAAe,EACf,AAAgBC,UAAmB,CACnC;QACA,KAAK,CAACD,eAFUC,aAAAA;QAGhB,IAAI,CAACH,IAAI,GAAG;IACd;AACF;AAEA;;;;;CAKC,GACD,OAAO,MAAMI,iCAAiCN;IAC5C,aAAc;QACZ,KAAK,CAAC;QACN,IAAI,CAACE,IAAI,GAAG;IACd;AACF"}
@@ -0,0 +1,22 @@
1
+ import type { LogOutcomeDto } from '@shipfox/api-workflows-dto';
2
+ import type { AttemptStream } from '#core/entities/attempt-stream.js';
3
+ import { type AttemptStreamIdentity } from '#db/streams.js';
4
+ export interface FinalizeAttemptLogStreamParams extends AttemptStreamIdentity {
5
+ logOutcome: LogOutcomeDto;
6
+ }
7
+ interface FinalizeMetrics {
8
+ recordAppendedCount: {
9
+ add: (value: number, attributes: {
10
+ kind: 'runner_lost';
11
+ }) => void;
12
+ };
13
+ streamClosedCount: {
14
+ add: (value: number, attributes: {
15
+ reason: 'declared' | 'timeout';
16
+ }) => void;
17
+ };
18
+ }
19
+ export declare function createFinalizeAttemptLogStream(metrics?: FinalizeMetrics): (params: FinalizeAttemptLogStreamParams) => Promise<AttemptStream>;
20
+ export declare const finalizeAttemptLogStream: (params: FinalizeAttemptLogStreamParams) => Promise<AttemptStream>;
21
+ export {};
22
+ //# sourceMappingURL=finalize-attempt-stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finalize-attempt-stream.d.ts","sourceRoot":"","sources":["../../src/core/finalize-attempt-stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,4BAA4B,CAAC;AAC9D,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,kCAAkC,CAAC;AAEpE,OAAO,EACL,KAAK,qBAAqB,EAG3B,MAAM,gBAAgB,CAAC;AAIxB,MAAM,WAAW,8BAA+B,SAAQ,qBAAqB;IAC3E,UAAU,EAAE,aAAa,CAAC;CAC3B;AAOD,UAAU,eAAe;IACvB,mBAAmB,EAAE;QAAC,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE;YAAC,IAAI,EAAE,aAAa,CAAA;SAAC,KAAK,IAAI,CAAA;KAAC,CAAC;IACvF,iBAAiB,EAAE;QACjB,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE;YAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;SAAC,KAAK,IAAI,CAAC;KAC5E,CAAC;CACH;AAOD,wBAAgB,8BAA8B,CAAC,OAAO,GAAE,eAAgC,IACxE,QAAQ,8BAA8B,KAAG,OAAO,CAAC,aAAa,CAAC,CAuB9E;AAED,eAAO,MAAM,wBAAwB,WAzBb,8BAA8B,KAAG,OAAO,CAAC,aAAa,CAyBN,CAAC"}
@@ -0,0 +1,48 @@
1
+ import { db } from '#db/db.js';
2
+ import { getAttemptStreamByIdInTransaction, getOrCreateAttemptStreamWithStatus } from '#db/streams.js';
3
+ import { recordAppendedCount, streamClosedCount } from '#metrics/instance.js';
4
+ import { closeStream } from './close-stream.js';
5
+ const defaultMetrics = {
6
+ recordAppendedCount,
7
+ streamClosedCount
8
+ };
9
+ export function createFinalizeAttemptLogStream(metrics = defaultMetrics) {
10
+ return async (params)=>{
11
+ const result = await db().transaction(async (tx)=>{
12
+ const { stream } = await getOrCreateAttemptStreamWithStatus(tx, params);
13
+ if (stream.state === 'closed') return {
14
+ stream,
15
+ closedReason: null
16
+ };
17
+ const reason = params.logOutcome === 'abandoned' ? 'timeout' : 'declared';
18
+ const closed = await closeStream(tx, {
19
+ streamId: stream.id,
20
+ reason
21
+ });
22
+ if (closed) return {
23
+ stream: closed,
24
+ closedReason: reason
25
+ };
26
+ const current = await getAttemptStreamByIdInTransaction(tx, stream.id);
27
+ if (!current) throw new Error(`Log stream disappeared during finalization: ${stream.id}`);
28
+ return {
29
+ stream: current,
30
+ closedReason: null
31
+ };
32
+ });
33
+ if (result.closedReason) {
34
+ if (result.closedReason === 'timeout') {
35
+ metrics.recordAppendedCount.add(1, {
36
+ kind: 'runner_lost'
37
+ });
38
+ }
39
+ metrics.streamClosedCount.add(1, {
40
+ reason: result.closedReason
41
+ });
42
+ }
43
+ return result.stream;
44
+ };
45
+ }
46
+ export const finalizeAttemptLogStream = createFinalizeAttemptLogStream();
47
+
48
+ //# sourceMappingURL=finalize-attempt-stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/finalize-attempt-stream.ts"],"sourcesContent":["import type {LogOutcomeDto} from '@shipfox/api-workflows-dto';\nimport type {AttemptStream} from '#core/entities/attempt-stream.js';\nimport {db} from '#db/db.js';\nimport {\n type AttemptStreamIdentity,\n getAttemptStreamByIdInTransaction,\n getOrCreateAttemptStreamWithStatus,\n} from '#db/streams.js';\nimport {recordAppendedCount, streamClosedCount} from '#metrics/instance.js';\nimport {closeStream} from './close-stream.js';\n\nexport interface FinalizeAttemptLogStreamParams extends AttemptStreamIdentity {\n logOutcome: LogOutcomeDto;\n}\n\ninterface FinalizeAttemptLogStreamResult {\n stream: AttemptStream;\n closedReason: 'declared' | 'timeout' | null;\n}\n\ninterface FinalizeMetrics {\n recordAppendedCount: {add: (value: number, attributes: {kind: 'runner_lost'}) => void};\n streamClosedCount: {\n add: (value: number, attributes: {reason: 'declared' | 'timeout'}) => void;\n };\n}\n\nconst defaultMetrics: FinalizeMetrics = {\n recordAppendedCount,\n streamClosedCount,\n};\n\nexport function createFinalizeAttemptLogStream(metrics: FinalizeMetrics = defaultMetrics) {\n return async (params: FinalizeAttemptLogStreamParams): Promise<AttemptStream> => {\n const result: FinalizeAttemptLogStreamResult = await db().transaction(async (tx) => {\n const {stream} = await getOrCreateAttemptStreamWithStatus(tx, params);\n if (stream.state === 'closed') return {stream, closedReason: null};\n\n const reason = params.logOutcome === 'abandoned' ? 'timeout' : 'declared';\n const closed = await closeStream(tx, {streamId: stream.id, reason});\n if (closed) return {stream: closed, closedReason: reason};\n\n const current = await getAttemptStreamByIdInTransaction(tx, stream.id);\n if (!current) throw new Error(`Log stream disappeared during finalization: ${stream.id}`);\n return {stream: current, closedReason: null};\n });\n\n if (result.closedReason) {\n if (result.closedReason === 'timeout') {\n metrics.recordAppendedCount.add(1, {kind: 'runner_lost'});\n }\n metrics.streamClosedCount.add(1, {reason: result.closedReason});\n }\n\n return result.stream;\n };\n}\n\nexport const finalizeAttemptLogStream = createFinalizeAttemptLogStream();\n"],"names":["db","getAttemptStreamByIdInTransaction","getOrCreateAttemptStreamWithStatus","recordAppendedCount","streamClosedCount","closeStream","defaultMetrics","createFinalizeAttemptLogStream","metrics","params","result","transaction","tx","stream","state","closedReason","reason","logOutcome","closed","streamId","id","current","Error","add","kind","finalizeAttemptLogStream"],"mappings":"AAEA,SAAQA,EAAE,QAAO,YAAY;AAC7B,SAEEC,iCAAiC,EACjCC,kCAAkC,QAC7B,iBAAiB;AACxB,SAAQC,mBAAmB,EAAEC,iBAAiB,QAAO,uBAAuB;AAC5E,SAAQC,WAAW,QAAO,oBAAoB;AAkB9C,MAAMC,iBAAkC;IACtCH;IACAC;AACF;AAEA,OAAO,SAASG,+BAA+BC,UAA2BF,cAAc;IACtF,OAAO,OAAOG;QACZ,MAAMC,SAAyC,MAAMV,KAAKW,WAAW,CAAC,OAAOC;YAC3E,MAAM,EAACC,MAAM,EAAC,GAAG,MAAMX,mCAAmCU,IAAIH;YAC9D,IAAII,OAAOC,KAAK,KAAK,UAAU,OAAO;gBAACD;gBAAQE,cAAc;YAAI;YAEjE,MAAMC,SAASP,OAAOQ,UAAU,KAAK,cAAc,YAAY;YAC/D,MAAMC,SAAS,MAAMb,YAAYO,IAAI;gBAACO,UAAUN,OAAOO,EAAE;gBAAEJ;YAAM;YACjE,IAAIE,QAAQ,OAAO;gBAACL,QAAQK;gBAAQH,cAAcC;YAAM;YAExD,MAAMK,UAAU,MAAMpB,kCAAkCW,IAAIC,OAAOO,EAAE;YACrE,IAAI,CAACC,SAAS,MAAM,IAAIC,MAAM,CAAC,4CAA4C,EAAET,OAAOO,EAAE,EAAE;YACxF,OAAO;gBAACP,QAAQQ;gBAASN,cAAc;YAAI;QAC7C;QAEA,IAAIL,OAAOK,YAAY,EAAE;YACvB,IAAIL,OAAOK,YAAY,KAAK,WAAW;gBACrCP,QAAQL,mBAAmB,CAACoB,GAAG,CAAC,GAAG;oBAACC,MAAM;gBAAa;YACzD;YACAhB,QAAQJ,iBAAiB,CAACmB,GAAG,CAAC,GAAG;gBAACP,QAAQN,OAAOK,YAAY;YAAA;QAC/D;QAEA,OAAOL,OAAOG,MAAM;IACtB;AACF;AAEA,OAAO,MAAMY,2BAA2BlB,iCAAiC"}
@@ -0,0 +1,33 @@
1
+ import type { AttemptStream, StreamState } from '#core/entities/attempt-stream.js';
2
+ export interface InlineLogRead {
3
+ mode: 'inline';
4
+ ndjson: string;
5
+ nextCursor: number;
6
+ hasMore: boolean;
7
+ state: StreamState;
8
+ truncated: boolean;
9
+ }
10
+ export interface PresignedLogRead {
11
+ mode: 'presigned';
12
+ url: string;
13
+ state: 'closed';
14
+ expiresAt: Date;
15
+ totalBytes: number;
16
+ truncated: boolean;
17
+ }
18
+ export type LogReadResult = InlineLogRead | PresignedLogRead;
19
+ /**
20
+ * Compaction-boundary guard: a stream can be loaded with no `objectKey`, then compaction
21
+ * publishes the key and deletes the chunks before the chunk read runs, which would yield a
22
+ * spurious empty inline frame on a closed stream. On that exact shape (closed stream, empty
23
+ * page) the row is re-read once; if it has since compacted, the presigned object is served
24
+ * instead.
25
+ *
26
+ * The re-read is scoped to a closed snapshot on purpose. An open stream returns empty pages
27
+ * on every idle tail poll, so re-reading each would add a row read to the hot path; and an
28
+ * open snapshot cannot have compacted (compaction runs only after close, seconds later via
29
+ * Temporal), so the open-empty-compacted window is too small to hit. If it ever did, the
30
+ * client's next poll reloads the now-compacted row and self-corrects.
31
+ */
32
+ export declare function buildLogReadResult(stream: AttemptStream, cursor: number): Promise<LogReadResult>;
33
+ //# sourceMappingURL=read-logs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-logs.d.ts","sourceRoot":"","sources":["../../src/core/read-logs.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,aAAa,EAAE,WAAW,EAAC,MAAM,kCAAkC,CAAC;AAIjF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,QAAQ,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,gBAAgB,CAAC;AAc7D;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,aAAa,CAAC,CAsBxB"}
@@ -0,0 +1,49 @@
1
+ import { presignedGetUrl } from '#api/object-storage.js';
2
+ import { config } from '#config.js';
3
+ import { readChunkPageBySeq } from '#db/chunks.js';
4
+ import { getAttemptStreamById } from '#db/streams.js';
5
+ async function presignedRead(stream, objectKey) {
6
+ const { url, expiresAt } = await presignedGetUrl(objectKey);
7
+ return {
8
+ mode: 'presigned',
9
+ url,
10
+ state: 'closed',
11
+ expiresAt,
12
+ totalBytes: stream.committedLength,
13
+ truncated: stream.truncated
14
+ };
15
+ }
16
+ /**
17
+ * Compaction-boundary guard: a stream can be loaded with no `objectKey`, then compaction
18
+ * publishes the key and deletes the chunks before the chunk read runs, which would yield a
19
+ * spurious empty inline frame on a closed stream. On that exact shape (closed stream, empty
20
+ * page) the row is re-read once; if it has since compacted, the presigned object is served
21
+ * instead.
22
+ *
23
+ * The re-read is scoped to a closed snapshot on purpose. An open stream returns empty pages
24
+ * on every idle tail poll, so re-reading each would add a row read to the hot path; and an
25
+ * open snapshot cannot have compacted (compaction runs only after close, seconds later via
26
+ * Temporal), so the open-empty-compacted window is too small to hit. If it ever did, the
27
+ * client's next poll reloads the now-compacted row and self-corrects.
28
+ */ export async function buildLogReadResult(stream, cursor) {
29
+ if (stream.objectKey) return presignedRead(stream, stream.objectKey);
30
+ const page = await readChunkPageBySeq({
31
+ streamId: stream.id,
32
+ afterSeq: cursor,
33
+ maxBytes: config.LOG_READ_INLINE_MAX_BYTES
34
+ });
35
+ if (page.data.length === 0 && stream.state === 'closed') {
36
+ const refreshed = await getAttemptStreamById(stream.id);
37
+ if (refreshed?.objectKey) return presignedRead(refreshed, refreshed.objectKey);
38
+ }
39
+ return {
40
+ mode: 'inline',
41
+ ndjson: page.data.toString('utf8'),
42
+ nextCursor: page.nextSeq,
43
+ hasMore: page.hasMore,
44
+ state: stream.state,
45
+ truncated: stream.truncated
46
+ };
47
+ }
48
+
49
+ //# sourceMappingURL=read-logs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/read-logs.ts"],"sourcesContent":["import {presignedGetUrl} from '#api/object-storage.js';\nimport {config} from '#config.js';\nimport type {AttemptStream, StreamState} from '#core/entities/attempt-stream.js';\nimport {readChunkPageBySeq} from '#db/chunks.js';\nimport {getAttemptStreamById} from '#db/streams.js';\n\nexport interface InlineLogRead {\n mode: 'inline';\n ndjson: string;\n nextCursor: number;\n hasMore: boolean;\n state: StreamState;\n truncated: boolean;\n}\n\nexport interface PresignedLogRead {\n mode: 'presigned';\n url: string;\n state: 'closed';\n expiresAt: Date;\n totalBytes: number;\n truncated: boolean;\n}\n\nexport type LogReadResult = InlineLogRead | PresignedLogRead;\n\nasync function presignedRead(stream: AttemptStream, objectKey: string): Promise<PresignedLogRead> {\n const {url, expiresAt} = await presignedGetUrl(objectKey);\n return {\n mode: 'presigned',\n url,\n state: 'closed',\n expiresAt,\n totalBytes: stream.committedLength,\n truncated: stream.truncated,\n };\n}\n\n/**\n * Compaction-boundary guard: a stream can be loaded with no `objectKey`, then compaction\n * publishes the key and deletes the chunks before the chunk read runs, which would yield a\n * spurious empty inline frame on a closed stream. On that exact shape (closed stream, empty\n * page) the row is re-read once; if it has since compacted, the presigned object is served\n * instead.\n *\n * The re-read is scoped to a closed snapshot on purpose. An open stream returns empty pages\n * on every idle tail poll, so re-reading each would add a row read to the hot path; and an\n * open snapshot cannot have compacted (compaction runs only after close, seconds later via\n * Temporal), so the open-empty-compacted window is too small to hit. If it ever did, the\n * client's next poll reloads the now-compacted row and self-corrects.\n */\nexport async function buildLogReadResult(\n stream: AttemptStream,\n cursor: number,\n): Promise<LogReadResult> {\n if (stream.objectKey) return presignedRead(stream, stream.objectKey);\n\n const page = await readChunkPageBySeq({\n streamId: stream.id,\n afterSeq: cursor,\n maxBytes: config.LOG_READ_INLINE_MAX_BYTES,\n });\n\n if (page.data.length === 0 && stream.state === 'closed') {\n const refreshed = await getAttemptStreamById(stream.id);\n if (refreshed?.objectKey) return presignedRead(refreshed, refreshed.objectKey);\n }\n\n return {\n mode: 'inline',\n ndjson: page.data.toString('utf8'),\n nextCursor: page.nextSeq,\n hasMore: page.hasMore,\n state: stream.state,\n truncated: stream.truncated,\n };\n}\n"],"names":["presignedGetUrl","config","readChunkPageBySeq","getAttemptStreamById","presignedRead","stream","objectKey","url","expiresAt","mode","state","totalBytes","committedLength","truncated","buildLogReadResult","cursor","page","streamId","id","afterSeq","maxBytes","LOG_READ_INLINE_MAX_BYTES","data","length","refreshed","ndjson","toString","nextCursor","nextSeq","hasMore"],"mappings":"AAAA,SAAQA,eAAe,QAAO,yBAAyB;AACvD,SAAQC,MAAM,QAAO,aAAa;AAElC,SAAQC,kBAAkB,QAAO,gBAAgB;AACjD,SAAQC,oBAAoB,QAAO,iBAAiB;AAsBpD,eAAeC,cAAcC,MAAqB,EAAEC,SAAiB;IACnE,MAAM,EAACC,GAAG,EAAEC,SAAS,EAAC,GAAG,MAAMR,gBAAgBM;IAC/C,OAAO;QACLG,MAAM;QACNF;QACAG,OAAO;QACPF;QACAG,YAAYN,OAAOO,eAAe;QAClCC,WAAWR,OAAOQ,SAAS;IAC7B;AACF;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,eAAeC,mBACpBT,MAAqB,EACrBU,MAAc;IAEd,IAAIV,OAAOC,SAAS,EAAE,OAAOF,cAAcC,QAAQA,OAAOC,SAAS;IAEnE,MAAMU,OAAO,MAAMd,mBAAmB;QACpCe,UAAUZ,OAAOa,EAAE;QACnBC,UAAUJ;QACVK,UAAUnB,OAAOoB,yBAAyB;IAC5C;IAEA,IAAIL,KAAKM,IAAI,CAACC,MAAM,KAAK,KAAKlB,OAAOK,KAAK,KAAK,UAAU;QACvD,MAAMc,YAAY,MAAMrB,qBAAqBE,OAAOa,EAAE;QACtD,IAAIM,WAAWlB,WAAW,OAAOF,cAAcoB,WAAWA,UAAUlB,SAAS;IAC/E;IAEA,OAAO;QACLG,MAAM;QACNgB,QAAQT,KAAKM,IAAI,CAACI,QAAQ,CAAC;QAC3BC,YAAYX,KAAKY,OAAO;QACxBC,SAASb,KAAKa,OAAO;QACrBnB,OAAOL,OAAOK,KAAK;QACnBG,WAAWR,OAAOQ,SAAS;IAC7B;AACF"}
@@ -0,0 +1,27 @@
1
+ export interface ReapStaleOpenStreamsResult {
2
+ /** Streams force-closed by this run. */
3
+ reaped: number;
4
+ /** Streams whose guarded close threw; logged, skipped, retried next run. */
5
+ failed: number;
6
+ }
7
+ export interface ReapStaleOpenStreamsParams {
8
+ /**
9
+ * A stream open longer than this since `created_at` can no longer be appended to: its job
10
+ * holds one lease minted at-or-before the stream's creation, so once the lease TTL has
11
+ * elapsed no append can land and the close is safe. Set above `AUTH_JOB_LEASE_TOKEN_EXPIRES_IN`.
12
+ */
13
+ olderThanSeconds: number;
14
+ batchLimit: number;
15
+ }
16
+ /**
17
+ * Force-closes open streams the one-shot job-terminal sweep missed: a stream whose first append
18
+ * landed after that sweep's snapshot would otherwise stay `open` forever, invisible to
19
+ * compaction and retention (both partial on `state = 'closed'`). Reusing the guarded
20
+ * `closeStream` re-drives each into the normal closed -> compact -> retention lifecycle.
21
+ *
22
+ * Each row closes in its own transaction, so a tick/retry overlap is idempotent (the guarded
23
+ * `markStreamClosed` lets only one close win) and one failed row is logged and skipped instead
24
+ * of aborting the batch.
25
+ */
26
+ export declare function reapStaleOpenStreams(params: ReapStaleOpenStreamsParams): Promise<ReapStaleOpenStreamsResult>;
27
+ //# sourceMappingURL=reap-stale-open-streams.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reap-stale-open-streams.d.ts","sourceRoot":"","sources":["../../src/core/reap-stale-open-streams.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,0BAA0B;IACzC,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,0BAA0B,GACjC,OAAO,CAAC,0BAA0B,CAAC,CAwBrC"}