@capybara-hq/kernel 0.1.0-rc.23 → 0.1.0-rc.25

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 (293) hide show
  1. package/bootstrap.json +25 -25
  2. package/dist/admin/admin.module.d.ts +2 -1
  3. package/dist/admin/admin.module.js +9 -3
  4. package/dist/admin/health.service.d.ts +2 -2
  5. package/dist/admin/health.service.js +6 -2
  6. package/dist/admin/outbox-dead.controller.d.ts +23 -0
  7. package/dist/admin/outbox-dead.controller.js +62 -0
  8. package/dist/admin/outbox-dead.schemas.d.ts +8 -0
  9. package/dist/admin/outbox-dead.schemas.js +18 -0
  10. package/dist/admin/outbox-dead.service.d.ts +66 -0
  11. package/dist/admin/outbox-dead.service.js +179 -0
  12. package/dist/admin/overview.service.d.ts +40 -5
  13. package/dist/admin/overview.service.js +41 -7
  14. package/dist/agents/agents.module.js +3 -0
  15. package/dist/agents/agents.service.d.ts +6 -1
  16. package/dist/agents/agents.service.js +11 -2
  17. package/dist/agents/from-template.js +1 -1
  18. package/dist/background/finish.d.ts +5 -2
  19. package/dist/background/finish.js +14 -3
  20. package/dist/background/launch.d.ts +2 -1
  21. package/dist/background/launch.js +5 -2
  22. package/dist/background/store.d.ts +8 -0
  23. package/dist/background/store.js +24 -0
  24. package/dist/catalog/admin-catalog.controller.d.ts +21 -2
  25. package/dist/catalog/admin-catalog.controller.js +45 -6
  26. package/dist/catalog/admin-catalog.schemas.d.ts +5 -0
  27. package/dist/catalog/admin-catalog.schemas.js +8 -0
  28. package/dist/catalog/catalog-import.module.js +0 -4
  29. package/dist/catalog/catalog-scope.service.d.ts +63 -26
  30. package/dist/catalog/catalog-scope.service.js +146 -71
  31. package/dist/catalog/catalog.module.d.ts +2 -1
  32. package/dist/catalog/catalog.module.js +9 -4
  33. package/dist/catalog/catalog.service.d.ts +11 -4
  34. package/dist/catalog/catalog.service.js +21 -5
  35. package/dist/catalog/cli.d.ts +4 -3
  36. package/dist/catalog/cli.js +8 -13
  37. package/dist/catalog/job-ids.d.ts +8 -20
  38. package/dist/catalog/job-ids.js +11 -16
  39. package/dist/catalog/marketplaces.service.d.ts +1 -1
  40. package/dist/catalog/org-plugin-uploads.d.ts +11 -4
  41. package/dist/catalog/org-plugin-uploads.js +21 -6
  42. package/dist/catalog/owned-items.d.ts +6 -13
  43. package/dist/catalog/owned-items.js +11 -29
  44. package/dist/catalog/owned-plugin-cleanup.d.ts +8 -17
  45. package/dist/catalog/owned-plugin-cleanup.js +12 -66
  46. package/dist/catalog/platform-components.d.ts +56 -34
  47. package/dist/catalog/platform-components.js +68 -78
  48. package/dist/catalog/platform-import-update.d.ts +50 -0
  49. package/dist/catalog/platform-import-update.js +116 -0
  50. package/dist/catalog/plugin-import.d.ts +64 -26
  51. package/dist/catalog/plugin-import.js +113 -77
  52. package/dist/catalog/plugin-upgrade.d.ts +22 -4
  53. package/dist/catalog/plugin-upgrade.js +26 -19
  54. package/dist/catalog/scope-jobs.d.ts +59 -0
  55. package/dist/catalog/scope-jobs.js +65 -0
  56. package/dist/catalog/scope-sync-workers.d.ts +22 -0
  57. package/dist/catalog/scope-sync-workers.js +58 -2
  58. package/dist/catalog/scope-sync.d.ts +40 -65
  59. package/dist/catalog/scope-sync.js +114 -122
  60. package/dist/channels/channels.module.d.ts +5 -2
  61. package/dist/channels/channels.module.js +9 -2
  62. package/dist/channels/deliver-job.d.ts +25 -0
  63. package/dist/channels/deliver-job.js +25 -0
  64. package/dist/channels/inbound.d.ts +1 -1
  65. package/dist/channels/inbound.js +4 -17
  66. package/dist/channels/jobs.d.ts +28 -18
  67. package/dist/channels/jobs.js +63 -26
  68. package/dist/channels/threads.d.ts +4 -3
  69. package/dist/channels/threads.js +4 -3
  70. package/dist/config/schema.d.ts +0 -3
  71. package/dist/config/schema.js +11 -20
  72. package/dist/conversations/agent-tree.d.ts +16 -0
  73. package/dist/conversations/agent-tree.js +36 -0
  74. package/dist/conversations/conversations.controller.js +4 -2
  75. package/dist/conversations/conversations.service.d.ts +61 -19
  76. package/dist/conversations/conversations.service.js +69 -43
  77. package/dist/conversations/event-log.d.ts +7 -10
  78. package/dist/conversations/event-log.js +24 -15
  79. package/dist/conversations/events.d.ts +21 -0
  80. package/dist/conversations/events.js +26 -2
  81. package/dist/conversations/queue.service.d.ts +8 -3
  82. package/dist/conversations/queue.service.js +37 -13
  83. package/dist/conversations/source.d.ts +10 -0
  84. package/dist/conversations/source.js +17 -0
  85. package/dist/conversations/titles.d.ts +21 -8
  86. package/dist/conversations/titles.js +42 -15
  87. package/dist/conversations/tree.d.ts +10 -0
  88. package/dist/conversations/tree.js +35 -0
  89. package/dist/db/kernel-schema.d.ts +35 -22
  90. package/dist/devices/devices.service.d.ts +23 -11
  91. package/dist/devices/devices.service.js +34 -26
  92. package/dist/errors.d.ts +11 -0
  93. package/dist/errors.js +11 -0
  94. package/dist/evals/run-replay.d.ts +13 -2
  95. package/dist/evals/run-replay.js +42 -9
  96. package/dist/evals/snapshot.d.ts +7 -1
  97. package/dist/evals/snapshot.js +2 -1
  98. package/dist/extensions/extensions.module.js +3 -0
  99. package/dist/extensions/hooks.d.ts +22 -3
  100. package/dist/extensions/hooks.js +15 -1
  101. package/dist/extensions/lifecycle-hooks.d.ts +99 -0
  102. package/dist/extensions/lifecycle-hooks.js +273 -0
  103. package/dist/extensions/lifecycle.d.ts +6 -1
  104. package/dist/extensions/lifecycle.js +9 -2
  105. package/dist/extensions/org-config.d.ts +13 -1
  106. package/dist/extensions/org-config.js +19 -2
  107. package/dist/extensions/permission-view.d.ts +9 -5
  108. package/dist/extensions/permission-view.js +9 -4
  109. package/dist/extensions/point-defaults.d.ts +28 -1
  110. package/dist/extensions/point-defaults.js +44 -13
  111. package/dist/extensions/usage-subscribers.d.ts +43 -0
  112. package/dist/extensions/usage-subscribers.js +108 -0
  113. package/dist/http/api.module.js +2 -0
  114. package/dist/inbox/inbox.service.d.ts +6 -20
  115. package/dist/inbox/inbox.service.js +0 -3
  116. package/dist/inbox/org-notices.d.ts +1 -1
  117. package/dist/inbox/platform-inbox.service.d.ts +3 -32
  118. package/dist/inbox/platform-inbox.service.js +5 -54
  119. package/dist/index.d.ts +1 -0
  120. package/dist/index.js +1 -0
  121. package/dist/infra/cli/kernel-cli.module.js +5 -0
  122. package/dist/infra/events/kernel-events.d.ts +9 -35
  123. package/dist/infra/outbox/outbox-relay.module.d.ts +6 -0
  124. package/dist/infra/outbox/outbox-relay.module.js +18 -0
  125. package/dist/infra/outbox/outbox.dispatcher.d.ts +103 -0
  126. package/dist/infra/outbox/outbox.dispatcher.js +310 -0
  127. package/dist/infra/outbox/outbox.module.d.ts +24 -0
  128. package/dist/infra/outbox/outbox.module.js +50 -0
  129. package/dist/infra/outbox/outbox.relay.d.ts +31 -0
  130. package/dist/infra/outbox/outbox.relay.js +76 -0
  131. package/dist/infra/outbox/outbox.service.d.ts +51 -0
  132. package/dist/infra/outbox/outbox.service.js +131 -0
  133. package/dist/infra/outbox/registry.d.ts +244 -0
  134. package/dist/infra/outbox/registry.js +234 -0
  135. package/dist/infra/queue/queue-names.d.ts +8 -0
  136. package/dist/infra/queue/queue-names.js +17 -0
  137. package/dist/kernel.module.js +3 -0
  138. package/dist/local-devices/device-policy.service.d.ts +4 -0
  139. package/dist/local-devices/device-policy.service.js +61 -3
  140. package/dist/local-devices/machines.d.ts +13 -5
  141. package/dist/local-devices/machines.js +45 -7
  142. package/dist/local-devices/policy-summary.d.ts +44 -1
  143. package/dist/local-devices/policy-summary.js +69 -0
  144. package/dist/models/embeddings.d.ts +9 -9
  145. package/dist/models/embeddings.js +11 -18
  146. package/dist/models/models.module.js +3 -0
  147. package/dist/orgs/directory-sync.js +1 -1
  148. package/dist/orgs/members.service.js +1 -1
  149. package/dist/orgs/offboarding.d.ts +50 -57
  150. package/dist/orgs/offboarding.js +117 -162
  151. package/dist/orgs/orgs.module.js +3 -0
  152. package/dist/orgs/orgs.service.d.ts +11 -5
  153. package/dist/orgs/orgs.service.js +22 -6
  154. package/dist/overrides.d.ts +23 -3
  155. package/dist/overrides.js +12 -2
  156. package/dist/platform/exclusions.js +5 -0
  157. package/dist/platform/ops/admin.ops.js +18 -2
  158. package/dist/platform/ops/catalog-admin.ops.d.ts +3 -1
  159. package/dist/platform/ops/catalog-admin.ops.js +32 -4
  160. package/dist/platform/ops/inbox.ops.js +4 -20
  161. package/dist/platform/ops/outbox.ops.d.ts +21 -0
  162. package/dist/platform/ops/outbox.ops.js +125 -0
  163. package/dist/platform/ops/platform.ops.d.ts +1 -0
  164. package/dist/platform/ops/platform.ops.js +6 -3
  165. package/dist/platform/ops/schedules.ops.js +40 -1
  166. package/dist/platform/platform-ops.module.d.ts +2 -1
  167. package/dist/platform/platform-ops.module.js +3 -0
  168. package/dist/platform/route-schemas.js +3 -0
  169. package/dist/roles/worker-role.d.ts +10 -5
  170. package/dist/roles/worker-role.js +19 -9
  171. package/dist/roles/worker.module.js +11 -8
  172. package/dist/runtime/context/build.d.ts +3 -0
  173. package/dist/runtime/context/build.js +15 -0
  174. package/dist/runtime/context/plugin-context.d.ts +20 -0
  175. package/dist/runtime/context/plugin-context.js +30 -0
  176. package/dist/runtime/langgraph-loop.js +17 -5
  177. package/dist/runtime/limits.d.ts +15 -0
  178. package/dist/runtime/limits.js +25 -0
  179. package/dist/runtime/loop.d.ts +16 -3
  180. package/dist/runtime/loop.js +2 -1
  181. package/dist/runtime/maintenance.d.ts +7 -3
  182. package/dist/runtime/maintenance.js +52 -12
  183. package/dist/runtime/model-call.d.ts +20 -17
  184. package/dist/runtime/model-call.js +15 -24
  185. package/dist/runtime/plugin-context.service.d.ts +69 -0
  186. package/dist/runtime/plugin-context.service.js +208 -0
  187. package/dist/runtime/queue-lost-jobs.d.ts +24 -0
  188. package/dist/runtime/queue-lost-jobs.js +51 -0
  189. package/dist/runtime/run-control.d.ts +47 -5
  190. package/dist/runtime/run-control.js +79 -12
  191. package/dist/runtime/run-finish.d.ts +39 -12
  192. package/dist/runtime/run-finish.js +97 -14
  193. package/dist/runtime/run-jobs.d.ts +95 -13
  194. package/dist/runtime/run-jobs.js +199 -28
  195. package/dist/runtime/run-orphan-check.job.d.ts +142 -0
  196. package/dist/runtime/run-orphan-check.job.js +339 -0
  197. package/dist/runtime/run-processors.d.ts +16 -9
  198. package/dist/runtime/run-processors.js +1 -1
  199. package/dist/runtime/run-queue.d.ts +75 -53
  200. package/dist/runtime/run-queue.js +189 -263
  201. package/dist/runtime/run-steps.d.ts +1 -2
  202. package/dist/runtime/run-steps.js +8 -11
  203. package/dist/runtime/run-worker.d.ts +26 -16
  204. package/dist/runtime/run-worker.js +132 -37
  205. package/dist/runtime/runtime.module.d.ts +2 -3
  206. package/dist/runtime/runtime.module.js +7 -6
  207. package/dist/runtime/step-log.d.ts +16 -4
  208. package/dist/runtime/step-log.js +26 -6
  209. package/dist/runtime/stream.controller.d.ts +2 -4
  210. package/dist/runtime/stream.controller.js +5 -11
  211. package/dist/runtime/subagents/child.js +1 -1
  212. package/dist/runtime/subagents/definition.d.ts +2 -1
  213. package/dist/runtime/subagents/definition.js +4 -2
  214. package/dist/runtime/subagents/deliveries.d.ts +6 -3
  215. package/dist/runtime/subagents/deliveries.js +16 -7
  216. package/dist/runtime/subagents/dispatch.d.ts +12 -12
  217. package/dist/runtime/subagents/dispatch.js +14 -38
  218. package/dist/runtime/subagents/messages.d.ts +5 -1
  219. package/dist/runtime/subagents/messages.js +11 -3
  220. package/dist/runtime/subagents/skill-fork.d.ts +3 -3
  221. package/dist/runtime/subagents/skill-fork.js +8 -5
  222. package/dist/runtime/subagents/subagents.service.js +1 -1
  223. package/dist/runtime/subagents/tree-jobs.d.ts +61 -0
  224. package/dist/runtime/subagents/tree-jobs.js +86 -0
  225. package/dist/runtime/usage-report-jobs.d.ts +23 -48
  226. package/dist/runtime/usage-report-jobs.js +67 -119
  227. package/dist/runtime/usage-report.d.ts +28 -77
  228. package/dist/runtime/usage-report.js +35 -213
  229. package/dist/runtime/waits.d.ts +48 -198
  230. package/dist/runtime/waits.js +103 -805
  231. package/dist/sandbox/jobs.d.ts +20 -10
  232. package/dist/sandbox/jobs.js +23 -17
  233. package/dist/sandbox/sandbox.service.d.ts +17 -4
  234. package/dist/sandbox/sandbox.service.js +55 -14
  235. package/dist/schedules/calendar.d.ts +134 -0
  236. package/dist/schedules/calendar.js +410 -0
  237. package/dist/schedules/fire.d.ts +26 -7
  238. package/dist/schedules/fire.js +91 -22
  239. package/dist/schedules/job-ids.d.ts +66 -17
  240. package/dist/schedules/job-ids.js +81 -13
  241. package/dist/schedules/run-tracker.d.ts +0 -5
  242. package/dist/schedules/run-tracker.js +2 -14
  243. package/dist/schedules/scan.job.d.ts +13 -32
  244. package/dist/schedules/scan.job.js +50 -164
  245. package/dist/schedules/schedule-math.d.ts +42 -0
  246. package/dist/schedules/schedule-math.js +52 -0
  247. package/dist/schedules/scheduled-tasks.service.d.ts +22 -6
  248. package/dist/schedules/scheduled-tasks.service.js +70 -14
  249. package/dist/schedules/schedules.controller.d.ts +5 -1
  250. package/dist/schedules/schedules.controller.js +15 -1
  251. package/dist/schedules/schedules.module.js +0 -4
  252. package/dist/schedules/schedules.schemas.d.ts +35 -0
  253. package/dist/schedules/schedules.schemas.js +27 -0
  254. package/dist/schedules/store.d.ts +5 -0
  255. package/dist/schedules/store.js +20 -0
  256. package/dist/skills/changes.controller.d.ts +1 -1
  257. package/dist/skills/changes.controller.js +1 -1
  258. package/dist/tools/approvals.service.d.ts +41 -11
  259. package/dist/tools/approvals.service.js +108 -14
  260. package/dist/tools/builtin-names.d.ts +5 -3
  261. package/dist/tools/builtin-names.js +12 -4
  262. package/dist/tools/builtins/builtin-tools.d.ts +1 -2
  263. package/dist/tools/builtins/builtin-tools.js +21 -17
  264. package/dist/tools/builtins/sandbox/bash.js +9 -3
  265. package/dist/tools/builtins/sandbox/common.d.ts +18 -1
  266. package/dist/tools/builtins/sandbox/common.js +14 -1
  267. package/dist/tools/builtins/sandbox/index.d.ts +2 -1
  268. package/dist/tools/builtins/sandbox/index.js +4 -1
  269. package/dist/tools/builtins/sandbox/task-list.d.ts +12 -0
  270. package/dist/tools/builtins/sandbox/task-list.js +130 -0
  271. package/dist/tools/builtins/sandbox/task-output.d.ts +8 -1
  272. package/dist/tools/builtins/sandbox/task-output.js +21 -3
  273. package/dist/tools/builtins/web/extract.d.ts +2 -4
  274. package/dist/tools/builtins/web/extract.js +2 -5
  275. package/dist/tools/pipeline.d.ts +12 -3
  276. package/dist/tools/pipeline.js +6 -1
  277. package/dist/tools/questions.service.d.ts +30 -10
  278. package/dist/tools/questions.service.js +41 -27
  279. package/dist/tools/registry.d.ts +12 -3
  280. package/dist/tools/registry.js +13 -3
  281. package/dist/tools/tools.module.js +4 -3
  282. package/package.json +3 -3
  283. package/src/migrations/0118_device_policy_ops.sql +6 -0
  284. package/src/migrations/0119_outbox.sql +22 -0
  285. package/src/migrations/0120_drop_usage_report_sweep.sql +7 -0
  286. package/src/migrations/0121_drop_platform_migration_runs.sql +12 -0
  287. package/src/migrations/0122_drop_scheduled_task_runs_rechecked_at.sql +5 -0
  288. package/src/migrations/0123_pgvector.sql +27 -0
  289. package/src/migrations/0124_conversation_source.sql +36 -0
  290. package/dist/catalog/platform-migration.d.ts +0 -115
  291. package/dist/catalog/platform-migration.js +0 -583
  292. package/dist/infra/queue/add-job-once.d.ts +0 -14
  293. package/dist/infra/queue/add-job-once.js +0 -16
@@ -0,0 +1,103 @@
1
+ import { ModuleRef } from "@nestjs/core";
2
+ import type { Queue } from "bullmq";
3
+ import type { KernelLogger } from "../../log.js";
4
+ import { KernelDb } from "../database/kernel-db.js";
5
+ import { TransactionHost } from "../database/transaction-host.js";
6
+ import { type KernelQueueName } from "../queue/queue-names.js";
7
+ /**
8
+ * Retry schedule of a row whose add failed (spec §3.4): the next attempt waits
9
+ * `min(capMs, baseMs * 2^attempts)`, and the row is dead (kept, never retried) at `maxAttempts`.
10
+ */
11
+ export declare const OUTBOX_BACKOFF: {
12
+ readonly baseMs: 1000;
13
+ readonly capMs: 300000;
14
+ readonly maxAttempts: 20;
15
+ };
16
+ /**
17
+ * How long the queue adds of one claim pass may take in all: the claim transaction holds its rows'
18
+ * locks and a pool connection for at most about this long.
19
+ */
20
+ export declare const OUTBOX_REDIS_TIMEOUT_MS = 2000;
21
+ /**
22
+ * Most fast-path passes running at once in one process; more are skipped and left to the relay,
23
+ * so a slow Redis cannot take the whole pool (a local resource bound, no shared state).
24
+ */
25
+ export declare const OUTBOX_FAST_PATH_CONCURRENCY = 4;
26
+ /**
27
+ * Test seam (`KernelOverride`): returns true to make the adds to the given queue fail (a Redis
28
+ * outage) without calling Redis. The kernel never provides it; the dispatcher defaults to "never
29
+ * fails".
30
+ */
31
+ export declare const OUTBOX_DISPATCH_FAILS: unique symbol;
32
+ /** What one pass did: rows delivered, rows whose add failed, and of those the rows now dead. */
33
+ export interface DispatchResult {
34
+ sent: number;
35
+ failed: number;
36
+ dead: number;
37
+ }
38
+ /** The queue handle the dispatcher uses: adds, and the connection's state. */
39
+ export type OutboxQueue = Pick<Queue, "addBulk" | "getBackend">;
40
+ /**
41
+ * Delivers outbox rows to BullMQ (transactional outbox spec §3.3, §3.4). A pass claims rows
42
+ * `FOR UPDATE SKIP LOCKED` (so concurrent passes never take the same row), adds their jobs per
43
+ * queue with `addBulk` (the queue defaults overlaid by the registry's options of the job name, the
44
+ * row's job id, a future `run_at` as the remaining `delay`), and deletes
45
+ * the delivered rows in the same short transaction. A crash after the add and before the commit
46
+ * leaves the rows; the next pass adds again and BullMQ ignores the existing job id. A queue whose
47
+ * add fails keeps its rows, and a second short update records the attempt with exponential
48
+ * backoff, setting `dead_at` at the last one.
49
+ *
50
+ * The fast path never queues behind an outage: it skips the pass (the relay delivers the rows) when
51
+ * a queue's Redis connection is not ready or {@link OUTBOX_FAST_PATH_CONCURRENCY} passes already
52
+ * run. Within a pass, the first failed queue fails the queues after it without trying them.
53
+ *
54
+ * The queue handles are the instances the queues' owner modules registered (one per queue, no
55
+ * connection of the outbox's own), resolved by token at use time. A queue whose owner this
56
+ * process's roles do not load is not reachable here: no pass claims its rows, which stay for a
57
+ * process that has it.
58
+ */
59
+ export declare class OutboxDispatcher {
60
+ readonly txHost: TransactionHost;
61
+ private readonly db;
62
+ private readonly log;
63
+ private readonly moduleRef;
64
+ private readonly dispatchFails;
65
+ /** The owners' queue instances resolved so far (a per-process handle cache). */
66
+ private readonly queues;
67
+ /** Fast-path passes running in this process (bounded by OUTBOX_FAST_PATH_CONCURRENCY). */
68
+ private fastPaths;
69
+ constructor(txHost: TransactionHost, db: KernelDb, log: KernelLogger, moduleRef: Pick<ModuleRef, "get">, dispatchFails?: (queue: KernelQueueName) => boolean);
70
+ /** The queue instance its owner module registered, or undefined when no loaded module has it. */
71
+ private queue;
72
+ /** The kernel queues this process can add to, with their handles. */
73
+ private reachable;
74
+ /**
75
+ * The fast path: delivers the given rows (a committed transaction's own) right away. Never
76
+ * throws: whatever it does not deliver stays for the relay, and a failure is logged.
77
+ */
78
+ dispatchOwn(ids: readonly string[]): Promise<void>;
79
+ /**
80
+ * Every reachable queue's connection is ready (they reach the same Redis; the fast path does not
81
+ * read its rows' queues before claiming them).
82
+ */
83
+ private redisReady;
84
+ /**
85
+ * The relay: delivers up to `limit` due rows (not dead, `next_attempt_at` reached), oldest
86
+ * first.
87
+ */
88
+ dispatchDue(limit: number): Promise<DispatchResult>;
89
+ private dispatch;
90
+ /**
91
+ * One claim transaction: locks the rows (skipping rows another pass holds), adds their jobs and
92
+ * deletes the delivered rows. The only Redis calls made while a PG transaction is open in the
93
+ * kernel (spec §3.3): the locks held are outbox rows only, and the pass's adds together are
94
+ * bounded by {@link OUTBOX_REDIS_TIMEOUT_MS}.
95
+ */
96
+ private claimAndSend;
97
+ /**
98
+ * Records a failed add on its rows (after the claim transaction, so the attempt is kept whatever
99
+ * that transaction did): one more attempt, the error, the next attempt by {@link OUTBOX_BACKOFF},
100
+ * and `dead_at` at the last attempt. Returns how many rows it made dead.
101
+ */
102
+ private recordFailure;
103
+ }
@@ -0,0 +1,310 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { getQueueToken } from "@nestjs/bullmq";
11
+ import { Inject, Injectable, Optional } from "@nestjs/common";
12
+ import { ModuleRef } from "@nestjs/core";
13
+ import { sql } from "kysely";
14
+ import { KernelDb } from "../database/kernel-db.js";
15
+ import { TransactionHost } from "../database/transaction-host.js";
16
+ import { Transactional } from "../database/transactional.decorator.js";
17
+ import { QUEUE_JOB_DEFAULTS } from "../queue/queue.module.js";
18
+ import { KERNEL_QUEUES } from "../queue/queue-names.js";
19
+ import { KERNEL_LOGGER } from "../tokens.js";
20
+ import { outboxJobOptions } from "./registry.js";
21
+ /**
22
+ * Retry schedule of a row whose add failed (spec §3.4): the next attempt waits
23
+ * `min(capMs, baseMs * 2^attempts)`, and the row is dead (kept, never retried) at `maxAttempts`.
24
+ */
25
+ export const OUTBOX_BACKOFF = { baseMs: 1000, capMs: 300_000, maxAttempts: 20 };
26
+ /**
27
+ * How long the queue adds of one claim pass may take in all: the claim transaction holds its rows'
28
+ * locks and a pool connection for at most about this long.
29
+ */
30
+ export const OUTBOX_REDIS_TIMEOUT_MS = 2000;
31
+ /**
32
+ * Most fast-path passes running at once in one process; more are skipped and left to the relay,
33
+ * so a slow Redis cannot take the whole pool (a local resource bound, no shared state).
34
+ */
35
+ export const OUTBOX_FAST_PATH_CONCURRENCY = 4;
36
+ /**
37
+ * Test seam (`KernelOverride`): returns true to make the adds to the given queue fail (a Redis
38
+ * outage) without calling Redis. The kernel never provides it; the dispatcher defaults to "never
39
+ * fails".
40
+ */
41
+ export const OUTBOX_DISPATCH_FAILS = Symbol("OUTBOX_DISPATCH_FAILS");
42
+ /** Longest `last_error` kept on a row. */
43
+ const MAX_ERROR_LENGTH = 2000;
44
+ const errMsg = (e) => (e instanceof Error ? e.message : String(e));
45
+ const NOT_READY = Symbol("NOT_READY");
46
+ /**
47
+ * Whether the queue's Redis connection is ready now, without waiting: the backend's client
48
+ * promise only settles once connected, so a pending one counts as not ready.
49
+ */
50
+ async function connectionReady(queue) {
51
+ try {
52
+ // an already settled client promise wins the race (its callback is queued first)
53
+ const client = await Promise.race([queue.getBackend().client, Promise.resolve(NOT_READY)]);
54
+ return client !== NOT_READY && client.status === "ready";
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ /** Rejects when `p` takes longer than `ms`; a late rejection of `p` is swallowed. */
61
+ async function withTimeout(p, ms, what) {
62
+ p.catch(() => { });
63
+ let timer;
64
+ const timeout = new Promise((_, reject) => {
65
+ timer = setTimeout(() => reject(new Error(`${what} timed out after ${ms} ms`)), ms);
66
+ });
67
+ try {
68
+ return await Promise.race([p, timeout]);
69
+ }
70
+ finally {
71
+ clearTimeout(timer);
72
+ }
73
+ }
74
+ /**
75
+ * Delivers outbox rows to BullMQ (transactional outbox spec §3.3, §3.4). A pass claims rows
76
+ * `FOR UPDATE SKIP LOCKED` (so concurrent passes never take the same row), adds their jobs per
77
+ * queue with `addBulk` (the queue defaults overlaid by the registry's options of the job name, the
78
+ * row's job id, a future `run_at` as the remaining `delay`), and deletes
79
+ * the delivered rows in the same short transaction. A crash after the add and before the commit
80
+ * leaves the rows; the next pass adds again and BullMQ ignores the existing job id. A queue whose
81
+ * add fails keeps its rows, and a second short update records the attempt with exponential
82
+ * backoff, setting `dead_at` at the last one.
83
+ *
84
+ * The fast path never queues behind an outage: it skips the pass (the relay delivers the rows) when
85
+ * a queue's Redis connection is not ready or {@link OUTBOX_FAST_PATH_CONCURRENCY} passes already
86
+ * run. Within a pass, the first failed queue fails the queues after it without trying them.
87
+ *
88
+ * The queue handles are the instances the queues' owner modules registered (one per queue, no
89
+ * connection of the outbox's own), resolved by token at use time. A queue whose owner this
90
+ * process's roles do not load is not reachable here: no pass claims its rows, which stay for a
91
+ * process that has it.
92
+ */
93
+ let OutboxDispatcher = class OutboxDispatcher {
94
+ txHost;
95
+ db;
96
+ log;
97
+ moduleRef;
98
+ dispatchFails;
99
+ /** The owners' queue instances resolved so far (a per-process handle cache). */
100
+ queues = new Map();
101
+ /** Fast-path passes running in this process (bounded by OUTBOX_FAST_PATH_CONCURRENCY). */
102
+ fastPaths = 0;
103
+ constructor(txHost, db, log, moduleRef, dispatchFails = () => false) {
104
+ this.txHost = txHost;
105
+ this.db = db;
106
+ this.log = log;
107
+ this.moduleRef = moduleRef;
108
+ this.dispatchFails = dispatchFails;
109
+ }
110
+ /** The queue instance its owner module registered, or undefined when no loaded module has it. */
111
+ queue(name) {
112
+ const known = this.queues.get(name);
113
+ if (known)
114
+ return known;
115
+ let queue;
116
+ try {
117
+ queue = this.moduleRef.get(getQueueToken(name), { strict: false });
118
+ }
119
+ catch {
120
+ return undefined;
121
+ }
122
+ if (queue)
123
+ this.queues.set(name, queue);
124
+ return queue;
125
+ }
126
+ /** The kernel queues this process can add to, with their handles. */
127
+ reachable() {
128
+ const found = [];
129
+ for (const name of KERNEL_QUEUES) {
130
+ const queue = this.queue(name);
131
+ if (queue)
132
+ found.push([name, queue]);
133
+ }
134
+ return found;
135
+ }
136
+ /**
137
+ * The fast path: delivers the given rows (a committed transaction's own) right away. Never
138
+ * throws: whatever it does not deliver stays for the relay, and a failure is logged.
139
+ */
140
+ async dispatchOwn(ids) {
141
+ if (ids.length === 0)
142
+ return;
143
+ if (this.fastPaths >= OUTBOX_FAST_PATH_CONCURRENCY) {
144
+ this.log.debug("outbox fast path busy; the relay delivers the rows", { rows: ids.length });
145
+ return;
146
+ }
147
+ this.fastPaths++;
148
+ try {
149
+ if (!(await this.redisReady())) {
150
+ this.log.debug("outbox fast path: Redis not ready; the relay delivers the rows", {
151
+ rows: ids.length,
152
+ });
153
+ return;
154
+ }
155
+ await this.dispatch({ ids });
156
+ }
157
+ catch (e) {
158
+ this.log.error("outbox fast path failed; the relay delivers the rows", {
159
+ rows: ids.length,
160
+ error: errMsg(e),
161
+ });
162
+ }
163
+ finally {
164
+ this.fastPaths--;
165
+ }
166
+ }
167
+ /**
168
+ * Every reachable queue's connection is ready (they reach the same Redis; the fast path does not
169
+ * read its rows' queues before claiming them).
170
+ */
171
+ async redisReady() {
172
+ const ready = await Promise.all(this.reachable().map(([, q]) => connectionReady(q)));
173
+ return ready.every(Boolean);
174
+ }
175
+ /**
176
+ * The relay: delivers up to `limit` due rows (not dead, `next_attempt_at` reached), oldest
177
+ * first.
178
+ */
179
+ dispatchDue(limit) {
180
+ return this.dispatch({ limit });
181
+ }
182
+ async dispatch(claim) {
183
+ const { sent, failed } = await this.claimAndSend(claim);
184
+ let dead = 0;
185
+ for (const group of failed)
186
+ dead += await this.recordFailure(group);
187
+ return { sent, failed: failed.reduce((n, g) => n + g.ids.length, 0), dead };
188
+ }
189
+ /**
190
+ * One claim transaction: locks the rows (skipping rows another pass holds), adds their jobs and
191
+ * deletes the delivered rows. The only Redis calls made while a PG transaction is open in the
192
+ * kernel (spec §3.3): the locks held are outbox rows only, and the pass's adds together are
193
+ * bounded by {@link OUTBOX_REDIS_TIMEOUT_MS}.
194
+ */
195
+ async claimAndSend(claim) {
196
+ const queues = new Map(this.reachable());
197
+ if (queues.size === 0)
198
+ return { sent: 0, failed: [] };
199
+ // rows of a queue this process cannot reach are left for a process that can
200
+ let query = this.db
201
+ .selectFrom("outbox")
202
+ .select(["id", "queue", "job_name", "job_id", "data", "run_at"])
203
+ .where("dead_at", "is", null)
204
+ .where("queue", "in", [...queues.keys()]);
205
+ query =
206
+ "ids" in claim
207
+ ? query.where("id", "in", [...claim.ids])
208
+ : query.where("next_attempt_at", "<=", sql `now()`).limit(claim.limit);
209
+ const rows = await query.orderBy("id").forUpdate().skipLocked().execute();
210
+ if (rows.length === 0)
211
+ return { sent: 0, failed: [] };
212
+ const byQueue = new Map();
213
+ for (const row of rows) {
214
+ const group = byQueue.get(row.queue);
215
+ if (group)
216
+ group.push(row);
217
+ else
218
+ byQueue.set(row.queue, [row]);
219
+ }
220
+ const now = Date.now();
221
+ const deadline = now + OUTBOX_REDIS_TIMEOUT_MS;
222
+ const delivered = [];
223
+ const failed = [];
224
+ // after the first failure the queues left are failed without a try: Redis is likely down
225
+ let firstError;
226
+ for (const [name, group] of byQueue) {
227
+ const ids = group.map((r) => r.id);
228
+ if (firstError !== undefined) {
229
+ failed.push({
230
+ ids,
231
+ error: `not tried after an earlier queue failed: ${firstError}`.slice(0, MAX_ERROR_LENGTH),
232
+ });
233
+ continue;
234
+ }
235
+ try {
236
+ if (this.dispatchFails(name))
237
+ throw new Error("outbox dispatch failure (test seam)");
238
+ const queue = queues.get(name);
239
+ if (!queue)
240
+ throw new Error(`"${name}" is not a kernel queue`);
241
+ const left = deadline - Date.now();
242
+ if (left <= 0)
243
+ throw new Error(`no time left of the pass's ${OUTBOX_REDIS_TIMEOUT_MS} ms`);
244
+ await withTimeout(queue.addBulk(group.map((r) => ({
245
+ name: r.job_name,
246
+ data: r.data,
247
+ opts: {
248
+ ...QUEUE_JOB_DEFAULTS,
249
+ ...outboxJobOptions(r.job_name),
250
+ jobId: r.job_id,
251
+ delay: r.run_at ? Math.max(0, r.run_at.getTime() - now) : 0,
252
+ },
253
+ }))), left, `addBulk to ${name}`);
254
+ delivered.push(...ids);
255
+ }
256
+ catch (e) {
257
+ firstError = errMsg(e);
258
+ failed.push({ ids, error: firstError.slice(0, MAX_ERROR_LENGTH) });
259
+ }
260
+ }
261
+ if (delivered.length > 0)
262
+ await this.db.deleteFrom("outbox").where("id", "in", delivered).execute();
263
+ return { sent: delivered.length, failed };
264
+ }
265
+ /**
266
+ * Records a failed add on its rows (after the claim transaction, so the attempt is kept whatever
267
+ * that transaction did): one more attempt, the error, the next attempt by {@link OUTBOX_BACKOFF},
268
+ * and `dead_at` at the last attempt. Returns how many rows it made dead.
269
+ */
270
+ async recordFailure(group) {
271
+ const { baseMs, capMs, maxAttempts } = OUTBOX_BACKOFF;
272
+ // the right-hand sides read the row before the update: the first failure waits baseMs
273
+ const backoffMs = sql `least(${capMs}::float8, ${baseMs}::float8 * power(2, attempts))`;
274
+ const updated = await this.db
275
+ .updateTable("outbox")
276
+ .set({
277
+ attempts: sql `attempts + 1`,
278
+ last_error: group.error,
279
+ next_attempt_at: sql `now() + ${backoffMs} * interval '1 millisecond'`,
280
+ dead_at: sql `case when attempts + 1 >= ${maxAttempts}::int then now() end`,
281
+ })
282
+ .where("id", "in", group.ids)
283
+ .where("dead_at", "is", null)
284
+ .returning(["dead_at"])
285
+ .execute();
286
+ const dead = updated.filter((r) => r.dead_at !== null).length;
287
+ this.log.warn("outbox delivery failed; retrying with backoff", {
288
+ rows: group.ids.length,
289
+ dead,
290
+ error: group.error,
291
+ });
292
+ if (dead > 0) {
293
+ this.log.error("outbox rows dead after the last attempt", { rows: dead, error: group.error });
294
+ }
295
+ return dead;
296
+ }
297
+ };
298
+ __decorate([
299
+ Transactional({ propagation: "requires_new" })
300
+ ], OutboxDispatcher.prototype, "claimAndSend", null);
301
+ OutboxDispatcher = __decorate([
302
+ Injectable(),
303
+ __param(0, Inject(TransactionHost)),
304
+ __param(1, Inject(KernelDb)),
305
+ __param(2, Inject(KERNEL_LOGGER)),
306
+ __param(3, Inject(ModuleRef)),
307
+ __param(4, Optional()),
308
+ __param(4, Inject(OUTBOX_DISPATCH_FAILS))
309
+ ], OutboxDispatcher);
310
+ export { OutboxDispatcher };
@@ -0,0 +1,24 @@
1
+ import { OutboxDispatcher } from "./outbox.dispatcher.js";
2
+ import { type OutboxFastPath } from "./outbox.service.js";
3
+ /**
4
+ * Test seam (`KernelOverride`): receives each detached fast-path pass, so a test can await what a
5
+ * request started after its commit (the kernel never provides it).
6
+ */
7
+ export declare const OUTBOX_FAST_PATH_SETTLED: unique symbol;
8
+ /**
9
+ * The fast path (spec §3.3): starts the dispatcher on a committed transaction's rows and returns
10
+ * at once, so the commit's caller never waits on Redis (R9). `dispatchOwn` never throws; what it
11
+ * does not deliver stays for the relay.
12
+ */
13
+ export declare const outboxFastPath: (dispatcher: Pick<OutboxDispatcher, "dispatchOwn">, settled?: (pass: Promise<void>) => void) => OutboxFastPath;
14
+ /**
15
+ * Global: `OutboxService` (owed work written in the caller's transaction, transactional outbox
16
+ * spec §3.2), the `OutboxDispatcher` that delivers the rows (the relay drives it too) and the fast
17
+ * path `add` hands each committed transaction's rows to.
18
+ *
19
+ * It registers no queue: the dispatcher adds through the instances the queues' owner modules
20
+ * registered (one instance and connection per queue, as everywhere in the kernel), resolved at use
21
+ * time, and leaves the rows of a queue whose owner this process does not load to one that does.
22
+ */
23
+ export declare class OutboxModule {
24
+ }
@@ -0,0 +1,50 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Global, Module } from "@nestjs/common";
8
+ import { OutboxDispatcher } from "./outbox.dispatcher.js";
9
+ import { OUTBOX_FAST_PATH, OutboxService } from "./outbox.service.js";
10
+ /**
11
+ * Test seam (`KernelOverride`): receives each detached fast-path pass, so a test can await what a
12
+ * request started after its commit (the kernel never provides it).
13
+ */
14
+ export const OUTBOX_FAST_PATH_SETTLED = Symbol("OUTBOX_FAST_PATH_SETTLED");
15
+ /**
16
+ * The fast path (spec §3.3): starts the dispatcher on a committed transaction's rows and returns
17
+ * at once, so the commit's caller never waits on Redis (R9). `dispatchOwn` never throws; what it
18
+ * does not deliver stays for the relay.
19
+ */
20
+ export const outboxFastPath = (dispatcher, settled) => async (ids) => {
21
+ const pass = dispatcher.dispatchOwn(ids);
22
+ settled?.(pass);
23
+ };
24
+ /**
25
+ * Global: `OutboxService` (owed work written in the caller's transaction, transactional outbox
26
+ * spec §3.2), the `OutboxDispatcher` that delivers the rows (the relay drives it too) and the fast
27
+ * path `add` hands each committed transaction's rows to.
28
+ *
29
+ * It registers no queue: the dispatcher adds through the instances the queues' owner modules
30
+ * registered (one instance and connection per queue, as everywhere in the kernel), resolved at use
31
+ * time, and leaves the rows of a queue whose owner this process does not load to one that does.
32
+ */
33
+ let OutboxModule = class OutboxModule {
34
+ };
35
+ OutboxModule = __decorate([
36
+ Global(),
37
+ Module({
38
+ providers: [
39
+ OutboxService,
40
+ OutboxDispatcher,
41
+ {
42
+ provide: OUTBOX_FAST_PATH,
43
+ inject: [OutboxDispatcher, { token: OUTBOX_FAST_PATH_SETTLED, optional: true }],
44
+ useFactory: (dispatcher, settled) => outboxFastPath(dispatcher, settled ?? undefined),
45
+ },
46
+ ],
47
+ exports: [OutboxService, OutboxDispatcher, OUTBOX_FAST_PATH],
48
+ })
49
+ ], OutboxModule);
50
+ export { OutboxModule };
@@ -0,0 +1,31 @@
1
+ import type { KernelLogger } from "../../log.js";
2
+ import { KernelConfigService } from "../config/kernel-config.service.js";
3
+ import { LockService } from "../lock/lock.service.js";
4
+ import { OutboxDispatcher } from "./outbox.dispatcher.js";
5
+ /** The relay's `@LockedInterval` job name (its run lock key is the default `cron:outbox-relay`). */
6
+ export declare const OUTBOX_RELAY_JOB = "outbox-relay";
7
+ /** The relay's period: a fixed constant, not a config field. */
8
+ export declare const OUTBOX_RELAY_PERIOD_MS = 2000;
9
+ /** Rows one claim pass of the relay takes. */
10
+ export declare const OUTBOX_RELAY_BATCH = 500;
11
+ /** Most claim passes in one tick; what is left waits for the next tick. */
12
+ export declare const OUTBOX_RELAY_MAX_ROUNDS = 10;
13
+ /**
14
+ * The outbox relay (transactional outbox spec §3.3), on the worker role only: once per tick across
15
+ * replicas it delivers the due rows the fast path did not (a failed or skipped pass, a process that
16
+ * died after the commit, a retry whose backoff ran out). A tick runs passes of
17
+ * {@link OUTBOX_RELAY_BATCH} rows until one handles fewer (the backlog is drained) or
18
+ * {@link OUTBOX_RELAY_MAX_ROUNDS} have run. It also stops early when the run is aborted, and after
19
+ * a pass that delivered nothing but failed rows (Redis trouble: the next tick tries again, rather
20
+ * than spinning more passes that each hold a pool connection).
21
+ */
22
+ export declare class OutboxRelay {
23
+ private readonly dispatcher;
24
+ readonly config: KernelConfigService;
25
+ readonly lockService: LockService;
26
+ readonly log: KernelLogger;
27
+ constructor(dispatcher: Pick<OutboxDispatcher, "dispatchDue">, config: KernelConfigService, lockService: LockService, log: KernelLogger);
28
+ relay(signal?: AbortSignal): Promise<void>;
29
+ /** One tick's passes (the body of {@link relay}); returns the rows delivered. */
30
+ relayOnce(signal?: AbortSignal): Promise<number>;
31
+ }
@@ -0,0 +1,76 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { Inject, Injectable } from "@nestjs/common";
11
+ import { KernelConfigService } from "../config/kernel-config.service.js";
12
+ import { LockService } from "../lock/lock.service.js";
13
+ import { LockedInterval } from "../schedule/locked-interval.js";
14
+ import { KERNEL_LOGGER } from "../tokens.js";
15
+ import { OutboxDispatcher } from "./outbox.dispatcher.js";
16
+ /** The relay's `@LockedInterval` job name (its run lock key is the default `cron:outbox-relay`). */
17
+ export const OUTBOX_RELAY_JOB = "outbox-relay";
18
+ /** The relay's period: a fixed constant, not a config field. */
19
+ export const OUTBOX_RELAY_PERIOD_MS = 2000;
20
+ /** Rows one claim pass of the relay takes. */
21
+ export const OUTBOX_RELAY_BATCH = 500;
22
+ /** Most claim passes in one tick; what is left waits for the next tick. */
23
+ export const OUTBOX_RELAY_MAX_ROUNDS = 10;
24
+ /**
25
+ * The outbox relay (transactional outbox spec §3.3), on the worker role only: once per tick across
26
+ * replicas it delivers the due rows the fast path did not (a failed or skipped pass, a process that
27
+ * died after the commit, a retry whose backoff ran out). A tick runs passes of
28
+ * {@link OUTBOX_RELAY_BATCH} rows until one handles fewer (the backlog is drained) or
29
+ * {@link OUTBOX_RELAY_MAX_ROUNDS} have run. It also stops early when the run is aborted, and after
30
+ * a pass that delivered nothing but failed rows (Redis trouble: the next tick tries again, rather
31
+ * than spinning more passes that each hold a pool connection).
32
+ */
33
+ let OutboxRelay = class OutboxRelay {
34
+ dispatcher;
35
+ config;
36
+ lockService;
37
+ log;
38
+ constructor(dispatcher, config, lockService, log) {
39
+ this.dispatcher = dispatcher;
40
+ this.config = config;
41
+ this.lockService = lockService;
42
+ this.log = log;
43
+ }
44
+ async relay(signal) {
45
+ await this.relayOnce(signal);
46
+ }
47
+ /** One tick's passes (the body of {@link relay}); returns the rows delivered. */
48
+ async relayOnce(signal) {
49
+ let sent = 0;
50
+ for (let round = 0; round < OUTBOX_RELAY_MAX_ROUNDS; round++) {
51
+ if (signal?.aborted)
52
+ break;
53
+ const r = await this.dispatcher.dispatchDue(OUTBOX_RELAY_BATCH);
54
+ sent += r.sent;
55
+ // rows handled: `dead` is a part of `failed`, not counted again
56
+ if (r.sent + r.failed < OUTBOX_RELAY_BATCH)
57
+ break;
58
+ if (r.sent === 0 && r.failed > 0)
59
+ break;
60
+ }
61
+ if (sent > 0)
62
+ this.log.debug("outbox relay delivered rows", { rows: sent });
63
+ return sent;
64
+ }
65
+ };
66
+ __decorate([
67
+ LockedInterval(OUTBOX_RELAY_JOB, () => OUTBOX_RELAY_PERIOD_MS)
68
+ ], OutboxRelay.prototype, "relay", null);
69
+ OutboxRelay = __decorate([
70
+ Injectable(),
71
+ __param(0, Inject(OutboxDispatcher)),
72
+ __param(1, Inject(KernelConfigService)),
73
+ __param(2, Inject(LockService)),
74
+ __param(3, Inject(KERNEL_LOGGER))
75
+ ], OutboxRelay);
76
+ export { OutboxRelay };
@@ -0,0 +1,51 @@
1
+ import type { KernelLogger } from "../../log.js";
2
+ import { KernelDb } from "../database/kernel-db.js";
3
+ import { TransactionHost } from "../database/transaction-host.js";
4
+ import type { KernelQueueName } from "../queue/queue-names.js";
5
+ import type { OutboxJobName } from "./registry.js";
6
+ /** A BullMQ job owed by the caller's transaction (transactional outbox spec §5). */
7
+ export interface OutboxJob<T> {
8
+ /** One of the kernel queues. */
9
+ queue: KernelQueueName;
10
+ /** A registered job name ({@link OutboxJobName}). */
11
+ name: OutboxJobName;
12
+ /** Deterministic, no `:`; one pending (not dead) row per (queue, jobId). */
13
+ jobId: string;
14
+ /** JSON data: stored as jsonb, so what JSON cannot carry (undefined, Date objects) is lost. */
15
+ data: T;
16
+ /** Delay target: delivered as a BullMQ `delay` computed at dispatch. */
17
+ runAt?: Date;
18
+ /** The org the work belongs to (its deletion removes the pending row); absent for platform work. */
19
+ orgId?: string;
20
+ }
21
+ /** Delivers the given outbox rows right after their transaction committed (the fast path, §3.3). */
22
+ export type OutboxFastPath = (ids: readonly string[]) => Promise<void>;
23
+ /** The fast path `add` hands each committed transaction's rows to (`OutboxModule` provides it). */
24
+ export declare const OUTBOX_FAST_PATH: unique symbol;
25
+ /**
26
+ * Test seam (`KernelOverride`): `true` makes a reused job id with different data throw instead of
27
+ * logging an error (spec §4, designer's call: "throws in tests, logs in production").
28
+ */
29
+ export declare const OUTBOX_STRICT_CONFLICTS: unique symbol;
30
+ /**
31
+ * Writes owed work as a row in the caller's transaction (spec §3.2): the row commits or rolls back
32
+ * with the business write, and after the commit the fast path delivers it when it is due now (a
33
+ * row with a future `runAt` waits for the relay, spec §3.3).
34
+ */
35
+ export declare class OutboxService {
36
+ readonly txHost: TransactionHost;
37
+ private readonly db;
38
+ private readonly log;
39
+ private readonly fastPath;
40
+ private readonly strictConflicts;
41
+ constructor(txHost: TransactionHost, db: KernelDb, log: KernelLogger, fastPath: OutboxFastPath, strictConflicts?: boolean);
42
+ /**
43
+ * Adds the job to the outbox in the transaction in context (throws outside one). Returns true when
44
+ * it inserted a row, false when the same (queue, jobId) is already owed (a no-op; a dead row is
45
+ * not owed). A conflict whose stored data differs is a bug in the job id: it throws under strict
46
+ * conflicts, else logs an error. Undefined data is stored as JSON null.
47
+ */
48
+ add<T>(job: OutboxJob<T>): Promise<boolean>;
49
+ /** Collects `id` for its transaction; the first id registers the transaction's one hint. */
50
+ private deliverAfterCommit;
51
+ }