okengine 0.1.4

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 (674) hide show
  1. package/AGENTS.md +91 -0
  2. package/README.md +327 -0
  3. package/docs/spec/complete-example.md +1188 -0
  4. package/docs/spec/console.md +739 -0
  5. package/docs/spec/four-applications.md +1188 -0
  6. package/docs/spec/unified-theory.md +489 -0
  7. package/package.json +152 -0
  8. package/spec/manifest.v1.schema.json +624 -0
  9. package/src/auth/api-keys.ts +168 -0
  10. package/src/auth/attenuation.ts +116 -0
  11. package/src/auth/auth.test.ts +273 -0
  12. package/src/auth/cross-plane.ts +98 -0
  13. package/src/auth/index.ts +112 -0
  14. package/src/auth/invites.ts +66 -0
  15. package/src/auth/invoke-as.ts +49 -0
  16. package/src/auth/operator.ts +178 -0
  17. package/src/auth/planes.ts +102 -0
  18. package/src/auth/plugin.ts +44 -0
  19. package/src/auth/roles.ts +84 -0
  20. package/src/auth/sessions.ts +418 -0
  21. package/src/auth/tables.ts +131 -0
  22. package/src/cli/args.ts +50 -0
  23. package/src/cli/branch.ts +75 -0
  24. package/src/cli/build.ts +86 -0
  25. package/src/cli/client-add.test.ts +140 -0
  26. package/src/cli/client-add.ts +164 -0
  27. package/src/cli/completion.test.ts +58 -0
  28. package/src/cli/completion.ts +296 -0
  29. package/src/cli/dev-app-runner.ts +77 -0
  30. package/src/cli/dev.test.ts +435 -0
  31. package/src/cli/dev.ts +594 -0
  32. package/src/cli/doc-drift.test.ts +146 -0
  33. package/src/cli/doc-drift.ts +397 -0
  34. package/src/cli/docker-cli.test.ts +221 -0
  35. package/src/cli/docker.ts +127 -0
  36. package/src/cli/doctor-diff-examples.ts +92 -0
  37. package/src/cli/doctor-diff.test.ts +91 -0
  38. package/src/cli/doctor-diff.ts +274 -0
  39. package/src/cli/doctor-pii.test.ts +98 -0
  40. package/src/cli/doctor-pii.ts +151 -0
  41. package/src/cli/doctor.test.ts +150 -0
  42. package/src/cli/doctor.ts +223 -0
  43. package/src/cli/eval.ts +122 -0
  44. package/src/cli/exit.ts +19 -0
  45. package/src/cli/gates-list.test.ts +47 -0
  46. package/src/cli/gates-list.ts +84 -0
  47. package/src/cli/images.ts +277 -0
  48. package/src/cli/index.ts +99 -0
  49. package/src/cli/json-out.test.ts +129 -0
  50. package/src/cli/load-config.ts +71 -0
  51. package/src/cli/mcp-from-console.ts +50 -0
  52. package/src/cli/privacy-erase.ts +73 -0
  53. package/src/cli/registry.ts +455 -0
  54. package/src/cli/safe-defaults.test.ts +92 -0
  55. package/src/cli/schema.ts +196 -0
  56. package/src/cli/stack.ts +82 -0
  57. package/src/cli/start.ts +106 -0
  58. package/src/cli/upgrade.ts +105 -0
  59. package/src/cli/vault-cmd.ts +140 -0
  60. package/src/client/budget-entry.ts +34 -0
  61. package/src/client/budget.test.ts +17 -0
  62. package/src/client/create.test.ts +159 -0
  63. package/src/client/create.ts +152 -0
  64. package/src/client/errors.ts +60 -0
  65. package/src/client/index.ts +35 -0
  66. package/src/client/notes-contract.test.ts +289 -0
  67. package/src/client/transport.test.ts +123 -0
  68. package/src/client/transport.ts +254 -0
  69. package/src/client/types.ts +282 -0
  70. package/src/compiler/aot.test.ts +248 -0
  71. package/src/compiler/aot.ts +182 -0
  72. package/src/compiler/differential.test.ts +241 -0
  73. package/src/compiler/dynamic.ts +54 -0
  74. package/src/compiler/effects-infer.ts +541 -0
  75. package/src/compiler/emit.ts +40 -0
  76. package/src/compiler/extract.test.ts +177 -0
  77. package/src/compiler/extract.ts +1368 -0
  78. package/src/compiler/fixtures/raw/raw-unannotated.ts +28 -0
  79. package/src/compiler/fixtures/skyport/oke.config.ts +24 -0
  80. package/src/compiler/fixtures/skyport/src/ai.ts +15 -0
  81. package/src/compiler/fixtures/skyport/src/app.ts +10 -0
  82. package/src/compiler/fixtures/skyport/src/channels.ts +6 -0
  83. package/src/compiler/fixtures/skyport/src/core.ts +3 -0
  84. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +57 -0
  85. package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +11 -0
  86. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +14 -0
  87. package/src/compiler/fixtures/skyport/src/flows/support/index.ts +20 -0
  88. package/src/compiler/fixtures/skyport/src/gates.ts +14 -0
  89. package/src/compiler/fixtures/skyport/src/journeys.ts +9 -0
  90. package/src/compiler/fixtures/skyport/src/schema.ts +3 -0
  91. package/src/compiler/fixtures/skyport/src/vault.ts +6 -0
  92. package/src/compiler/fixtures/skyport.expected.json +241 -0
  93. package/src/compiler/fixtures/triggers/five-triggers.ts +69 -0
  94. package/src/compiler/http-parse.ts +214 -0
  95. package/src/compiler/index.ts +67 -0
  96. package/src/compiler/response.ts +89 -0
  97. package/src/compiler/sucrose.ts +238 -0
  98. package/src/config/index.ts +167 -0
  99. package/src/console/budget.test.ts +73 -0
  100. package/src/console/index.ts +30 -0
  101. package/src/console/server/access.test.ts +370 -0
  102. package/src/console/server/access.ts +777 -0
  103. package/src/console/server/ai.test.ts +172 -0
  104. package/src/console/server/ai.ts +632 -0
  105. package/src/console/server/app.ts +294 -0
  106. package/src/console/server/auth-rate.test.ts +81 -0
  107. package/src/console/server/auth-rate.ts +72 -0
  108. package/src/console/server/bind.ts +25 -0
  109. package/src/console/server/channels.test.ts +179 -0
  110. package/src/console/server/channels.ts +469 -0
  111. package/src/console/server/claim.test.ts +66 -0
  112. package/src/console/server/claim.ts +134 -0
  113. package/src/console/server/clock.test.ts +239 -0
  114. package/src/console/server/clock.ts +390 -0
  115. package/src/console/server/console.test.ts +338 -0
  116. package/src/console/server/diff.test.ts +299 -0
  117. package/src/console/server/diff.ts +352 -0
  118. package/src/console/server/dry-run.audit.test.ts +118 -0
  119. package/src/console/server/flows-invoke.test.ts +148 -0
  120. package/src/console/server/flows.ts +3633 -0
  121. package/src/console/server/gates.test.ts +360 -0
  122. package/src/console/server/gates.ts +869 -0
  123. package/src/console/server/index.ts +141 -0
  124. package/src/console/server/live.test.ts +30 -0
  125. package/src/console/server/live.ts +109 -0
  126. package/src/console/server/plugin.ts +50 -0
  127. package/src/console/server/plugins.test.ts +131 -0
  128. package/src/console/server/plugins.ts +429 -0
  129. package/src/console/server/runs-pii.ts +98 -0
  130. package/src/console/server/security-headers.ts +97 -0
  131. package/src/console/server/security.gate.test.ts +286 -0
  132. package/src/console/server/serve.ts +319 -0
  133. package/src/console/server/signals.test.ts +88 -0
  134. package/src/console/server/signals.ts +282 -0
  135. package/src/console/server/state.ts +982 -0
  136. package/src/console/server/store.test.ts +233 -0
  137. package/src/console/server/store.ts +861 -0
  138. package/src/console/server/structural.ts +95 -0
  139. package/src/console/server/vault.test.ts +251 -0
  140. package/src/console/server/vault.ts +430 -0
  141. package/src/console/ui/access/AccessA11yView.tsx +237 -0
  142. package/src/console/ui/access/a11y.test.tsx +122 -0
  143. package/src/console/ui/access/acknowledgement.test.ts +13 -0
  144. package/src/console/ui/access/acknowledgement.ts +35 -0
  145. package/src/console/ui/access/blast-radius.test.ts +26 -0
  146. package/src/console/ui/access/blast-radius.ts +40 -0
  147. package/src/console/ui/access/confirmation.test.ts +38 -0
  148. package/src/console/ui/access/confirmation.ts +36 -0
  149. package/src/console/ui/access/fixture.ts +203 -0
  150. package/src/console/ui/access/grantable.test.ts +17 -0
  151. package/src/console/ui/access/grantable.ts +36 -0
  152. package/src/console/ui/access/hygiene.test.ts +12 -0
  153. package/src/console/ui/access/hygiene.ts +39 -0
  154. package/src/console/ui/access/index.ts +54 -0
  155. package/src/console/ui/access/provenance.test.ts +12 -0
  156. package/src/console/ui/access/provenance.ts +29 -0
  157. package/src/console/ui/access/search.test.ts +17 -0
  158. package/src/console/ui/access/search.ts +72 -0
  159. package/src/console/ui/access/types.ts +111 -0
  160. package/src/console/ui/ai/AiA11yView.tsx +265 -0
  161. package/src/console/ui/ai/a11y.test.tsx +99 -0
  162. package/src/console/ui/ai/findings.ts +41 -0
  163. package/src/console/ui/ai/fixture.ts +182 -0
  164. package/src/console/ui/ai/format.ts +90 -0
  165. package/src/console/ui/ai/group.test.ts +37 -0
  166. package/src/console/ui/ai/group.ts +88 -0
  167. package/src/console/ui/ai/index.ts +66 -0
  168. package/src/console/ui/ai/promotion.test.ts +77 -0
  169. package/src/console/ui/ai/promotion.ts +150 -0
  170. package/src/console/ui/ai/search.test.ts +40 -0
  171. package/src/console/ui/ai/search.ts +89 -0
  172. package/src/console/ui/ai/types.ts +139 -0
  173. package/src/console/ui/architecture/ArchitectureA11yView.tsx +163 -0
  174. package/src/console/ui/architecture/a11y.test.tsx +97 -0
  175. package/src/console/ui/architecture/boundary.ts +42 -0
  176. package/src/console/ui/architecture/declared.ts +165 -0
  177. package/src/console/ui/architecture/fixture.ts +74 -0
  178. package/src/console/ui/architecture/index.ts +84 -0
  179. package/src/console/ui/architecture/layers.ts +64 -0
  180. package/src/console/ui/architecture/layout.ts +68 -0
  181. package/src/console/ui/architecture/pathologies.ts +260 -0
  182. package/src/console/ui/architecture/search.ts +145 -0
  183. package/src/console/ui/architecture/traffic.ts +102 -0
  184. package/src/console/ui/architecture/types.ts +144 -0
  185. package/src/console/ui/architecture/view.test.ts +165 -0
  186. package/src/console/ui/architecture/view.ts +516 -0
  187. package/src/console/ui/channels/ChannelsA11yView.tsx +180 -0
  188. package/src/console/ui/channels/a11y.test.tsx +96 -0
  189. package/src/console/ui/channels/confirmation.test.ts +37 -0
  190. package/src/console/ui/channels/confirmation.ts +27 -0
  191. package/src/console/ui/channels/fallback.test.ts +15 -0
  192. package/src/console/ui/channels/fallback.ts +20 -0
  193. package/src/console/ui/channels/findings.ts +38 -0
  194. package/src/console/ui/channels/fixture.ts +134 -0
  195. package/src/console/ui/channels/group.ts +25 -0
  196. package/src/console/ui/channels/index.ts +57 -0
  197. package/src/console/ui/channels/locale.test.ts +17 -0
  198. package/src/console/ui/channels/locale.ts +31 -0
  199. package/src/console/ui/channels/mask.test.ts +14 -0
  200. package/src/console/ui/channels/mask.ts +12 -0
  201. package/src/console/ui/channels/search.ts +58 -0
  202. package/src/console/ui/channels/taxonomy.test.ts +17 -0
  203. package/src/console/ui/channels/taxonomy.ts +49 -0
  204. package/src/console/ui/channels/types.ts +117 -0
  205. package/src/console/ui/clock/ClockA11yView.tsx +182 -0
  206. package/src/console/ui/clock/a11y.test.tsx +99 -0
  207. package/src/console/ui/clock/confirmation.test.ts +32 -0
  208. package/src/console/ui/clock/confirmation.ts +34 -0
  209. package/src/console/ui/clock/findings.ts +40 -0
  210. package/src/console/ui/clock/fixture.ts +111 -0
  211. package/src/console/ui/clock/health.test.ts +25 -0
  212. package/src/console/ui/clock/health.ts +61 -0
  213. package/src/console/ui/clock/index.ts +49 -0
  214. package/src/console/ui/clock/search.test.ts +39 -0
  215. package/src/console/ui/clock/search.ts +76 -0
  216. package/src/console/ui/clock/timeline.test.ts +44 -0
  217. package/src/console/ui/clock/timeline.ts +41 -0
  218. package/src/console/ui/clock/types.ts +78 -0
  219. package/src/console/ui/clock/waiting-on.test.ts +34 -0
  220. package/src/console/ui/clock/waiting-on.ts +83 -0
  221. package/src/console/ui/confirmation.unify.test.ts +92 -0
  222. package/src/console/ui/diff/DiffA11yView.tsx +85 -0
  223. package/src/console/ui/diff/a11y.test.tsx +99 -0
  224. package/src/console/ui/diff/fixture.ts +85 -0
  225. package/src/console/ui/diff/group.test.ts +58 -0
  226. package/src/console/ui/diff/group.ts +97 -0
  227. package/src/console/ui/diff/index.ts +38 -0
  228. package/src/console/ui/diff/search.test.ts +32 -0
  229. package/src/console/ui/diff/search.ts +75 -0
  230. package/src/console/ui/diff/summary.ts +74 -0
  231. package/src/console/ui/diff/types.ts +48 -0
  232. package/src/console/ui/dist/assets/index-B71Yl_SS.js +10 -0
  233. package/src/console/ui/dist/assets/panel-access-Dd37LU2c.js +64 -0
  234. package/src/console/ui/dist/assets/panel-ai-CC7LR6-J.js +1 -0
  235. package/src/console/ui/dist/assets/panel-architecture-B5b3iKCz.js +1 -0
  236. package/src/console/ui/dist/assets/panel-channels-CeNjTKXp.js +1 -0
  237. package/src/console/ui/dist/assets/panel-clock-DgFTLoHV.js +1 -0
  238. package/src/console/ui/dist/assets/panel-diff-DxehccqB.js +1 -0
  239. package/src/console/ui/dist/assets/panel-flows-BtrVn-Eg.js +45 -0
  240. package/src/console/ui/dist/assets/panel-gates-Z9MKRGdH.js +1 -0
  241. package/src/console/ui/dist/assets/panel-overview-Bd48d9km.js +1 -0
  242. package/src/console/ui/dist/assets/panel-plugins-DWd0TowH.js +1 -0
  243. package/src/console/ui/dist/assets/panel-runs-BwsWqKeB.js +1 -0
  244. package/src/console/ui/dist/assets/panel-signals-9najbZY2.js +1 -0
  245. package/src/console/ui/dist/assets/panel-store-OHkP2pDp.js +1 -0
  246. package/src/console/ui/dist/assets/panel-traces-tn2JoY8U.js +1 -0
  247. package/src/console/ui/dist/assets/panel-vault-BbfWdox0.js +1 -0
  248. package/src/console/ui/dist/assets/rolldown-runtime-CNC7AqOf.js +1 -0
  249. package/src/console/ui/dist/assets/style-Cnl7WLya.css +3 -0
  250. package/src/console/ui/dist/index.html +14 -0
  251. package/src/console/ui/flows/FlowsA11yView.tsx +155 -0
  252. package/src/console/ui/flows/a11y.test.tsx +105 -0
  253. package/src/console/ui/flows/buffer.test.ts +91 -0
  254. package/src/console/ui/flows/buffer.ts +118 -0
  255. package/src/console/ui/flows/confirmation.ts +131 -0
  256. package/src/console/ui/flows/contract.test.ts +89 -0
  257. package/src/console/ui/flows/contract.ts +353 -0
  258. package/src/console/ui/flows/fixture.ts +81 -0
  259. package/src/console/ui/flows/graph.test.ts +101 -0
  260. package/src/console/ui/flows/graph.ts +549 -0
  261. package/src/console/ui/flows/index.ts +87 -0
  262. package/src/console/ui/flows/save-as-test.test.ts +47 -0
  263. package/src/console/ui/flows/save-as-test.ts +99 -0
  264. package/src/console/ui/flows/search.ts +255 -0
  265. package/src/console/ui/flows/tiers.ts +140 -0
  266. package/src/console/ui/gates/GatesA11yView.tsx +192 -0
  267. package/src/console/ui/gates/a11y.test.tsx +116 -0
  268. package/src/console/ui/gates/audit.test.ts +40 -0
  269. package/src/console/ui/gates/audit.ts +63 -0
  270. package/src/console/ui/gates/denial.test.ts +36 -0
  271. package/src/console/ui/gates/denial.ts +51 -0
  272. package/src/console/ui/gates/findings.ts +19 -0
  273. package/src/console/ui/gates/fixture.ts +189 -0
  274. package/src/console/ui/gates/group.test.ts +30 -0
  275. package/src/console/ui/gates/group.ts +102 -0
  276. package/src/console/ui/gates/index.ts +47 -0
  277. package/src/console/ui/gates/search.test.ts +49 -0
  278. package/src/console/ui/gates/search.ts +115 -0
  279. package/src/console/ui/gates/types.ts +121 -0
  280. package/src/console/ui/overview/OverviewA11yView.tsx +152 -0
  281. package/src/console/ui/overview/a11y.test.tsx +113 -0
  282. package/src/console/ui/overview/busiest.ts +26 -0
  283. package/src/console/ui/overview/compose.ts +133 -0
  284. package/src/console/ui/overview/cost.test.ts +39 -0
  285. package/src/console/ui/overview/cost.ts +138 -0
  286. package/src/console/ui/overview/fixture.ts +161 -0
  287. package/src/console/ui/overview/golden.ts +57 -0
  288. package/src/console/ui/overview/index.ts +59 -0
  289. package/src/console/ui/overview/rank.test.ts +136 -0
  290. package/src/console/ui/overview/rank.ts +210 -0
  291. package/src/console/ui/overview/slo.test.ts +73 -0
  292. package/src/console/ui/overview/slo.ts +229 -0
  293. package/src/console/ui/overview/types.ts +121 -0
  294. package/src/console/ui/overview/verdict.ts +131 -0
  295. package/src/console/ui/plugins/PluginsA11yView.tsx +227 -0
  296. package/src/console/ui/plugins/a11y.test.tsx +97 -0
  297. package/src/console/ui/plugins/command.test.ts +22 -0
  298. package/src/console/ui/plugins/command.ts +27 -0
  299. package/src/console/ui/plugins/findings.ts +39 -0
  300. package/src/console/ui/plugins/fixture.ts +233 -0
  301. package/src/console/ui/plugins/group.test.ts +38 -0
  302. package/src/console/ui/plugins/group.ts +67 -0
  303. package/src/console/ui/plugins/index.ts +40 -0
  304. package/src/console/ui/plugins/search.ts +67 -0
  305. package/src/console/ui/plugins/types.ts +115 -0
  306. package/src/console/ui/runs/RunsA11yView.tsx +267 -0
  307. package/src/console/ui/runs/a11y.test.tsx +99 -0
  308. package/src/console/ui/runs/explain.test.ts +23 -0
  309. package/src/console/ui/runs/explain.ts +32 -0
  310. package/src/console/ui/runs/fixture.ts +189 -0
  311. package/src/console/ui/runs/group.test.ts +24 -0
  312. package/src/console/ui/runs/group.ts +85 -0
  313. package/src/console/ui/runs/histogram.test.ts +31 -0
  314. package/src/console/ui/runs/histogram.ts +85 -0
  315. package/src/console/ui/runs/index.ts +75 -0
  316. package/src/console/ui/runs/project.ts +122 -0
  317. package/src/console/ui/runs/query.test.ts +50 -0
  318. package/src/console/ui/runs/query.ts +283 -0
  319. package/src/console/ui/runs/search.test.ts +45 -0
  320. package/src/console/ui/runs/search.ts +141 -0
  321. package/src/console/ui/runs/trace-link.test.ts +30 -0
  322. package/src/console/ui/runs/trace-link.ts +99 -0
  323. package/src/console/ui/runs/types.ts +137 -0
  324. package/src/console/ui/shell/App.tsx +56 -0
  325. package/src/console/ui/shell/client.ts +1715 -0
  326. package/src/console/ui/shell/components/ui.tsx +110 -0
  327. package/src/console/ui/shell/index.html +12 -0
  328. package/src/console/ui/shell/layout/Shell.tsx +95 -0
  329. package/src/console/ui/shell/main.tsx +281 -0
  330. package/src/console/ui/shell/panels/Access.tsx +5 -0
  331. package/src/console/ui/shell/panels/Ai.tsx +5 -0
  332. package/src/console/ui/shell/panels/Architecture.tsx +5 -0
  333. package/src/console/ui/shell/panels/Channels.tsx +5 -0
  334. package/src/console/ui/shell/panels/Clock.tsx +5 -0
  335. package/src/console/ui/shell/panels/Diff.tsx +5 -0
  336. package/src/console/ui/shell/panels/Flows.tsx +5 -0
  337. package/src/console/ui/shell/panels/Gates.tsx +5 -0
  338. package/src/console/ui/shell/panels/Overview.tsx +6 -0
  339. package/src/console/ui/shell/panels/Plugins.tsx +5 -0
  340. package/src/console/ui/shell/panels/Runs.tsx +5 -0
  341. package/src/console/ui/shell/panels/Signals.tsx +5 -0
  342. package/src/console/ui/shell/panels/Store.tsx +5 -0
  343. package/src/console/ui/shell/panels/Traces.tsx +5 -0
  344. package/src/console/ui/shell/panels/Vault.tsx +5 -0
  345. package/src/console/ui/shell/panels/access/AccessPanel.tsx +813 -0
  346. package/src/console/ui/shell/panels/ai/AiPanel.tsx +563 -0
  347. package/src/console/ui/shell/panels/architecture/ArchitecturePanel.tsx +377 -0
  348. package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +454 -0
  349. package/src/console/ui/shell/panels/clock/ClockPanel.tsx +479 -0
  350. package/src/console/ui/shell/panels/diff/DiffPanel.tsx +275 -0
  351. package/src/console/ui/shell/panels/flows/ContractEditor.tsx +301 -0
  352. package/src/console/ui/shell/panels/flows/FlowDrawer.tsx +350 -0
  353. package/src/console/ui/shell/panels/flows/FlowsPanel.tsx +502 -0
  354. package/src/console/ui/shell/panels/gates/GatesPanel.tsx +482 -0
  355. package/src/console/ui/shell/panels/overview/OverviewPanel.tsx +428 -0
  356. package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +413 -0
  357. package/src/console/ui/shell/panels/runs/RunsPanel.tsx +649 -0
  358. package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +840 -0
  359. package/src/console/ui/shell/panels/store/StorePanel.tsx +787 -0
  360. package/src/console/ui/shell/panels/traces/TracesPanel.tsx +539 -0
  361. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +449 -0
  362. package/src/console/ui/shell/plugin/Sandbox.tsx +29 -0
  363. package/src/console/ui/shell/setup/Wizard.tsx +189 -0
  364. package/src/console/ui/shell/styles.css +75 -0
  365. package/src/console/ui/signals/SignalsA11yView.tsx +276 -0
  366. package/src/console/ui/signals/a11y.test.tsx +99 -0
  367. package/src/console/ui/signals/confirmation.test.ts +39 -0
  368. package/src/console/ui/signals/confirmation.ts +63 -0
  369. package/src/console/ui/signals/dry-run.test.ts +18 -0
  370. package/src/console/ui/signals/dry-run.ts +36 -0
  371. package/src/console/ui/signals/durable.test.ts +22 -0
  372. package/src/console/ui/signals/durable.ts +41 -0
  373. package/src/console/ui/signals/findings.ts +33 -0
  374. package/src/console/ui/signals/fixture.ts +242 -0
  375. package/src/console/ui/signals/group.test.ts +32 -0
  376. package/src/console/ui/signals/group.ts +49 -0
  377. package/src/console/ui/signals/index.ts +52 -0
  378. package/src/console/ui/signals/monitor.test.ts +30 -0
  379. package/src/console/ui/signals/monitor.ts +79 -0
  380. package/src/console/ui/signals/schema-form.test.ts +34 -0
  381. package/src/console/ui/signals/schema-form.ts +112 -0
  382. package/src/console/ui/signals/search.ts +102 -0
  383. package/src/console/ui/signals/types.ts +71 -0
  384. package/src/console/ui/store/StoreA11yView.tsx +199 -0
  385. package/src/console/ui/store/a11y.test.tsx +99 -0
  386. package/src/console/ui/store/cache-view.test.ts +13 -0
  387. package/src/console/ui/store/cache-view.ts +36 -0
  388. package/src/console/ui/store/confirmation.test.ts +38 -0
  389. package/src/console/ui/store/confirmation.ts +55 -0
  390. package/src/console/ui/store/dry-run.test.ts +15 -0
  391. package/src/console/ui/store/dry-run.ts +29 -0
  392. package/src/console/ui/store/fixture.ts +163 -0
  393. package/src/console/ui/store/group.test.ts +21 -0
  394. package/src/console/ui/store/group.ts +41 -0
  395. package/src/console/ui/store/index.ts +40 -0
  396. package/src/console/ui/store/search.ts +78 -0
  397. package/src/console/ui/store/types.ts +65 -0
  398. package/src/console/ui/store/will-not-fire.test.ts +19 -0
  399. package/src/console/ui/store/will-not-fire.ts +40 -0
  400. package/src/console/ui/traces/TracesA11yView.tsx +200 -0
  401. package/src/console/ui/traces/a11y.test.tsx +97 -0
  402. package/src/console/ui/traces/chain.test.ts +61 -0
  403. package/src/console/ui/traces/chain.ts +144 -0
  404. package/src/console/ui/traces/critical-path.test.ts +24 -0
  405. package/src/console/ui/traces/critical-path.ts +59 -0
  406. package/src/console/ui/traces/filter.test.ts +78 -0
  407. package/src/console/ui/traces/filter.ts +112 -0
  408. package/src/console/ui/traces/fixture.ts +126 -0
  409. package/src/console/ui/traces/fold.test.ts +67 -0
  410. package/src/console/ui/traces/fold.ts +170 -0
  411. package/src/console/ui/traces/index.ts +64 -0
  412. package/src/console/ui/traces/mini.ts +65 -0
  413. package/src/console/ui/traces/replay.test.ts +24 -0
  414. package/src/console/ui/traces/replay.ts +35 -0
  415. package/src/console/ui/traces/sampling.test.ts +26 -0
  416. package/src/console/ui/traces/sampling.ts +66 -0
  417. package/src/console/ui/traces/search.ts +148 -0
  418. package/src/console/ui/traces/tier.ts +72 -0
  419. package/src/console/ui/traces/types.ts +125 -0
  420. package/src/console/ui/tsconfig.json +20 -0
  421. package/src/console/ui/vault/VaultA11yView.tsx +177 -0
  422. package/src/console/ui/vault/a11y.test.tsx +97 -0
  423. package/src/console/ui/vault/blast-radius.test.ts +35 -0
  424. package/src/console/ui/vault/blast-radius.ts +55 -0
  425. package/src/console/ui/vault/confirmation.test.ts +41 -0
  426. package/src/console/ui/vault/confirmation.ts +39 -0
  427. package/src/console/ui/vault/dormant.test.ts +37 -0
  428. package/src/console/ui/vault/dormant.ts +28 -0
  429. package/src/console/ui/vault/export-safe.test.ts +30 -0
  430. package/src/console/ui/vault/export-safe.ts +65 -0
  431. package/src/console/ui/vault/fixture.ts +75 -0
  432. package/src/console/ui/vault/group.test.ts +25 -0
  433. package/src/console/ui/vault/group.ts +50 -0
  434. package/src/console/ui/vault/index.ts +45 -0
  435. package/src/console/ui/vault/search.ts +62 -0
  436. package/src/console/ui/vault/types.ts +57 -0
  437. package/src/console/ui/vite.config.ts +84 -0
  438. package/src/console/xss.gate.test.ts +118 -0
  439. package/src/docker/compose.ts +226 -0
  440. package/src/docker/credentials.ts +25 -0
  441. package/src/docker/derive.ts +83 -0
  442. package/src/docker/docker.test.ts +185 -0
  443. package/src/docker/dockerfile.integration.test.ts +170 -0
  444. package/src/docker/dockerfile.ts +39 -0
  445. package/src/docker/helpers.ts +103 -0
  446. package/src/docker/index.ts +52 -0
  447. package/src/docker/pin.ts +90 -0
  448. package/src/docker/recipes/index.ts +33 -0
  449. package/src/docker/recipes/postgres.ts +18 -0
  450. package/src/docker/recipes/redis.ts +18 -0
  451. package/src/docker/stack.integration.test.ts +140 -0
  452. package/src/docker/stack.ts +62 -0
  453. package/src/docker/types.ts +134 -0
  454. package/src/drivers/ai-anthropic.ts +128 -0
  455. package/src/drivers/ai-mock.ts +82 -0
  456. package/src/drivers/ai-openai-compatible.ts +139 -0
  457. package/src/drivers/ai-providers.test.ts +163 -0
  458. package/src/drivers/ai-types.ts +83 -0
  459. package/src/drivers/bun-native-completeness.test.ts +155 -0
  460. package/src/drivers/channel-console.ts +118 -0
  461. package/src/drivers/channel-fcm.ts +96 -0
  462. package/src/drivers/channel-resend.ts +27 -0
  463. package/src/drivers/channel-smtp.ts +33 -0
  464. package/src/drivers/channel-types.ts +160 -0
  465. package/src/drivers/channel-unifonic.ts +79 -0
  466. package/src/drivers/channel-wa-cloud.ts +84 -0
  467. package/src/drivers/channel-webpush.ts +317 -0
  468. package/src/drivers/conformance.test.ts +89 -0
  469. package/src/drivers/conformance.ts +162 -0
  470. package/src/drivers/fs.ts +73 -0
  471. package/src/drivers/index.ts +194 -0
  472. package/src/drivers/kv-lua.ts +222 -0
  473. package/src/drivers/memory.ts +472 -0
  474. package/src/drivers/pgvector.ts +130 -0
  475. package/src/drivers/postgres.ts +221 -0
  476. package/src/drivers/redis.ts +254 -0
  477. package/src/drivers/s3.ts +129 -0
  478. package/src/drivers/signal-engine.ts +690 -0
  479. package/src/drivers/signal-memory.ts +27 -0
  480. package/src/drivers/signal-nats.ts +131 -0
  481. package/src/drivers/signal-postgres.ts +1089 -0
  482. package/src/drivers/signal-redis.ts +275 -0
  483. package/src/drivers/signal-types.ts +328 -0
  484. package/src/drivers/sqlite.ts +53 -0
  485. package/src/drivers/types.ts +334 -0
  486. package/src/drivers/vault-dotenv-parse.ts +56 -0
  487. package/src/drivers/vault-env.ts +101 -0
  488. package/src/drivers/vault-infisical.ts +61 -0
  489. package/src/drivers/vault-managed.ts +42 -0
  490. package/src/drivers/vault-memory.ts +38 -0
  491. package/src/drivers/vault-openbao.ts +80 -0
  492. package/src/drivers/vault-sops.ts +252 -0
  493. package/src/drivers/vault-types.ts +84 -0
  494. package/src/elements/ai/declare.ts +206 -0
  495. package/src/elements/ai/eval.ts +113 -0
  496. package/src/elements/ai/pii.ts +108 -0
  497. package/src/elements/ai/runtime.ts +540 -0
  498. package/src/elements/ai/schema.ts +194 -0
  499. package/src/elements/ai.test.ts +393 -0
  500. package/src/elements/ai.ts +56 -0
  501. package/src/elements/channel/consent.ts +81 -0
  502. package/src/elements/channel/costs.test.ts +62 -0
  503. package/src/elements/channel/costs.ts +130 -0
  504. package/src/elements/channel/declare.ts +141 -0
  505. package/src/elements/channel/email-auth.test.ts +47 -0
  506. package/src/elements/channel/email-auth.ts +109 -0
  507. package/src/elements/channel/locale.ts +97 -0
  508. package/src/elements/channel/mask.test.ts +19 -0
  509. package/src/elements/channel/mask.ts +60 -0
  510. package/src/elements/channel/mime.ts +23 -0
  511. package/src/elements/channel/outcomes.test.ts +40 -0
  512. package/src/elements/channel/outcomes.ts +126 -0
  513. package/src/elements/channel/receipts.ts +121 -0
  514. package/src/elements/channel/runtime.ts +500 -0
  515. package/src/elements/channel/suppression.ts +136 -0
  516. package/src/elements/channel.test.ts +232 -0
  517. package/src/elements/channel.ts +109 -0
  518. package/src/elements/clock/actions.ts +170 -0
  519. package/src/elements/clock/declare.ts +58 -0
  520. package/src/elements/clock/dst.ts +197 -0
  521. package/src/elements/clock/durable.ts +140 -0
  522. package/src/elements/clock/duration.ts +29 -0
  523. package/src/elements/clock/health.test.ts +103 -0
  524. package/src/elements/clock/health.ts +112 -0
  525. package/src/elements/clock/leader.ts +127 -0
  526. package/src/elements/clock/reconcile.ts +241 -0
  527. package/src/elements/clock/runtime.ts +243 -0
  528. package/src/elements/clock/schedule.ts +262 -0
  529. package/src/elements/clock/time-travel.ts +53 -0
  530. package/src/elements/clock.test.ts +365 -0
  531. package/src/elements/clock.ts +80 -0
  532. package/src/elements/gate/declare.ts +134 -0
  533. package/src/elements/gate/permissions.ts +104 -0
  534. package/src/elements/gate/runtime.ts +177 -0
  535. package/src/elements/gate/strategies.ts +388 -0
  536. package/src/elements/gate.test.ts +145 -0
  537. package/src/elements/gate.ts +41 -0
  538. package/src/elements/index.ts +169 -0
  539. package/src/elements/signal/chaos-child.ts +47 -0
  540. package/src/elements/signal/declare.ts +77 -0
  541. package/src/elements/signal/dry-run-replay.test.ts +95 -0
  542. package/src/elements/signal/dry-run-write-isolation.test.ts +94 -0
  543. package/src/elements/signal/reconcile.test.ts +23 -0
  544. package/src/elements/signal/reconcile.ts +122 -0
  545. package/src/elements/signal/runtime.ts +94 -0
  546. package/src/elements/signal.test.ts +457 -0
  547. package/src/elements/signal.ts +26 -0
  548. package/src/elements/store/cache.ts +197 -0
  549. package/src/elements/store/classify.ts +125 -0
  550. package/src/elements/store/declare.ts +200 -0
  551. package/src/elements/store/files-policy.test.ts +31 -0
  552. package/src/elements/store/files-policy.ts +70 -0
  553. package/src/elements/store/replica.ts +68 -0
  554. package/src/elements/store/runtime.ts +393 -0
  555. package/src/elements/store/sql-condition.ts +205 -0
  556. package/src/elements/store/sql-session.ts +579 -0
  557. package/src/elements/store/table.ts +282 -0
  558. package/src/elements/store.test.ts +370 -0
  559. package/src/elements/store.ts +99 -0
  560. package/src/elements/vault/declare.ts +143 -0
  561. package/src/elements/vault/fingerprint.ts +30 -0
  562. package/src/elements/vault/redact.ts +52 -0
  563. package/src/elements/vault/runtime.ts +461 -0
  564. package/src/elements/vault.test.ts +296 -0
  565. package/src/elements/vault.ts +44 -0
  566. package/src/index.ts +183 -0
  567. package/src/kernel/adopt-routes.ts +194 -0
  568. package/src/kernel/app.ts +1068 -0
  569. package/src/kernel/auth-resolve.ts +93 -0
  570. package/src/kernel/boot.test.ts +173 -0
  571. package/src/kernel/boot.ts +439 -0
  572. package/src/kernel/budget-entry.ts +55 -0
  573. package/src/kernel/budget.test.ts +19 -0
  574. package/src/kernel/call.test.ts +83 -0
  575. package/src/kernel/capability.ts +113 -0
  576. package/src/kernel/dry-run.test.ts +64 -0
  577. package/src/kernel/dry-run.ts +171 -0
  578. package/src/kernel/effects.test.ts +89 -0
  579. package/src/kernel/effects.ts +140 -0
  580. package/src/kernel/errors.registry.test.ts +39 -0
  581. package/src/kernel/errors.ts +225 -0
  582. package/src/kernel/flow.test.ts +154 -0
  583. package/src/kernel/flow.ts +290 -0
  584. package/src/kernel/fx.test.ts +245 -0
  585. package/src/kernel/fx.ts +996 -0
  586. package/src/kernel/hook-timing.test.ts +72 -0
  587. package/src/kernel/hook-timing.ts +190 -0
  588. package/src/kernel/hooks.test.ts +181 -0
  589. package/src/kernel/hooks.ts +275 -0
  590. package/src/kernel/index.ts +278 -0
  591. package/src/kernel/journal.ts +405 -0
  592. package/src/kernel/journey.ts +75 -0
  593. package/src/kernel/on.ts +74 -0
  594. package/src/kernel/pipeline.test.ts +349 -0
  595. package/src/kernel/pipeline.ts +245 -0
  596. package/src/kernel/plug.ts +82 -0
  597. package/src/kernel/plugin/capabilities.test.ts +84 -0
  598. package/src/kernel/plugin/conflicts.test.ts +106 -0
  599. package/src/kernel/plugin/decorate.test.ts +111 -0
  600. package/src/kernel/plugin/scoping.test.ts +138 -0
  601. package/src/kernel/plugin.ts +485 -0
  602. package/src/kernel/rate-limit.ts +25 -0
  603. package/src/kernel/registry.ts +398 -0
  604. package/src/kernel/router.test.ts +114 -0
  605. package/src/kernel/router.ts +556 -0
  606. package/src/kernel/routing-budget.test.ts +19 -0
  607. package/src/kernel/run-telemetry.ts +70 -0
  608. package/src/kernel/triggers.ts +324 -0
  609. package/src/kernel/unit.ts +88 -0
  610. package/src/manifest/diff.test.ts +866 -0
  611. package/src/manifest/diff.ts +1670 -0
  612. package/src/manifest/fixtures/base.manifest.json +112 -0
  613. package/src/manifest/fixtures/skyport.excerpt.json +51 -0
  614. package/src/manifest/fixtures/skyport.manifest.json +153 -0
  615. package/src/manifest/index.ts +27 -0
  616. package/src/manifest/types.ts +358 -0
  617. package/src/manifest/undeclared.test.ts +82 -0
  618. package/src/manifest/undeclared.ts +65 -0
  619. package/src/manifest/validate.test.ts +105 -0
  620. package/src/manifest/validate.ts +174 -0
  621. package/src/mcp/authorization.ts +340 -0
  622. package/src/mcp/confirmation.ts +174 -0
  623. package/src/mcp/data.ts +90 -0
  624. package/src/mcp/index.ts +80 -0
  625. package/src/mcp/injection.gate.test.ts +176 -0
  626. package/src/mcp/mcp.test.ts +348 -0
  627. package/src/mcp/protocol.ts +172 -0
  628. package/src/mcp/server.ts +283 -0
  629. package/src/mcp/session.ts +139 -0
  630. package/src/mcp/tools.ts +391 -0
  631. package/src/plugins/catalogue.test.ts +48 -0
  632. package/src/plugins/catalogue.ts +113 -0
  633. package/src/plugins/node-import-scan.test.ts +28 -0
  634. package/src/plugins/node-import-scan.ts +98 -0
  635. package/src/plugins/supply-chain.test.ts +48 -0
  636. package/src/plugins/supply-chain.ts +460 -0
  637. package/src/release/index.ts +30 -0
  638. package/src/release/limits.ts +20 -0
  639. package/src/release/measure.ts +443 -0
  640. package/src/release/publish.ts +56 -0
  641. package/src/runs/bench.test.ts +93 -0
  642. package/src/runs/collect.ts +144 -0
  643. package/src/runs/drivers/clickhouse.ts +108 -0
  644. package/src/runs/drivers/files.ts +201 -0
  645. package/src/runs/drivers/memory.ts +94 -0
  646. package/src/runs/drivers/postgres.ts +110 -0
  647. package/src/runs/duckdb.ts +99 -0
  648. package/src/runs/index.ts +82 -0
  649. package/src/runs/outlier.ts +180 -0
  650. package/src/runs/parquet.ts +210 -0
  651. package/src/runs/privacy.ts +49 -0
  652. package/src/runs/runs.test.ts +324 -0
  653. package/src/runs/runtime.ts +229 -0
  654. package/src/runs/shred.ts +243 -0
  655. package/src/runs/telemetry.ts +10 -0
  656. package/src/runs/types.ts +179 -0
  657. package/src/runtime/bun.ts +140 -0
  658. package/src/runtime/cold-start.bench.ts +20 -0
  659. package/src/runtime/index.ts +46 -0
  660. package/src/runtime/primitives.ts +242 -0
  661. package/src/runtime/security.test.ts +170 -0
  662. package/src/runtime/security.ts +168 -0
  663. package/src/runtime/serve.test.ts +223 -0
  664. package/src/runtime/types.ts +217 -0
  665. package/src/runtime/web-standard.ts +77 -0
  666. package/src/test/create-test-app.test.ts +161 -0
  667. package/src/test/create-test-app.ts +533 -0
  668. package/src/test/index.ts +21 -0
  669. package/src/test/provisions.integration.test.ts +174 -0
  670. package/src/upgrade/codemods.test.ts +58 -0
  671. package/src/upgrade/codemods.ts +83 -0
  672. package/src/validation/index.ts +23 -0
  673. package/src/validation/standard-schema.test.ts +96 -0
  674. package/src/validation/standard-schema.ts +316 -0
@@ -0,0 +1,1089 @@
1
+ /**
2
+ * `postgres` signal driver — transactional emit (dual-write fix).
3
+ *
4
+ * `fx.emit` inserts into the outbox on the caller's connection. Consumers
5
+ * claim with `FOR UPDATE SKIP LOCKED`; wakeups use `LISTEN` / `NOTIFY`.
6
+ */
7
+
8
+ import type { SignalDecl } from "../elements/signal/declare.ts";
9
+ import type { SignalDelivery } from "../manifest/types.ts";
10
+ import {
11
+ DryRunWriteIsolationError,
12
+ setDryRunMessageId,
13
+ withDryRun,
14
+ } from "../kernel/dry-run.ts";
15
+ import type {
16
+ DeadLetter,
17
+ LiveHandler,
18
+ SignalBus,
19
+ SignalDiscardOptions,
20
+ SignalDriver,
21
+ SignalFailureReason,
22
+ SignalHandler,
23
+ SignalMessage,
24
+ SignalOpenOptions,
25
+ SignalReplayOptions,
26
+ SignalReplayResult,
27
+ SignalStats,
28
+ SignalTransaction,
29
+ SignalUnsubscribe,
30
+ } from "./signal-types.ts";
31
+
32
+ /** Row shape in `oke_signal_messages`. */
33
+ interface MsgRow {
34
+ id: string;
35
+ signal: string;
36
+ payload: string;
37
+ delivery: SignalDelivery;
38
+ attempts: number;
39
+ failures: string;
40
+ created_at: number;
41
+ available_at: number;
42
+ status: "pending" | "inflight" | "delivered" | "dead";
43
+ locked_by: string | null;
44
+ delivered_to: string;
45
+ }
46
+
47
+ /** Minimal SQL + listen surface for the postgres signal driver. */
48
+ export interface PostgresSignalSql {
49
+ query(sql: string, params?: readonly unknown[]): Promise<Record<string, unknown>[]>;
50
+ exec(sql: string, params?: readonly unknown[]): Promise<{ changes: number }>;
51
+ /**
52
+ * Run `fn` inside a transaction. Nested calls join the outer txn.
53
+ *
54
+ * @param fn - Body
55
+ */
56
+ begin<T>(fn: (sql: PostgresSignalSql) => Promise<T>): Promise<T>;
57
+ /**
58
+ * LISTEN channel; returns unsubscribe.
59
+ *
60
+ * @param channel - Channel name
61
+ * @param onNotify - Payload callback
62
+ */
63
+ listen(channel: string, onNotify: (payload: string) => void): Promise<() => void>;
64
+ /**
65
+ * NOTIFY channel.
66
+ *
67
+ * @param channel - Channel name
68
+ * @param payload - Payload
69
+ */
70
+ notify(channel: string, payload: string): Promise<void>;
71
+ close(): Promise<void>;
72
+ }
73
+
74
+ const CHANNEL = "oke_signal";
75
+
76
+ /**
77
+ * In-memory Postgres-protocol fake with transactions, SKIP LOCKED, LISTEN/NOTIFY.
78
+ */
79
+ export function createPostgresSignalFake(options?: {
80
+ readonly now?: () => number;
81
+ readonly durablePath?: string;
82
+ }): PostgresSignalSql & {
83
+ /** Force-kill mid-transaction (drops uncommitted state). */
84
+ killActiveTransaction(): void;
85
+ } {
86
+ const listeners = new Map<string, Set<(payload: string) => void>>();
87
+
88
+ type State = {
89
+ messages: MsgRow[];
90
+ writes: Map<string, unknown>;
91
+ };
92
+
93
+ let committed: State = { messages: [], writes: new Map() };
94
+ let active: { state: State; done: boolean } | null = null;
95
+
96
+ async function hydrate(): Promise<void> {
97
+ if (!options?.durablePath) return;
98
+ const file = Bun.file(options.durablePath);
99
+ if (!(await file.exists())) return;
100
+ const text = await file.text();
101
+ if (!text.trim()) return;
102
+ const snap = JSON.parse(text) as {
103
+ messages: MsgRow[];
104
+ writes: Array<[string, unknown]>;
105
+ };
106
+ committed = {
107
+ messages: snap.messages,
108
+ writes: new Map(snap.writes),
109
+ };
110
+ }
111
+
112
+ async function persist(): Promise<void> {
113
+ if (!options?.durablePath) return;
114
+ const { mkdir } = await import("node:fs/promises");
115
+ const { dirname } = await import("node:path");
116
+ await mkdir(dirname(options.durablePath), { recursive: true });
117
+ await Bun.write(
118
+ options.durablePath,
119
+ JSON.stringify({
120
+ messages: committed.messages,
121
+ writes: [...committed.writes.entries()],
122
+ }),
123
+ );
124
+ }
125
+
126
+ // Eager load if durable — open() will await ensureReady.
127
+ let ready: Promise<void> | null = null;
128
+ function ensureReady(): Promise<void> {
129
+ if (!ready) ready = hydrate();
130
+ return ready;
131
+ }
132
+
133
+ function view(): State {
134
+ return active?.state ?? committed;
135
+ }
136
+
137
+ function cloneState(s: State): State {
138
+ return {
139
+ messages: s.messages.map((m) => ({ ...m })),
140
+ writes: new Map(s.writes),
141
+ };
142
+ }
143
+
144
+ const api: PostgresSignalSql & { killActiveTransaction(): void; _ensureReady: () => Promise<void> } = {
145
+ _ensureReady: ensureReady,
146
+ killActiveTransaction() {
147
+ active = null;
148
+ },
149
+ async query(sql, params = []) {
150
+ await ensureReady();
151
+ const text = sql.trim();
152
+ const state = view();
153
+
154
+ // Claim: SELECT … FOR UPDATE SKIP LOCKED
155
+ const claim =
156
+ /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+signal\s*=\s*\?\s+AND\s+delivery\s*=\s*'once'\s+AND\s+status\s*=\s*'pending'\s+AND\s+available_at\s*<=\s*\?\s+AND\s+locked_by\s+IS\s+NULL\s+ORDER\s+BY\s+created_at\s+ASC\s+LIMIT\s+1\s+FOR\s+UPDATE\s+SKIP\s+LOCKED$/i.exec(
157
+ text,
158
+ );
159
+ if (claim) {
160
+ const signal = String(params[0]);
161
+ const t = Number(params[1]);
162
+ const row = state.messages.find(
163
+ (m) =>
164
+ m.signal === signal &&
165
+ m.delivery === "once" &&
166
+ m.status === "pending" &&
167
+ m.available_at <= t &&
168
+ m.locked_by === null,
169
+ );
170
+ if (!row) return [];
171
+ row.status = "inflight";
172
+ row.locked_by = String(params[2] ?? "worker");
173
+ row.attempts += 1;
174
+ return [{ ...row }];
175
+ }
176
+
177
+ const selMsg =
178
+ /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+signal\s*=\s*\?\s+AND\s+status\s*=\s*\?\s*$/i.exec(
179
+ text,
180
+ );
181
+ if (selMsg) {
182
+ return state.messages
183
+ .filter(
184
+ (m) => m.signal === params[0] && m.status === params[1],
185
+ )
186
+ .map((m) => ({ ...m }));
187
+ }
188
+
189
+ const selPending =
190
+ /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+status\s+IN\s*\('pending',\s*'inflight'\)\s*$/i.exec(
191
+ text,
192
+ );
193
+ if (selPending) {
194
+ return state.messages
195
+ .filter((m) => m.status === "pending" || m.status === "inflight")
196
+ .map((m) => ({ ...m }));
197
+ }
198
+
199
+ const selAll =
200
+ /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+signal\s*=\s*\?\s*$/i.exec(
201
+ text,
202
+ );
203
+ if (selAll) {
204
+ return state.messages
205
+ .filter((m) => m.signal === params[0])
206
+ .map((m) => ({ ...m }));
207
+ }
208
+
209
+ const selWrite =
210
+ /^SELECT\s+value\s+FROM\s+oke_signal_writes\s+WHERE\s+key\s*=\s*\?\s*$/i.exec(
211
+ text,
212
+ );
213
+ if (selWrite) {
214
+ const v = state.writes.get(String(params[0]));
215
+ if (v === undefined) return [];
216
+ return [{ value: JSON.stringify(v) }];
217
+ }
218
+
219
+ const byId =
220
+ /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+id\s*=\s*\?\s*$/i.exec(
221
+ text,
222
+ );
223
+ if (byId) {
224
+ return state.messages
225
+ .filter((m) => m.id === params[0])
226
+ .map((m) => ({ ...m }));
227
+ }
228
+
229
+ const selLive =
230
+ /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+signal\s*=\s*\?\s+AND\s+delivery\s*=\s*'live'\s+ORDER\s+BY\s+created_at\s+ASC\s*$/i.exec(
231
+ text,
232
+ );
233
+ if (selLive) {
234
+ return state.messages
235
+ .filter((m) => m.signal === params[0] && m.delivery === "live")
236
+ .map((m) => ({ ...m }));
237
+ }
238
+
239
+ throw new Error(`postgres signal fake: unsupported query: ${sql}`);
240
+ },
241
+ async exec(sql, params = []) {
242
+ await ensureReady();
243
+ const text = sql.trim();
244
+ const state = view();
245
+
246
+ if (/^CREATE\s+TABLE/i.test(text)) return { changes: 0 };
247
+
248
+ const delDead =
249
+ /^DELETE\s+FROM\s+oke_signal_messages\s+WHERE\s+id\s*=\s*\?\s+AND\s+signal\s*=\s*\?\s+AND\s+status\s*=\s*'dead'\s*$/i.exec(
250
+ text,
251
+ );
252
+ if (delDead) {
253
+ let changes = 0;
254
+ for (let i = state.messages.length - 1; i >= 0; i--) {
255
+ const m = state.messages[i]!;
256
+ if (
257
+ m.id === params[0] &&
258
+ m.signal === params[1] &&
259
+ m.status === "dead"
260
+ ) {
261
+ state.messages.splice(i, 1);
262
+ changes += 1;
263
+ }
264
+ }
265
+ return { changes };
266
+ }
267
+
268
+ const insertMsg =
269
+ /^INSERT\s+INTO\s+oke_signal_messages\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)\s*$/i.exec(
270
+ text,
271
+ );
272
+ if (insertMsg) {
273
+ const cols = insertMsg[1]!.split(",").map((c) => c.trim());
274
+ const row: Record<string, unknown> = {};
275
+ cols.forEach((c, i) => {
276
+ row[c] = params[i];
277
+ });
278
+ state.messages.push({
279
+ id: String(row.id),
280
+ signal: String(row.signal),
281
+ payload: String(row.payload),
282
+ delivery: row.delivery as SignalDelivery,
283
+ attempts: Number(row.attempts ?? 0),
284
+ failures: String(row.failures ?? "[]"),
285
+ created_at: Number(row.created_at),
286
+ available_at: Number(row.available_at),
287
+ status: (row.status as MsgRow["status"]) ?? "pending",
288
+ locked_by: (row.locked_by as string | null) ?? null,
289
+ delivered_to: String(row.delivered_to ?? "[]"),
290
+ });
291
+ return { changes: 1 };
292
+ }
293
+
294
+ const insertWrite =
295
+ /^INSERT\s+INTO\s+oke_signal_writes\s*\(key,\s*value\)\s*VALUES\s*\(\?,\s*\?\)\s*$/i.exec(
296
+ text,
297
+ );
298
+ if (insertWrite) {
299
+ state.writes.set(String(params[0]), JSON.parse(String(params[1])));
300
+ return { changes: 1 };
301
+ }
302
+
303
+ const upd =
304
+ /^UPDATE\s+oke_signal_messages\s+SET\s+(.+)\s+WHERE\s+id\s*=\s*\?\s*$/i.exec(
305
+ text,
306
+ );
307
+ if (upd) {
308
+ const id = String(params[params.length - 1]);
309
+ const row = state.messages.find((m) => m.id === id);
310
+ if (!row) return { changes: 0 };
311
+ const sets = upd[1]!.split(",").map((s) => s.trim());
312
+ let pi = 0;
313
+ const mut = row as unknown as Record<string, unknown>;
314
+ for (const set of sets) {
315
+ const [col, rhs] = set.split("=").map((x) => x.trim());
316
+ if (rhs === "?") {
317
+ mut[col!] = params[pi++];
318
+ } else if (rhs === "NULL") {
319
+ mut[col!] = null;
320
+ } else if (
321
+ (rhs?.startsWith("'") && rhs.endsWith("'")) ||
322
+ (rhs?.startsWith('"') && rhs.endsWith('"'))
323
+ ) {
324
+ mut[col!] = rhs.slice(1, -1);
325
+ }
326
+ }
327
+ return { changes: 1 };
328
+ }
329
+
330
+ throw new Error(`postgres signal fake: unsupported exec: ${sql}`);
331
+ },
332
+ async begin(fn) {
333
+ await ensureReady();
334
+ if (active && !active.done) {
335
+ // Join caller's transaction (dual-write enrolment).
336
+ return fn(api);
337
+ }
338
+ active = { state: cloneState(committed), done: false };
339
+ try {
340
+ const result = await fn(api);
341
+ if (active) {
342
+ committed = active.state;
343
+ active.done = true;
344
+ active = null;
345
+ await persist();
346
+ }
347
+ return result;
348
+ } catch (err) {
349
+ active = null;
350
+ throw err;
351
+ }
352
+ },
353
+ async listen(channel, onNotify) {
354
+ let set = listeners.get(channel);
355
+ if (!set) {
356
+ set = new Set();
357
+ listeners.set(channel, set);
358
+ }
359
+ set.add(onNotify);
360
+ return () => {
361
+ set!.delete(onNotify);
362
+ };
363
+ },
364
+ async notify(channel, payload) {
365
+ const set = listeners.get(channel);
366
+ if (!set) return;
367
+ for (const fn of set) fn(payload);
368
+ },
369
+ async close() {
370
+ listeners.clear();
371
+ active = null;
372
+ },
373
+ };
374
+
375
+ return api;
376
+ }
377
+
378
+ async function ensureSchema(sql: PostgresSignalSql): Promise<void> {
379
+ await sql.exec(`CREATE TABLE IF NOT EXISTS oke_signal_messages (
380
+ id TEXT PRIMARY KEY,
381
+ signal TEXT,
382
+ payload TEXT,
383
+ delivery TEXT,
384
+ attempts INTEGER,
385
+ failures TEXT,
386
+ created_at BIGINT,
387
+ available_at BIGINT,
388
+ status TEXT,
389
+ locked_by TEXT,
390
+ delivered_to TEXT
391
+ )`);
392
+ await sql.exec(`CREATE TABLE IF NOT EXISTS oke_signal_writes (
393
+ key TEXT PRIMARY KEY,
394
+ value TEXT
395
+ )`);
396
+ }
397
+
398
+ function rowToMessage(row: Record<string, unknown>): SignalMessage {
399
+ return {
400
+ id: String(row.id),
401
+ signal: String(row.signal),
402
+ payload: JSON.parse(String(row.payload)),
403
+ delivery: row.delivery as SignalDelivery,
404
+ attempts: Number(row.attempts),
405
+ failures: JSON.parse(String(row.failures ?? "[]")) as SignalFailureReason[],
406
+ createdAt: Number(row.created_at),
407
+ availableAt: Number(row.available_at),
408
+ status: row.status as SignalMessage["status"],
409
+ };
410
+ }
411
+
412
+ /**
413
+ * Open a postgres-backed signal bus.
414
+ *
415
+ * @param options - Declarations / sql fake / clock
416
+ */
417
+ export async function openPostgresSignal(
418
+ options: SignalOpenOptions,
419
+ ): Promise<SignalBus> {
420
+ const now = options.now ?? (() => Date.now());
421
+ const signals = options.signals;
422
+ const sql =
423
+ (options.sql as PostgresSignalSql | undefined) ??
424
+ createPostgresSignalFake({
425
+ now,
426
+ durablePath: options.durablePath,
427
+ });
428
+
429
+ if ("_ensureReady" in sql && typeof sql._ensureReady === "function") {
430
+ await (sql as { _ensureReady: () => Promise<void> })._ensureReady();
431
+ }
432
+ await ensureSchema(sql);
433
+
434
+ const consumers: Array<{
435
+ signal: string;
436
+ subscriberId: string;
437
+ handler: SignalHandler;
438
+ }> = [];
439
+ const liveHandlers = new Map<string, Set<LiveHandler>>();
440
+ const deliveredAt: number[] = [];
441
+ const recentLive = new Map<string, unknown[]>();
442
+ const subscriberErrors = new Map<string, number>();
443
+ let unlisten: (() => void) | null = null;
444
+ let draining: Promise<void> | null = null;
445
+
446
+ function failureFromError(
447
+ err: unknown,
448
+ attempt: number,
449
+ ): SignalFailureReason {
450
+ const code =
451
+ err &&
452
+ typeof err === "object" &&
453
+ "code" in err &&
454
+ typeof (err as { code: unknown }).code === "string"
455
+ ? (err as { code: string }).code
456
+ : err instanceof Error && err.name !== "Error"
457
+ ? err.name
458
+ : "handler_error";
459
+ return {
460
+ code,
461
+ message: err instanceof Error ? err.message : String(err),
462
+ at: now(),
463
+ attempt,
464
+ };
465
+ }
466
+
467
+ function noteDelivered(): void {
468
+ const t = now();
469
+ deliveredAt.push(t);
470
+ while (deliveredAt.length > 0 && deliveredAt[0]! < t - 1_000) {
471
+ deliveredAt.shift();
472
+ }
473
+ }
474
+
475
+ function throughputPerSec(): number {
476
+ const t = now();
477
+ let n = 0;
478
+ for (let i = deliveredAt.length - 1; i >= 0; i--) {
479
+ if (deliveredAt[i]! < t - 1_000) break;
480
+ n += 1;
481
+ }
482
+ return n;
483
+ }
484
+
485
+ unlisten = await sql.listen(CHANNEL, () => {
486
+ void drainQuiet();
487
+ });
488
+
489
+ function requireDecl(name: string): SignalDecl {
490
+ const decl = signals.get(name);
491
+ if (!decl) throw new Error(`Unknown signal: ${name}`);
492
+ return decl;
493
+ }
494
+
495
+ async function insertEmit(
496
+ tx: PostgresSignalSql,
497
+ signal: string,
498
+ payload: unknown,
499
+ ): Promise<void> {
500
+ const decl = requireDecl(signal);
501
+ const t = now();
502
+ await tx.exec(
503
+ `INSERT INTO oke_signal_messages (id, signal, payload, delivery, attempts, failures, created_at, available_at, status, locked_by, delivered_to) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
504
+ [
505
+ crypto.randomUUID(),
506
+ signal,
507
+ JSON.stringify(payload ?? null),
508
+ decl.delivery,
509
+ 0,
510
+ "[]",
511
+ t,
512
+ t,
513
+ "pending",
514
+ null,
515
+ "[]",
516
+ ],
517
+ );
518
+ }
519
+
520
+ async function begin(): Promise<SignalTransaction> {
521
+ let finished = false;
522
+ // Defer the real SQL begin until commit so staging is local,
523
+ // then enrol everything in one postgres transaction.
524
+ const stagedEmits: Array<{ signal: string; payload: unknown }> = [];
525
+ const stagedWrites = new Map<string, unknown>();
526
+
527
+ return {
528
+ async write(key, value) {
529
+ if (finished) throw new Error("transaction finished");
530
+ stagedWrites.set(key, value);
531
+ },
532
+ async emit(signal, payload) {
533
+ if (finished) throw new Error("transaction finished");
534
+ requireDecl(signal);
535
+ stagedEmits.push({ signal, payload });
536
+ },
537
+ async commit() {
538
+ if (finished) throw new Error("transaction finished");
539
+ finished = true;
540
+ await sql.begin(async (tx) => {
541
+ for (const [k, v] of stagedWrites) {
542
+ await tx.exec(
543
+ `INSERT INTO oke_signal_writes (key, value) VALUES (?, ?)`,
544
+ [k, JSON.stringify(v)],
545
+ );
546
+ }
547
+ for (const e of stagedEmits) {
548
+ await insertEmit(tx, e.signal, e.payload);
549
+ }
550
+ });
551
+ await sql.notify(CHANNEL, "commit");
552
+ },
553
+ async rollback() {
554
+ if (finished) throw new Error("transaction finished");
555
+ finished = true;
556
+ stagedEmits.length = 0;
557
+ stagedWrites.clear();
558
+ },
559
+ };
560
+ }
561
+
562
+ /**
563
+ * Enrol emit in an already-open SQL transaction (dual-write fix).
564
+ *
565
+ * @param tx - Caller's postgres transaction
566
+ * @param signal - Signal name
567
+ * @param payload - Payload
568
+ */
569
+ async function emitInTransaction(
570
+ tx: PostgresSignalSql,
571
+ signal: string,
572
+ payload?: unknown,
573
+ ): Promise<void> {
574
+ await insertEmit(tx, signal, payload);
575
+ }
576
+
577
+ async function emit(signal: string, payload?: unknown): Promise<void> {
578
+ const tx = await begin();
579
+ await tx.emit(signal, payload);
580
+ await tx.commit();
581
+ }
582
+
583
+ async function subscribe(
584
+ signal: string,
585
+ subscriberId: string,
586
+ handler: SignalHandler,
587
+ ): Promise<SignalUnsubscribe> {
588
+ requireDecl(signal);
589
+ const entry = { signal, subscriberId, handler };
590
+ consumers.push(entry);
591
+ return () => {
592
+ const i = consumers.indexOf(entry);
593
+ if (i >= 0) consumers.splice(i, 1);
594
+ };
595
+ }
596
+
597
+ async function live(
598
+ signal: string,
599
+ handler: LiveHandler,
600
+ ): Promise<SignalUnsubscribe> {
601
+ const decl = requireDecl(signal);
602
+ if (decl.delivery !== "live") {
603
+ throw new Error(`signal "${signal}" is not delivery: "live"`);
604
+ }
605
+ let set = liveHandlers.get(signal);
606
+ if (!set) {
607
+ set = new Set();
608
+ liveHandlers.set(signal, set);
609
+ }
610
+ set.add(handler);
611
+ const history = await sql.query(
612
+ `SELECT * FROM oke_signal_messages WHERE signal = ? AND delivery = 'live' ORDER BY created_at ASC`,
613
+ [signal],
614
+ );
615
+ for (const row of history) {
616
+ await handler(JSON.parse(String(row.payload)));
617
+ }
618
+ return () => {
619
+ set!.delete(handler);
620
+ };
621
+ }
622
+
623
+ async function deliverOnce(
624
+ row: Record<string, unknown>,
625
+ consumer: { subscriberId: string; handler: SignalHandler },
626
+ ): Promise<void> {
627
+ const decl = requireDecl(String(row.signal));
628
+ const msg = rowToMessage(row);
629
+ try {
630
+ await consumer.handler(msg);
631
+ await sql.exec(
632
+ `UPDATE oke_signal_messages SET status = ?, locked_by = NULL WHERE id = ?`,
633
+ ["delivered", msg.id],
634
+ );
635
+ noteDelivered();
636
+ } catch (err) {
637
+ const failures = [...msg.failures, failureFromError(err, msg.attempts)];
638
+ if (msg.attempts > decl.retries) {
639
+ await sql.exec(
640
+ `UPDATE oke_signal_messages SET status = ?, locked_by = NULL, failures = ? WHERE id = ?`,
641
+ [decl.deadLetter ? "dead" : "delivered", JSON.stringify(failures), msg.id],
642
+ );
643
+ } else {
644
+ await sql.exec(
645
+ `UPDATE oke_signal_messages SET status = ?, locked_by = NULL, failures = ?, available_at = ? WHERE id = ?`,
646
+ ["pending", JSON.stringify(failures), now(), msg.id],
647
+ );
648
+ }
649
+ }
650
+ }
651
+
652
+ async function drainQuiet(): Promise<void> {
653
+ try {
654
+ await drain();
655
+ } catch {
656
+ /* ignore during teardown */
657
+ }
658
+ }
659
+
660
+ async function drainOncePass(): Promise<boolean> {
661
+ let progress = false;
662
+ for (const consumer of consumers) {
663
+ const decl = signals.get(consumer.signal);
664
+ if (decl?.delivery !== "once") continue;
665
+ const claimed = await sql.query(
666
+ `SELECT * FROM oke_signal_messages WHERE signal = ? AND delivery = 'once' AND status = 'pending' AND available_at <= ? AND locked_by IS NULL ORDER BY created_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED`,
667
+ [consumer.signal, now(), consumer.subscriberId],
668
+ );
669
+ if (claimed[0]) {
670
+ progress = true;
671
+ const row = claimed[0];
672
+ if (row.locked_by == null) {
673
+ await sql.exec(
674
+ `UPDATE oke_signal_messages SET status = 'inflight', locked_by = ?, attempts = ? WHERE id = ?`,
675
+ [consumer.subscriberId, Number(row.attempts) + 1, row.id],
676
+ );
677
+ row.locked_by = consumer.subscriberId;
678
+ row.attempts = Number(row.attempts) + 1;
679
+ row.status = "inflight";
680
+ }
681
+ await deliverOnce(row, consumer);
682
+ }
683
+ }
684
+ return progress;
685
+ }
686
+
687
+ async function drainBroadcastPass(): Promise<boolean> {
688
+ let progress = false;
689
+ for (const consumer of consumers) {
690
+ const decl = signals.get(consumer.signal);
691
+ if (decl?.delivery !== "broadcast") continue;
692
+ const rows = await sql.query(
693
+ `SELECT * FROM oke_signal_messages WHERE signal = ?`,
694
+ [consumer.signal],
695
+ );
696
+ for (const row of rows) {
697
+ if (row.status === "dead" || row.status === "delivered") continue;
698
+ if (row.delivery !== "broadcast") continue;
699
+ // Re-read delivered_to so concurrent drain (NOTIFY) cannot double-deliver.
700
+ const fresh = await sql.query(
701
+ `SELECT * FROM oke_signal_messages WHERE signal = ?`,
702
+ [consumer.signal],
703
+ );
704
+ const current = fresh.find((r) => r.id === row.id);
705
+ if (!current || current.status === "delivered") continue;
706
+ const deliveredTo = new Set(
707
+ JSON.parse(String(current.delivered_to ?? "[]")) as string[],
708
+ );
709
+ if (deliveredTo.has(consumer.subscriberId)) continue;
710
+ // Claim this subscriber slot before invoking the handler.
711
+ deliveredTo.add(consumer.subscriberId);
712
+ const subs = consumers.filter((c) => c.signal === consumer.signal);
713
+ const status = subs.every((s) => deliveredTo.has(s.subscriberId))
714
+ ? "delivered"
715
+ : "pending";
716
+ await sql.exec(
717
+ `UPDATE oke_signal_messages SET delivered_to = ?, status = ?, attempts = ? WHERE id = ?`,
718
+ [
719
+ JSON.stringify([...deliveredTo]),
720
+ status,
721
+ Number(current.attempts) + 1,
722
+ row.id,
723
+ ],
724
+ );
725
+ progress = true;
726
+ const msg = rowToMessage(current);
727
+ try {
728
+ await consumer.handler(msg);
729
+ noteDelivered();
730
+ } catch (err) {
731
+ const failures = [
732
+ ...msg.failures,
733
+ failureFromError(err, Number(current.attempts) + 1),
734
+ ];
735
+ const key = `${consumer.signal}::${consumer.subscriberId}`;
736
+ subscriberErrors.set(key, (subscriberErrors.get(key) ?? 0) + 1);
737
+ await sql.exec(
738
+ `UPDATE oke_signal_messages SET failures = ? WHERE id = ?`,
739
+ [JSON.stringify(failures), row.id],
740
+ );
741
+ }
742
+ }
743
+ }
744
+ return progress;
745
+ }
746
+
747
+ async function drainLivePass(): Promise<boolean> {
748
+ let progress = false;
749
+ const pending = await sql.query(
750
+ `SELECT * FROM oke_signal_messages WHERE status IN ('pending', 'inflight')`,
751
+ );
752
+ for (const row of pending) {
753
+ if (row.delivery !== "live") continue;
754
+ progress = true;
755
+ const handlers = liveHandlers.get(String(row.signal));
756
+ const payload = JSON.parse(String(row.payload));
757
+ if (handlers) {
758
+ for (const h of handlers) await h(payload);
759
+ }
760
+ const sig = String(row.signal);
761
+ let list = recentLive.get(sig);
762
+ if (!list) {
763
+ list = [];
764
+ recentLive.set(sig, list);
765
+ }
766
+ list.push(payload);
767
+ while (list.length > 50) list.shift();
768
+ noteDelivered();
769
+ await sql.exec(
770
+ `UPDATE oke_signal_messages SET status = 'delivered', locked_by = NULL WHERE id = ?`,
771
+ [row.id],
772
+ );
773
+ }
774
+ return progress;
775
+ }
776
+
777
+ async function drain(): Promise<void> {
778
+ if (draining) {
779
+ await draining;
780
+ return;
781
+ }
782
+ draining = (async () => {
783
+ for (let i = 0; i < 1000; i++) {
784
+ const a = await drainOncePass();
785
+ const b = await drainBroadcastPass();
786
+ const c = await drainLivePass();
787
+ if (!a && !b && !c) break;
788
+ }
789
+ })();
790
+ try {
791
+ await draining;
792
+ } finally {
793
+ draining = null;
794
+ }
795
+ }
796
+
797
+ async function deadLetters(signal: string): Promise<readonly DeadLetter[]> {
798
+ const rows = await sql.query(
799
+ `SELECT * FROM oke_signal_messages WHERE signal = ? AND status = ?`,
800
+ [signal, "dead"],
801
+ );
802
+ return rows.map((r) => ({ ...rowToMessage(r), status: "dead" as const }));
803
+ }
804
+
805
+ async function statsFor(name: string): Promise<SignalStats | null> {
806
+ const decl = signals.get(name);
807
+ if (!decl) return null;
808
+ const rows = await sql.query(
809
+ `SELECT * FROM oke_signal_messages WHERE signal = ?`,
810
+ [name],
811
+ );
812
+ let pending = 0;
813
+ let inflight = 0;
814
+ let dead = 0;
815
+ let delivered = 0;
816
+ let oldestPending: number | null = null;
817
+ const deadList: DeadLetter[] = [];
818
+ for (const row of rows) {
819
+ const status = String(row.status);
820
+ if (status === "pending") {
821
+ pending += 1;
822
+ const created = Number(row.created_at);
823
+ if (oldestPending === null || created < oldestPending) {
824
+ oldestPending = created;
825
+ }
826
+ } else if (status === "inflight") {
827
+ inflight += 1;
828
+ const created = Number(row.created_at);
829
+ if (oldestPending === null || created < oldestPending) {
830
+ oldestPending = created;
831
+ }
832
+ } else if (status === "dead") {
833
+ dead += 1;
834
+ deadList.push({ ...rowToMessage(row), status: "dead" });
835
+ } else if (status === "delivered") {
836
+ delivered += 1;
837
+ }
838
+ }
839
+ const subs = consumers.filter((c) => c.signal === name);
840
+ const subscribers = await Promise.all(
841
+ subs.map(async (c) => {
842
+ let lag = 0;
843
+ for (const row of rows) {
844
+ if (row.delivery !== "broadcast") continue;
845
+ if (row.status === "dead" || row.status === "delivered") continue;
846
+ const deliveredTo = new Set(
847
+ JSON.parse(String(row.delivered_to ?? "[]")) as string[],
848
+ );
849
+ if (!deliveredTo.has(c.subscriberId)) lag += 1;
850
+ }
851
+ return {
852
+ id: c.subscriberId,
853
+ lag,
854
+ errorCount: subscriberErrors.get(`${name}::${c.subscriberId}`) ?? 0,
855
+ };
856
+ }),
857
+ );
858
+ return {
859
+ signal: name,
860
+ delivery: decl.delivery,
861
+ pending,
862
+ inflight,
863
+ dead,
864
+ delivered,
865
+ retries: decl.retries,
866
+ deadLetterEnabled: decl.deadLetter,
867
+ outboxLagMs:
868
+ oldestPending === null ? null : Math.max(0, now() - oldestPending),
869
+ subscribers,
870
+ connections: liveHandlers.get(name)?.size ?? 0,
871
+ throughputPerSec: throughputPerSec(),
872
+ schema: decl.schema,
873
+ recentLive: [...(recentLive.get(name) ?? [])],
874
+ deadLetters: deadList,
875
+ };
876
+ }
877
+
878
+ async function inspect(signal?: string): Promise<readonly SignalStats[]> {
879
+ if (signal) {
880
+ const one = await statsFor(signal);
881
+ return one ? [one] : [];
882
+ }
883
+ const out: SignalStats[] = [];
884
+ for (const name of signals.keys()) {
885
+ const s = await statsFor(name);
886
+ if (s) out.push(s);
887
+ }
888
+ return out;
889
+ }
890
+
891
+ async function replay(
892
+ options: SignalReplayOptions,
893
+ ): Promise<SignalReplayResult> {
894
+ requireDecl(options.signal);
895
+ const rate = Math.max(1, options.ratePerSec);
896
+ const intervalMs = Math.floor(1_000 / rate);
897
+ const dead = await deadLetters(options.signal);
898
+ const ids =
899
+ options.messageIds && options.messageIds.length > 0
900
+ ? new Set(options.messageIds)
901
+ : null;
902
+ const targets = dead.filter((m) => ids === null || ids.has(m.id));
903
+ const results: SignalReplayResult["results"][number][] = [];
904
+ const wouldHaveFired: SignalReplayResult["wouldHaveFired"][number][] = [];
905
+ let succeeded = 0;
906
+ let failed = 0;
907
+
908
+ for (let i = 0; i < targets.length; i++) {
909
+ if (i > 0 && intervalMs > 0) {
910
+ await new Promise((r) => setTimeout(r, intervalMs));
911
+ }
912
+ const m = targets[i]!;
913
+ const payload =
914
+ options.payloads?.[m.id] !== undefined
915
+ ? options.payloads[m.id]
916
+ : m.payload;
917
+ if (
918
+ options.payloads?.[m.id] !== undefined &&
919
+ !options.dryRun
920
+ ) {
921
+ await sql.exec(
922
+ `UPDATE oke_signal_messages SET payload = ? WHERE id = ?`,
923
+ [JSON.stringify(payload), m.id],
924
+ );
925
+ }
926
+
927
+ const handlers = consumers.filter((c) => {
928
+ if (c.signal !== m.signal) return false;
929
+ if (options.subscriberId) return c.subscriberId === options.subscriberId;
930
+ return true;
931
+ });
932
+
933
+ if (handlers.length === 0) {
934
+ results.push({
935
+ id: m.id,
936
+ ok: false,
937
+ error: {
938
+ code: "no_consumer",
939
+ message: "No consumer registered for replay",
940
+ },
941
+ });
942
+ failed += 1;
943
+ continue;
944
+ }
945
+
946
+ let ok = true;
947
+ let lastErr: { code: string; message: string } | undefined;
948
+ for (const h of handlers) {
949
+ try {
950
+ const msg = { ...m, payload };
951
+ if (options.dryRun) {
952
+ const stubbed = await withDryRun(async () => {
953
+ setDryRunMessageId(m.id);
954
+ await h.handler(msg);
955
+ });
956
+ for (const w of stubbed.wouldHaveFired) {
957
+ wouldHaveFired.push({
958
+ kind: w.kind,
959
+ resource: w.resource,
960
+ messageId: w.messageId ?? m.id,
961
+ });
962
+ }
963
+ } else {
964
+ await h.handler(msg);
965
+ }
966
+ } catch (err) {
967
+ if (options.dryRun && err instanceof DryRunWriteIsolationError) {
968
+ return {
969
+ attempted: 0,
970
+ succeeded: 0,
971
+ failed: 0,
972
+ dryRun: true,
973
+ results: [],
974
+ wouldHaveFired: [],
975
+ refused: {
976
+ code: "dry_run_unsafe",
977
+ reason: err.message,
978
+ },
979
+ };
980
+ }
981
+ ok = false;
982
+ const reason = failureFromError(err, m.attempts + 1);
983
+ lastErr = { code: reason.code, message: reason.message };
984
+ if (!options.dryRun) {
985
+ const failures = [...m.failures, reason];
986
+ await sql.exec(
987
+ `UPDATE oke_signal_messages SET failures = ? WHERE id = ?`,
988
+ [JSON.stringify(failures), m.id],
989
+ );
990
+ }
991
+ }
992
+ }
993
+
994
+ if (ok) {
995
+ succeeded += 1;
996
+ results.push({ id: m.id, ok: true });
997
+ if (!options.dryRun) {
998
+ if (options.subscriberId && m.delivery === "broadcast") {
999
+ const row = (
1000
+ await sql.query(
1001
+ `SELECT * FROM oke_signal_messages WHERE id = ?`,
1002
+ [m.id],
1003
+ )
1004
+ )[0];
1005
+ const deliveredTo = new Set(
1006
+ JSON.parse(String(row?.delivered_to ?? "[]")) as string[],
1007
+ );
1008
+ deliveredTo.delete(options.subscriberId);
1009
+ await sql.exec(
1010
+ `UPDATE oke_signal_messages SET status = 'pending', locked_by = NULL, attempts = 0, available_at = ?, delivered_to = ? WHERE id = ?`,
1011
+ [now(), JSON.stringify([...deliveredTo]), m.id],
1012
+ );
1013
+ } else {
1014
+ await sql.exec(
1015
+ `UPDATE oke_signal_messages SET status = 'pending', locked_by = NULL, attempts = 0, available_at = ?, delivered_to = '[]' WHERE id = ?`,
1016
+ [now(), m.id],
1017
+ );
1018
+ }
1019
+ }
1020
+ } else {
1021
+ failed += 1;
1022
+ results.push({ id: m.id, ok: false, error: lastErr });
1023
+ }
1024
+ }
1025
+
1026
+ return {
1027
+ attempted: targets.length,
1028
+ succeeded,
1029
+ failed,
1030
+ dryRun: options.dryRun,
1031
+ results,
1032
+ wouldHaveFired,
1033
+ };
1034
+ }
1035
+
1036
+ async function discard(
1037
+ options: SignalDiscardOptions,
1038
+ ): Promise<{ readonly discarded: number }> {
1039
+ let discarded = 0;
1040
+ for (const id of options.messageIds) {
1041
+ const result = await sql.exec(
1042
+ `DELETE FROM oke_signal_messages WHERE id = ? AND signal = ? AND status = 'dead'`,
1043
+ [id, options.signal],
1044
+ );
1045
+ discarded += result.changes;
1046
+ }
1047
+ return { discarded };
1048
+ }
1049
+
1050
+ async function getWrite(key: string): Promise<unknown> {
1051
+ const rows = await sql.query(
1052
+ `SELECT value FROM oke_signal_writes WHERE key = ?`,
1053
+ [key],
1054
+ );
1055
+ if (!rows[0]) return undefined;
1056
+ return JSON.parse(String(rows[0].value));
1057
+ }
1058
+
1059
+ const bus: SignalBus & {
1060
+ emitInTransaction: typeof emitInTransaction;
1061
+ sql: PostgresSignalSql;
1062
+ } = {
1063
+ driverId: "postgres",
1064
+ emit,
1065
+ begin,
1066
+ subscribe,
1067
+ live,
1068
+ drain,
1069
+ deadLetters,
1070
+ inspect,
1071
+ replay,
1072
+ discard,
1073
+ getWrite,
1074
+ async close() {
1075
+ unlisten?.();
1076
+ await sql.close();
1077
+ },
1078
+ emitInTransaction,
1079
+ sql,
1080
+ };
1081
+
1082
+ return bus;
1083
+ }
1084
+
1085
+ /** Protocol-named postgres signal driver. */
1086
+ export const postgresSignalDriver: SignalDriver = {
1087
+ id: "postgres",
1088
+ open: openPostgresSignal,
1089
+ };