@pattern-stack/codegen 0.16.1 → 0.17.1

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 (127) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/consumer-skills/entities/families-and-queries.md +5 -3
  3. package/consumer-skills/integration/audit-and-detection.md +29 -4
  4. package/dist/{chunk-H6FO2ZDJ.js → chunk-4PFF3ED4.js} +4 -4
  5. package/dist/{chunk-CO6LUM72.js → chunk-7P5ODGLA.js} +34 -2
  6. package/dist/chunk-7P5ODGLA.js.map +1 -0
  7. package/dist/{chunk-QSJ3J4HE.js → chunk-BHZP6LOV.js} +7 -7
  8. package/dist/{chunk-RUSUZZAF.js → chunk-BK5ICA2F.js} +4 -4
  9. package/dist/{chunk-T4YJRD22.js → chunk-DUMI2J5M.js} +45 -14
  10. package/dist/chunk-DUMI2J5M.js.map +1 -0
  11. package/dist/{chunk-TKVTEUBD.js → chunk-EJBK7I4F.js} +2 -2
  12. package/dist/{chunk-IT6FRTEW.js → chunk-FVNAU7VO.js} +39 -18
  13. package/dist/chunk-FVNAU7VO.js.map +1 -0
  14. package/dist/{chunk-JM3T27ZW.js → chunk-FWRL7BZ5.js} +7 -7
  15. package/dist/{chunk-DGYTSCKN.js → chunk-HOIRY5XP.js} +14 -14
  16. package/dist/{chunk-AYC2HEAL.js → chunk-HPS554L4.js} +9 -9
  17. package/dist/{chunk-2WDX6I7T.js → chunk-IOQMMH6C.js} +16 -6
  18. package/dist/{chunk-2WDX6I7T.js.map → chunk-IOQMMH6C.js.map} +1 -1
  19. package/dist/{chunk-24WXSC3C.js → chunk-JA7GJDNI.js} +15 -9
  20. package/dist/chunk-JA7GJDNI.js.map +1 -0
  21. package/dist/{chunk-36U5UGIO.js → chunk-JEINYUJH.js} +8 -5
  22. package/dist/chunk-JEINYUJH.js.map +1 -0
  23. package/dist/{chunk-BOPZWRJK.js → chunk-JYBFPNBJ.js} +8 -8
  24. package/dist/chunk-JYBFPNBJ.js.map +1 -0
  25. package/dist/{chunk-K2I6XIK5.js → chunk-KSTZIULO.js} +4 -4
  26. package/dist/chunk-MKWQKKK7.js +72 -0
  27. package/dist/chunk-MKWQKKK7.js.map +1 -0
  28. package/dist/{chunk-CRBVI4GE.js → chunk-PSDVGPQR.js} +5 -5
  29. package/dist/{chunk-DLG62MQY.js → chunk-SFQRETXJ.js} +7 -7
  30. package/dist/{chunk-NXNVTXKG.js → chunk-SGSWVNNB.js} +5 -5
  31. package/dist/{chunk-5LXOJGO2.js → chunk-VNBC3VXM.js} +6 -6
  32. package/dist/{job-orchestrator.protocol-DubMVbm9.d.ts → job-orchestrator.protocol-ZuJ3ow-O.d.ts} +77 -3
  33. package/dist/runtime/base-classes/activity-entity-repository.d.ts +39 -7
  34. package/dist/runtime/base-classes/activity-entity-repository.js +1 -1
  35. package/dist/runtime/base-classes/activity-entity-service.d.ts +12 -10
  36. package/dist/runtime/base-classes/activity-entity-service.js +1 -1
  37. package/dist/runtime/base-classes/index.js +18 -18
  38. package/dist/runtime/shared/openapi/index.js +3 -3
  39. package/dist/runtime/subsystems/auth/index.js +3 -3
  40. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.d.ts +1 -1
  41. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +2 -2
  42. package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +2 -2
  43. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +6 -6
  44. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +1 -1
  45. package/dist/runtime/subsystems/bridge/bridge.module.js +19 -19
  46. package/dist/runtime/subsystems/bridge/event-flow.service.d.ts +1 -1
  47. package/dist/runtime/subsystems/bridge/index.d.ts +1 -1
  48. package/dist/runtime/subsystems/bridge/index.js +21 -21
  49. package/dist/runtime/subsystems/cache/cache.module.js +1 -1
  50. package/dist/runtime/subsystems/cache/index.js +3 -3
  51. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +2 -2
  52. package/dist/runtime/subsystems/events/events.module.js +3 -3
  53. package/dist/runtime/subsystems/events/index.js +3 -3
  54. package/dist/runtime/subsystems/index.d.ts +1 -1
  55. package/dist/runtime/subsystems/index.js +50 -50
  56. package/dist/runtime/subsystems/integration/deep-equal.differ.d.ts +19 -0
  57. package/dist/runtime/subsystems/integration/deep-equal.differ.js +1 -1
  58. package/dist/runtime/subsystems/integration/index.js +22 -22
  59. package/dist/runtime/subsystems/integration/integration.module.d.ts +20 -0
  60. package/dist/runtime/subsystems/integration/integration.module.js +4 -4
  61. package/dist/runtime/subsystems/jobs/index.d.ts +1 -1
  62. package/dist/runtime/subsystems/jobs/index.js +43 -43
  63. package/dist/runtime/subsystems/jobs/job-handler.base.d.ts +1 -1
  64. package/dist/runtime/subsystems/jobs/job-handler.base.js +11 -3
  65. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +1 -1
  66. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +7 -6
  67. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
  68. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +1 -1
  69. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +4 -3
  70. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +11 -1
  71. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +3 -3
  72. package/dist/runtime/subsystems/jobs/job-orchestrator.protocol.d.ts +1 -1
  73. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +1 -1
  74. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +1 -1
  75. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +3 -3
  76. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +1 -1
  77. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +3 -3
  78. package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +1 -1
  79. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +1 -1
  80. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +3 -3
  81. package/dist/runtime/subsystems/jobs/job-worker.d.ts +1 -1
  82. package/dist/runtime/subsystems/jobs/job-worker.js +3 -3
  83. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +1 -1
  84. package/dist/runtime/subsystems/jobs/job-worker.module.js +13 -13
  85. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +11 -11
  86. package/dist/runtime/subsystems/jobs/jobs-errors.d.ts +1 -1
  87. package/dist/runtime/subsystems/observability/index.d.ts +1 -1
  88. package/dist/runtime/subsystems/observability/observability.protocol.d.ts +1 -1
  89. package/dist/runtime/subsystems/observability/observability.service.d.ts +1 -1
  90. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +1 -1
  91. package/dist/runtime/subsystems/observability/reporters/index.d.ts +1 -1
  92. package/dist/runtime/subsystems/storage/index.js +4 -4
  93. package/dist/runtime/subsystems/storage/storage.module.js +2 -2
  94. package/dist/src/cli/index.js +34 -12
  95. package/dist/src/cli/index.js.map +1 -1
  96. package/dist/src/index.d.ts +23 -8
  97. package/dist/src/index.js +7 -7
  98. package/package.json +2 -1
  99. package/runtime/base-classes/activity-entity-repository.ts +72 -13
  100. package/runtime/base-classes/activity-entity-service.ts +14 -12
  101. package/runtime/subsystems/integration/deep-equal.differ.ts +34 -5
  102. package/runtime/subsystems/integration/integration.module.ts +26 -2
  103. package/runtime/subsystems/jobs/job-handler.base.ts +115 -2
  104. package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +43 -16
  105. package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +58 -18
  106. package/src/patterns/library/activity.pattern.ts +40 -10
  107. package/templates/subsystem/integration-config/codegen-config-integration-block.ejs.t +17 -0
  108. package/dist/chunk-24WXSC3C.js.map +0 -1
  109. package/dist/chunk-36U5UGIO.js.map +0 -1
  110. package/dist/chunk-BOPZWRJK.js.map +0 -1
  111. package/dist/chunk-CO6LUM72.js.map +0 -1
  112. package/dist/chunk-IT6FRTEW.js.map +0 -1
  113. package/dist/chunk-T4YJRD22.js.map +0 -1
  114. package/dist/chunk-XCEI7NUH.js +0 -41
  115. package/dist/chunk-XCEI7NUH.js.map +0 -1
  116. /package/dist/{chunk-H6FO2ZDJ.js.map → chunk-4PFF3ED4.js.map} +0 -0
  117. /package/dist/{chunk-QSJ3J4HE.js.map → chunk-BHZP6LOV.js.map} +0 -0
  118. /package/dist/{chunk-RUSUZZAF.js.map → chunk-BK5ICA2F.js.map} +0 -0
  119. /package/dist/{chunk-TKVTEUBD.js.map → chunk-EJBK7I4F.js.map} +0 -0
  120. /package/dist/{chunk-JM3T27ZW.js.map → chunk-FWRL7BZ5.js.map} +0 -0
  121. /package/dist/{chunk-DGYTSCKN.js.map → chunk-HOIRY5XP.js.map} +0 -0
  122. /package/dist/{chunk-AYC2HEAL.js.map → chunk-HPS554L4.js.map} +0 -0
  123. /package/dist/{chunk-K2I6XIK5.js.map → chunk-KSTZIULO.js.map} +0 -0
  124. /package/dist/{chunk-CRBVI4GE.js.map → chunk-PSDVGPQR.js.map} +0 -0
  125. /package/dist/{chunk-DLG62MQY.js.map → chunk-SFQRETXJ.js.map} +0 -0
  126. /package/dist/{chunk-NXNVTXKG.js.map → chunk-SGSWVNNB.js.map} +0 -0
  127. /package/dist/{chunk-5LXOJGO2.js.map → chunk-VNBC3VXM.js.map} +0 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,127 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.17.1] — 2026-06-04
8
+
9
+ **Two dogfood fixes that bit the same swe-brain mutation drain** (ADR-0009
10
+ Amendment B): the jobs orchestrator silently dropped function-form
11
+ concurrency/dedupe keys, and the integration differ unconditionally ignored
12
+ `deletedAt`. Both are honored now; both are backward-compatible.
13
+
14
+ ### Fixed
15
+
16
+ - **`@JobHandler` function-form `concurrency.key` / `dedupe.key` are honored
17
+ end-to-end** (JOB-FN-KEY; swe-brain ADR-0009 Amendment B §B3). The typed API
18
+ (`ConcurrencyPolicy.key`/`DedupePolicy.key`) had ALWAYS required a function,
19
+ but registration (`upsertJobRows`) stored only `typeof key === 'string' ? key
20
+ : null` — a function key persisted as a NULL `concurrency_key_template`, so
21
+ `start()` wrote a NULL `job_run.concurrency_key` and the worker's queue-release
22
+ gate (which keys off `claimed.concurrencyKey`) never engaged. Observed in
23
+ swe-brain: three `inbound-sync` runs the handler believed shared one
24
+ `collisionMode: 'queue'` lane ran fully concurrently, three `integration_runs`
25
+ racing one message row. Now both backends persist a function key as the
26
+ `FN_KEY_SENTINEL` marker (non-null, so the collision/dedupe path engages, and
27
+ hash-stable so the definition-hash gate doesn't churn) and re-resolve the live
28
+ function from `JOB_HANDLER_REGISTRY` at `start()`. A `FN_KEY_SENTINEL` with no
29
+ live function throws `JobKeyFunctionUnavailableError` (fail loud, never
30
+ silently degrade to no-key). Drizzle + memory + BullMQ (which delegates to the
31
+ Drizzle `start`) all agree.
32
+
33
+ ### Changed
34
+
35
+ - **`ConcurrencyPolicy.key` / `DedupePolicy.key` widen to
36
+ `JobKeySelector<TInput> = string | ((input) => string)`.** The string form is
37
+ documented as a `{{field}}` template (evaluated by `evaluateKeyTemplate`); the
38
+ function form is the one that previously type-checked but was dropped. Existing
39
+ function keys now WORK (were silently no-key before); existing string-template
40
+ keys behave identically. New exports from `@pattern-stack/codegen/runtime/*`
41
+ jobs: `JobKeySelector`, `FN_KEY_SENTINEL`, `keySelectorToTemplate`,
42
+ `resolveJobKey`, `JobKeyFunctionUnavailableError`.
43
+
44
+ ### Added
45
+
46
+ - **`DeepEqualDifferOptions.unignore`** — the inverse of `ignore`, subtracted
47
+ from the default ignore set after the merge (DIFFER-UNIGNORE; swe-brain
48
+ ADR-0009 Amendment B §B4). Lets a consumer declare that a normally-metadata
49
+ column is DOMAIN DATA for their entity. The canonical case: an entity with
50
+ `softDelete: false` whose `deletedAt` carries a vendor-observed retraction
51
+ tombstone ON the canonical record (a Slack `message_deleted` → `deletedAt`,
52
+ ADR-0008 §1). `deletedAt` is in `DEFAULT_IGNORE_FIELDS`, so the tombstone
53
+ overlay diffed to `'noop'` → the upsert was skipped → `deleted_at` never
54
+ landed (observed: `integration_run_items` `{operation: noop, changed_fields:
55
+ {}}` for every delete). `new DeepEqualDiffer({ unignore: ['deletedAt'] })` now
56
+ makes the field register as a change. `unignore` wins over a field also in
57
+ `ignore`; un-ignoring a field not in the set is a harmless no-op; per-instance
58
+ (never mutates `DEFAULT_IGNORE_FIELDS`).
59
+ - **`IntegrationModuleOptions.differ` + `integration.differ.{ignore,unignore}`
60
+ config threading.** `IntegrationModule.forRoot({ differ: { unignore:
61
+ [...] } })` threads into the default `DeepEqualDiffer` bound to
62
+ `INTEGRATION_FIELD_DIFFER`, and the subsystem barrel generator emits that
63
+ `forRoot` option from `integration.differ.*` in `codegen.config.yaml` (same
64
+ off-by-default config-threading shape as 0.16.0's `listen_notify`; vendored +
65
+ package mode both covered). A feature module that binds its own
66
+ `IFieldDiffer<T>` still overrides entirely.
67
+
68
+ ### Docs
69
+
70
+ - Differ header comment + `integration` consumer skill (`audit-and-detection.md`,
71
+ `protocols-and-ports.md`) document the `unignore` knob and the
72
+ `integration.differ.*` config path; the `integration-config` codegen.config
73
+ template gains a commented `differ:` block.
74
+
75
+ ## [0.17.0] — 2026-06-04
76
+
77
+ **`ActivityPattern` subject scoping is config-driven** (ACTIVITY-SUBJECT-1) —
78
+ the library's Activity base classes no longer bake the CRM term "opportunity"
79
+ into their finders. An activity/interaction entity declares which subject it is
80
+ scoped to via the pattern's new `config:` block, and the generated repo/service
81
+ expose generic, config-resolved subject lookups. Surfaced by the swe-brain
82
+ dogfood, whose interactions (meeting, email, transcript, message) reference
83
+ `person`/`repo`/`team` subjects (ADR-0006), not CRM opportunities.
84
+
85
+ Consumer census confirmed **no project used the Activity pattern** (dealbrain's
86
+ sole `findByOpportunityId` is a `JunctionSyncRepository` method, not the pattern;
87
+ swe-brain's interactions are all `pattern: Integrated`), so this is a clean cut
88
+ with no aliases per the "no backwards compatibility until users" rule. The
89
+ `patterns: [Integrated, Activity]` composition — the swe-brain target — is
90
+ validated and tested.
91
+
92
+ ### Added
93
+
94
+ - **`ActivityPattern.configSchema`** — `{ subject?, subjectColumn?, occurredAt? }`
95
+ (all optional, `.strict()`). `subject` derives the FK column `<subject>_id`;
96
+ `subjectColumn` overrides it explicitly; `occurredAt` names the recency column
97
+ (default `occurred_at`). Validated at parse time via the standard ADR-031
98
+ composition path; emitted onto the concrete repo as `patternConfig` (the same
99
+ hand-off `IntegratedEntityRepository` uses for `integrationConfig`).
100
+ - **`ActivityPatternConfig`** interface exported from
101
+ `runtime/base-classes/activity-entity-repository.ts`.
102
+
103
+ ### Changed
104
+
105
+ - **`ActivityEntityRepository` finders are config-driven.**
106
+ `findByOpportunityId` / `findRecentByOpportunityId` are replaced by
107
+ `findBySubjectId` / `findRecentBySubjectId`, which resolve the subject FK column
108
+ (and recency-ordering column) from `this.patternConfig` at runtime. Calling a
109
+ subject finder with no subject configured throws a clear error naming the
110
+ config key to set. `findByDateRange` and `findByUserId` are unchanged (actor
111
+ scoping is generally applicable, not CRM-shaped).
112
+ - **`ActivityEntityService`** mirrors the rename: `findBySubject` /
113
+ `findRecent` (was `findByOpportunity` / `findRecent`); `IActivityEntityRepository`
114
+ drops the opportunity methods for the subject ones. `findByDateRange` /
115
+ `findByUser` unchanged.
116
+ - **`ActivityPattern` inherited-method comment strings** now advertise the
117
+ subject finders; the header's byte-identical-to-FAMILY_MAP claim is removed (it
118
+ was a one-time PATTERN-5 migration guarantee, not a standing contract).
119
+
120
+ ### Migration
121
+
122
+ No consumer action required — no project used the Activity pattern. A project
123
+ adopting it now declares `pattern: Activity` (or `patterns: [Integrated,
124
+ Activity]`) plus `config: { Activity: { subject: <entity> } }`. Named per-subject
125
+ finders (`findByPersonId`) remain available the same way they always were — via
126
+ the entity's declarative `queries:` block.
127
+
7
128
  ## [0.16.1] — 2026-06-04
8
129
 
9
130
  **`WebhookFetchCallback<T>` yields `{ record, eventId?, cursor? }`** —
@@ -16,7 +16,7 @@ an optional `tx?: DrizzleTx` for transactional composition.
16
16
  |---|---|---|
17
17
  | `Base` | plain tables with no special access pattern | nothing — standard CRUD only |
18
18
  | `Synced` | records mirrored from an external system (have an external id + per-user visibility) | `findByExternalId`, `findAllByUserId`, `findVisibleByUserId`, `syncUpsert` |
19
- | `Activity` | time-ordered activity/event rows tied to a parent | `findByDateRange`, `findByUserId`, `findByOpportunityId`, `findRecentByOpportunityId` |
19
+ | `Activity` | time-ordered activity/interaction rows scoped to a subject | `findByDateRange`, `findByUserId`, `findBySubjectId`, `findRecentBySubjectId` (subject FK + recency column resolved from `config: { Activity: { subject: <entity> } }`) |
20
20
  | `Metadata` | key/value or definition/value rows describing other entities | `findByEntityIdAndType`, `listByEntityId`, `listHistoryByEntityId` |
21
21
  | `Knowledge` | semantically-searchable knowledge rows (pgvector at runtime) | `semanticSearch`, `findPendingByOpportunityId`, `updateStatus`, `updateStatusBatch` |
22
22
 
@@ -77,6 +77,8 @@ and a `GET /<plural>/search` route. `paginate: true` makes the route accept
77
77
  - **`order:` is the default sort**, not a parameter — add a `queries:` search
78
78
  entry if you need caller-controlled ordering.
79
79
  - **Family methods assume their columns exist.** `Synced` expects an external-id
80
- + user-visibility shape; `Activity` expects a parent FK like
81
- `opportunity_id`. If your table doesn't fit, pick `Base` and add explicit
80
+ + user-visibility shape; `Activity`'s subject finders expect the subject FK
81
+ named by its `config:` (`subject: person` `person_id`, or an explicit
82
+ `subjectColumn`) plus a recency column (`occurred_at` by default, or
83
+ `config.occurredAt`). If your table doesn't fit, pick `Base` and add explicit
82
84
  `queries:`.
@@ -285,15 +285,40 @@ as changes), but the diff output preserves the *raw* values:
285
285
  Drizzle while adapters deliver numbers; a numeric and a finite-parseable
286
286
  string compare equal (with an empty-string guard against silent 0-equality).
287
287
 
288
- **Augment the ignore list per entity** (values merge with the defaults; you
289
- cannot remove defaults):
288
+ **Augment the ignore list** (`ignore` values merge with the defaults):
290
289
 
291
290
  ```ts
292
291
  { provide: INTEGRATION_FIELD_DIFFER, useValue: new DeepEqualDiffer({ ignore: ['integration_version'] }) }
293
292
  ```
294
293
 
295
- Bind it as `useValue: new DeepEqualDiffer(...)`, not `useClass` the
296
- constructor's optional options object confuses Nest's metadata reflection.
294
+ **Un-ignore a default** (`unignore` the inverse knob). A normally-metadata
295
+ column can be *domain data* for a given entity. The canonical case: an entity
296
+ with `softDelete: false` whose `deletedAt` carries a vendor-observed retraction
297
+ tombstone *on the canonical record* (e.g. a Slack `message_deleted` maps to
298
+ `deletedAt`). Because `deletedAt` is in the default ignore list, the tombstone
299
+ overlay diffs to `'noop'`, the upsert is skipped, and `deleted_at` never lands.
300
+ `unignore` removes it from the ignore set so it registers as a field change:
301
+
302
+ ```ts
303
+ { provide: INTEGRATION_FIELD_DIFFER, useValue: new DeepEqualDiffer({ unignore: ['deletedAt'] }) }
304
+ ```
305
+
306
+ `unignore` is subtracted after `ignore` is merged, so it wins on a field listed
307
+ in both. Un-ignoring a field that isn't in the (merged) set is a harmless no-op.
308
+
309
+ **Set it once for the whole app via config** instead of binding per feature
310
+ module — `integration.differ.{ignore,unignore}` in `codegen.config.yaml` threads
311
+ into the default differ that `IntegrationModule.forRoot` provides:
312
+
313
+ ```yaml
314
+ integration:
315
+ backend: drizzle
316
+ differ:
317
+ unignore: [deletedAt] # this entity's deletedAt is domain data
318
+ ```
319
+
320
+ Bind a per-module differ as `useValue: new DeepEqualDiffer(...)`, not `useClass`
321
+ — the constructor's optional options object confuses Nest's metadata reflection.
297
322
 
298
323
  **`providerChangedFields` is advisory.** When a CDC provider tells you which
299
324
  columns changed, set it on the `Change<T>` and the differ skips deep-equal over
@@ -1,11 +1,11 @@
1
- import {
2
- BRIDGE_OUTBOX_DRAIN_HOOK
3
- } from "./chunk-4LH67P4U.js";
4
1
  import {
5
2
  EVENTS_WAKE_CHANNEL,
6
3
  PgNotifyListener,
7
4
  pgNotify
8
5
  } from "./chunk-MYQIQ27N.js";
6
+ import {
7
+ BRIDGE_OUTBOX_DRAIN_HOOK
8
+ } from "./chunk-4LH67P4U.js";
9
9
  import {
10
10
  domainEvents
11
11
  } from "./chunk-OFRRBC7M.js";
@@ -393,4 +393,4 @@ DrizzleEventBus = __decorateClass([
393
393
  export {
394
394
  DrizzleEventBus
395
395
  };
396
- //# sourceMappingURL=chunk-H6FO2ZDJ.js.map
396
+ //# sourceMappingURL=chunk-4PFF3ED4.js.map
@@ -47,6 +47,34 @@ var HandlerRegistry;
47
47
  }
48
48
  HandlerRegistry2.get = get;
49
49
  })(HandlerRegistry || (HandlerRegistry = {}));
50
+ var FN_KEY_SENTINEL = "<fn>";
51
+ function keySelectorToTemplate(key) {
52
+ if (typeof key === "string") return key;
53
+ if (typeof key === "function") return FN_KEY_SENTINEL;
54
+ return null;
55
+ }
56
+ function resolveJobKey(kind, type, template, payload, evaluateTemplate) {
57
+ if (template == null) return null;
58
+ if (template !== FN_KEY_SENTINEL) return evaluateTemplate(template, payload);
59
+ const meta = JOB_HANDLER_REGISTRY.get(type)?.meta;
60
+ const key = meta?.[kind]?.key;
61
+ if (typeof key !== "function") {
62
+ throw new JobKeyFunctionUnavailableError(type, kind);
63
+ }
64
+ return key(payload);
65
+ }
66
+ var JobKeyFunctionUnavailableError = class extends Error {
67
+ constructor(jobType, kind) {
68
+ super(
69
+ `[jobs] ${kind} key for job '${jobType}' was persisted as a function sentinel ('${FN_KEY_SENTINEL}') but no live function is registered for it. The @JobHandler must be imported before start() so its meta is in JOB_HANDLER_REGISTRY.`
70
+ );
71
+ this.jobType = jobType;
72
+ this.kind = kind;
73
+ this.name = "JobKeyFunctionUnavailableError";
74
+ }
75
+ jobType;
76
+ kind;
77
+ };
50
78
 
51
79
  export {
52
80
  ParentClosePolicy,
@@ -54,6 +82,10 @@ export {
54
82
  JOB_HANDLER_REGISTRY,
55
83
  JOB_HANDLER_METADATA_KEY,
56
84
  JobHandler,
57
- HandlerRegistry
85
+ HandlerRegistry,
86
+ FN_KEY_SENTINEL,
87
+ keySelectorToTemplate,
88
+ resolveJobKey,
89
+ JobKeyFunctionUnavailableError
58
90
  };
59
- //# sourceMappingURL=chunk-CO6LUM72.js.map
91
+ //# sourceMappingURL=chunk-7P5ODGLA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../runtime/subsystems/jobs/job-handler.base.ts"],"sourcesContent":["/**\n * Handler base class, JobContext, @JobHandler decorator, and policy types\n * for the job orchestration domain (ADR-022, JOB-2).\n *\n * User-authored jobs subclass `JobHandlerBase<TInput, TOutput>` and decorate\n * the class with `@JobHandler<TInput>('job_type', meta)`. The decorator\n * 1. stores metadata via `Reflect.defineMetadata` so Nest's reflector can\n * pick it up at module boot, and\n * 2. populates `JOB_HANDLER_REGISTRY` — a module-singleton map consumed by\n * `JobWorkerModule` (JOB-5) to materialise `job` rows and resolve\n * handler classes during claim/execute.\n *\n * No runtime orchestration lives here; this file is a pure type + decorator\n * surface so downstream PRs (JOB-3..JOB-5) can implement against a stable\n * shape.\n */\n// TODO(logging-subsystem): swap to ILogger once ADR-028 lands\nimport type { Logger } from '@nestjs/common';\nimport { tokenKey } from '../token-key';\nimport type { EventOfType, EventTypeName } from '../events/event-registry';\nimport type { JobRun } from './job-orchestrator.protocol';\n\n// ─── ParentClosePolicy ──────────────────────────────────────────────────────\n\n/**\n * What happens to running child runs when a parent enters a terminal state.\n * Stored on the child at spawn; changes to the parent after spawn do NOT\n * retroactively rewrite children.\n */\nexport enum ParentClosePolicy {\n Terminate = 'terminate',\n Cancel = 'cancel',\n Abandon = 'abandon',\n}\n\n// ─── Policy types ───────────────────────────────────────────────────────────\n\nexport interface RetryPolicy {\n attempts: number;\n backoff: 'fixed' | 'exponential';\n baseMs: number;\n nonRetryableErrors?: string[];\n}\n\n/**\n * Concurrency lane key (JOB-FN-KEY, 0.16.2).\n *\n * Two authoring forms, both honored end-to-end (the typed function form was\n * previously dropped to `null` at registration — see `upsertJobRows` — so\n * `collisionMode` silently never engaged):\n *\n * - **`string`** — a `{{field}}` template evaluated against the start\n * payload by `evaluateKeyTemplate` (single-key substitution, no dotted\n * paths). Persisted verbatim to `job.concurrency_key_template`.\n * - **`(input) => string`** — an arbitrary function of the input. Persisted\n * as the `FN_KEY_SENTINEL` marker so the definition-hash gate stays stable\n * and the collision path engages; `start()` re-resolves the live function\n * from `JOB_HANDLER_REGISTRY` and evaluates it against the payload.\n *\n * Both forms produce a per-lane key; same key + in-flight incumbent ⇒\n * `collisionMode` ('queue' | 'reject' | 'replace') decides.\n */\nexport type JobKeySelector<TInput> = string | ((input: TInput) => string);\n\nexport interface ConcurrencyPolicy<TInput> {\n key: JobKeySelector<TInput>;\n collisionMode: 'queue' | 'reject' | 'replace';\n}\n\nexport interface DedupePolicy<TInput> {\n key: JobKeySelector<TInput>;\n windowMs: number;\n}\n\n/**\n * Declarative scope reference. `TScope` is parameterised so JOB-7 can narrow\n * `entity` to the generated `ScopeEntityType` union at the call site without\n * modifying this file (OQ-1 resolution, 2026-04-20).\n */\nexport interface ScopeRef<TInput, TScope extends string = string> {\n entity: TScope;\n from: (input: TInput) => string;\n}\n\n/**\n * Bridge trigger authoring shape (BRIDGE-6 follow-up — BRIDGE-6 shipped the\n * generator + runtime for `@JobHandler({ triggers })` but never added the\n * authoring field to this type; the generator's tests scan source as strings,\n * so a real decorator was never compiled and the gap went uncaught).\n *\n * Declared on `@JobHandler({ triggers })`; the codegen bridge-registry\n * generator (`src/cli/shared/bridge-registry-generator.ts`) scans these from\n * source and emits `bridge/generated/registry.ts`, validating each `event`\n * against the generated `eventRegistry` at `gen-all`. The distributed union\n * narrows `map`/`when` per `event`, so callbacks are typed against the event\n * payload (ADR-023, \"typed against PayloadOfType<T>\").\n *\n * Typed against events' generated types — the same `import type` coupling the\n * bridge already has (erased at runtime). `jobs` must NOT import `bridge`, so\n * the post-gen `BridgeTriggerEntry` is deliberately not referenced here;\n * `triggerId`/`jobType` are computed by the generator, not authored.\n */\nexport type JobTrigger<TInput> = {\n [T in EventTypeName]: {\n /** Event type that fires this trigger. Validated against `eventRegistry`. */\n event: T;\n /** Maps the event to the job input. Inlined verbatim into the registry. */\n map: (event: EventOfType<T>) => TInput;\n /** Optional guard; `false` → wrapper records `status='skipped'`. */\n when?: (event: EventOfType<T>) => boolean;\n };\n}[EventTypeName];\n\nexport interface JobHandlerMeta<TInput> {\n pool?: string;\n scope?: ScopeRef<TInput>;\n retry?: RetryPolicy;\n concurrency?: ConcurrencyPolicy<TInput>;\n dedupe?: DedupePolicy<TInput>;\n timeoutMs?: number;\n replayFrom?: 'scratch' | 'last_step' | 'last_checkpoint';\n /**\n * Bridge triggers (ADR-023 Tier 3). Codegen scans these into `bridgeRegistry`;\n * the framework `BridgeDeliveryHandler` starts this job per matched event.\n * Absent for jobs started directly or via `IEventFlow.publishAndStart`.\n */\n triggers?: readonly JobTrigger<TInput>[];\n}\n\n// ─── Runtime option shapes ──────────────────────────────────────────────────\n\nexport interface StepOptions {\n retry?: RetryPolicy;\n timeoutMs?: number;\n}\n\nexport interface SpawnChildOptions {\n closePolicy?: ParentClosePolicy;\n runAt?: Date;\n priority?: number;\n tags?: Record<string, string>;\n}\n\n// ─── JobContext ─────────────────────────────────────────────────────────────\n\nexport interface JobContext<TInput> {\n readonly input: TInput;\n readonly run: JobRun;\n step<TOutput>(\n stepId: string,\n fn: () => Promise<TOutput>,\n opts?: StepOptions,\n ): Promise<TOutput>;\n spawnChild(type: string, input: unknown, opts?: SpawnChildOptions): Promise<JobRun>;\n readonly logger: Logger;\n // NOT in Phase 1 — deferred to ADR-025:\n // waitFor(kind, token, opts)\n // signal(token, payload)\n // sleep(ms)\n}\n\n// ─── JobHandlerBase ─────────────────────────────────────────────────────────\n\nexport abstract class JobHandlerBase<TInput, TOutput = unknown> {\n abstract run(ctx: JobContext<TInput>): Promise<TOutput>;\n}\n\n// ─── Registry + decorator ───────────────────────────────────────────────────\n\n/**\n * Module-singleton map keyed by job type. Populated by the `@JobHandler`\n * decorator at class definition time; consumed by `JobWorkerModule` (JOB-5)\n * to upsert `job` rows and resolve handler classes during claim/execute.\n */\nexport const JOB_HANDLER_REGISTRY = new Map<\n string,\n {\n type: string;\n meta: JobHandlerMeta<unknown>;\n handlerClass: new (...args: unknown[]) => JobHandlerBase<unknown>;\n }\n>();\n\n// ADR-037: namespaced `Symbol.for(...)` (via `tokenKey()`) so the reflection-metadata\n// key matches by value across import boundaries (the @JobHandler decorator and the\n// reader may resolve different runtime copies). Distinct from the DI tokens but\n// subject to the same dual-package identity hazard.\nexport const JOB_HANDLER_METADATA_KEY = Symbol.for(tokenKey('jobs', 'handler-metadata'));\n\n/**\n * Class decorator that registers a handler with the job type, the full\n * metadata shape, and the target class constructor.\n *\n * Duplicate-type behaviour (OQ-3, resolved 2026-04-18):\n * - `NODE_ENV === 'production'` → throw; silent overwrite in prod is a\n * correctness bug.\n * - `NODE_ENV === 'test'` → silent overwrite (tests intentionally\n * re-register handlers).\n * - otherwise (dev) → `console.warn` + overwrite. `console`\n * is used intentionally instead of the Nest `Logger` — decorators run\n * at module-load time before any Nest container exists.\n */\nexport function JobHandler<TInput>(\n type: string,\n meta: JobHandlerMeta<TInput>,\n): ClassDecorator {\n return (target) => {\n if (JOB_HANDLER_REGISTRY.has(type)) {\n const env = process.env.NODE_ENV;\n if (env === 'production') {\n throw new Error(\n `[JobHandler] Duplicate registration for job type '${type}'. ` +\n `Each @JobHandler must declare a unique type.`,\n );\n }\n if (env !== 'test') {\n // eslint-disable-next-line no-console\n console.warn(\n `[JobHandler] Duplicate registration for job type '${type}'. ` +\n `Overwriting previous handler — this is almost certainly a bug.`,\n );\n }\n }\n\n Reflect.defineMetadata(JOB_HANDLER_METADATA_KEY, { type, meta }, target);\n JOB_HANDLER_REGISTRY.set(type, {\n type,\n meta: meta as JobHandlerMeta<unknown>,\n handlerClass: target as unknown as new (\n ...args: unknown[]\n ) => JobHandlerBase<unknown>,\n });\n };\n}\n\n// ─── HandlerRegistry — read helpers consumed by JobWorkerModule (JOB-5) ─────\n\n/**\n * Single entry shape returned by `HandlerRegistry.getAll()` / `.get()` and\n * exposed to `JobWorkerModule.onModuleInit` for boot-time upserts.\n *\n * Structurally compatible with `IJobOrchestrator.upsertJobRows`'s\n * `JobUpsertEntry` so the worker module can pass entries through verbatim\n * without re-mapping.\n */\nexport interface HandlerRegistryEntry {\n type: string;\n meta: JobHandlerMeta<unknown>;\n handlerClass: new (...args: unknown[]) => JobHandlerBase<unknown>;\n}\n\n/**\n * Read facade over `JOB_HANDLER_REGISTRY`. The decorator's write path is\n * unchanged; this namespace exists so consumers (the worker module, tests)\n * don't import the raw `Map` and accidentally mutate it.\n */\nexport namespace HandlerRegistry {\n /** All registered entries in insertion order. */\n export function getAll(): HandlerRegistryEntry[] {\n return Array.from(JOB_HANDLER_REGISTRY.values());\n }\n\n /** Lookup by job type, or `undefined` if no `@JobHandler` is registered. */\n export function get(type: string): HandlerRegistryEntry | undefined {\n return JOB_HANDLER_REGISTRY.get(type);\n }\n}\n\n// ─── Key resolution (JOB-FN-KEY, 0.16.2) ────────────────────────────────────\n\n/**\n * Sentinel persisted to `job.concurrency_key_template` / `dedupe_key_template`\n * when the authored `key` is a function rather than a `{{field}}` template.\n *\n * Why a sentinel (not `null`): the collision/dedupe paths in both backends gate\n * on `definition.concurrencyKeyTemplate != null`. A function key persisted as\n * `null` (the pre-0.16.2 bug) left those columns empty, so `collisionMode` /\n * the dedupe window never engaged — the job ran with NO key. A stable sentinel\n * keeps the column non-null (path engages) AND keeps the definition-hash gate\n * (`upsertJobRows`' `IS DISTINCT FROM` clause) stable across boots, since the\n * function identity itself can't be hashed. `start()` detects the sentinel and\n * re-resolves the live function from `JOB_HANDLER_REGISTRY`.\n *\n * Chosen as an angle-bracketed token so it can never collide with a real\n * `{{field}}` template (which never contains a literal `<`).\n */\nexport const FN_KEY_SENTINEL = '<fn>';\n\n/**\n * Registration-time projection: collapse an authored `JobKeySelector` to the\n * string stored in the `job` definition row. A string template is stored\n * verbatim; a function is stored as `FN_KEY_SENTINEL`; absence stays `null`.\n */\nexport function keySelectorToTemplate(\n key: JobKeySelector<unknown> | undefined,\n): string | null {\n if (typeof key === 'string') return key;\n if (typeof key === 'function') return FN_KEY_SENTINEL;\n return null;\n}\n\n/** Which meta policy a key belongs to — selects the live fn at `start()`. */\nexport type KeyKind = 'concurrency' | 'dedupe';\n\n/**\n * `start()`-time resolution shared by every backend. Turns the persisted\n * template column into the concrete per-run key for the given payload.\n *\n * - `template == null` → `null` (no key; caller skips the collision/dedupe path).\n * - `template === FN_KEY_SENTINEL` → look the live `@JobHandler` meta up in\n * `JOB_HANDLER_REGISTRY`, pull `meta[kind].key`, and invoke it against the\n * payload. The registry is the runtime source of truth (the worker already\n * resolves handler classes the same way), so the function survives the DB\n * round-trip even though it can't be persisted.\n * - otherwise → a `{{field}}` template, evaluated via the injected\n * `evaluateTemplate` (each backend passes its own copy to avoid a runtime\n * import cycle).\n *\n * Throws `JobKeyFunctionUnavailableError` if the sentinel is present but no\n * live function can be found (e.g. the registry was reset, or a function key\n * was persisted by a newer build and read by an older one). Failing loud beats\n * silently degrading to no-key — the exact regression this fix exists to kill.\n */\nexport function resolveJobKey(\n kind: KeyKind,\n type: string,\n template: string | null,\n payload: Record<string, unknown>,\n evaluateTemplate: (template: string, payload: Record<string, unknown>) => string,\n): string | null {\n if (template == null) return null;\n if (template !== FN_KEY_SENTINEL) return evaluateTemplate(template, payload);\n\n const meta = JOB_HANDLER_REGISTRY.get(type)?.meta;\n const key = (meta?.[kind] as { key?: unknown } | undefined)?.key;\n if (typeof key !== 'function') {\n throw new JobKeyFunctionUnavailableError(type, kind);\n }\n return (key as (input: unknown) => string)(payload);\n}\n\n/**\n * Raised when a `${FN_KEY_SENTINEL}` template is read but the live function\n * key is missing from `JOB_HANDLER_REGISTRY`. Kept here (not in `jobs-errors`)\n * so `job-handler.base` stays import-cycle-free.\n */\nexport class JobKeyFunctionUnavailableError extends Error {\n constructor(\n readonly jobType: string,\n readonly kind: KeyKind,\n ) {\n super(\n `[jobs] ${kind} key for job '${jobType}' was persisted as a function ` +\n `sentinel ('${FN_KEY_SENTINEL}') but no live function is registered ` +\n `for it. The @JobHandler must be imported before start() so its meta ` +\n `is in JOB_HANDLER_REGISTRY.`,\n );\n this.name = 'JobKeyFunctionUnavailableError';\n }\n}\n"],"mappings":";;;;;AA6BO,IAAK,oBAAL,kBAAKA,uBAAL;AACL,EAAAA,mBAAA,eAAY;AACZ,EAAAA,mBAAA,YAAS;AACT,EAAAA,mBAAA,aAAU;AAHA,SAAAA;AAAA,GAAA;AAsIL,IAAe,iBAAf,MAAyD;AAEhE;AASO,IAAM,uBAAuB,oBAAI,IAOtC;AAMK,IAAM,2BAA2B,OAAO,IAAI,SAAS,QAAQ,kBAAkB,CAAC;AAehF,SAAS,WACd,MACA,MACgB;AAChB,SAAO,CAAC,WAAW;AACjB,QAAI,qBAAqB,IAAI,IAAI,GAAG;AAClC,YAAM,MAAM,QAAQ,IAAI;AACxB,UAAI,QAAQ,cAAc;AACxB,cAAM,IAAI;AAAA,UACR,qDAAqD,IAAI;AAAA,QAE3D;AAAA,MACF;AACA,UAAI,QAAQ,QAAQ;AAElB,gBAAQ;AAAA,UACN,qDAAqD,IAAI;AAAA,QAE3D;AAAA,MACF;AAAA,IACF;AAEA,YAAQ,eAAe,0BAA0B,EAAE,MAAM,KAAK,GAAG,MAAM;AACvE,yBAAqB,IAAI,MAAM;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,cAAc;AAAA,IAGhB,CAAC;AAAA,EACH;AACF;AAuBO,IAAU;AAAA,CAAV,CAAUC,qBAAV;AAEE,WAAS,SAAiC;AAC/C,WAAO,MAAM,KAAK,qBAAqB,OAAO,CAAC;AAAA,EACjD;AAFO,EAAAA,iBAAS;AAKT,WAAS,IAAI,MAAgD;AAClE,WAAO,qBAAqB,IAAI,IAAI;AAAA,EACtC;AAFO,EAAAA,iBAAS;AAAA,GAPD;AA8BV,IAAM,kBAAkB;AAOxB,SAAS,sBACd,KACe;AACf,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,OAAO,QAAQ,WAAY,QAAO;AACtC,SAAO;AACT;AAwBO,SAAS,cACd,MACA,MACA,UACA,SACA,kBACe;AACf,MAAI,YAAY,KAAM,QAAO;AAC7B,MAAI,aAAa,gBAAiB,QAAO,iBAAiB,UAAU,OAAO;AAE3E,QAAM,OAAO,qBAAqB,IAAI,IAAI,GAAG;AAC7C,QAAM,MAAO,OAAO,IAAI,GAAqC;AAC7D,MAAI,OAAO,QAAQ,YAAY;AAC7B,UAAM,IAAI,+BAA+B,MAAM,IAAI;AAAA,EACrD;AACA,SAAQ,IAAmC,OAAO;AACpD;AAOO,IAAM,iCAAN,cAA6C,MAAM;AAAA,EACxD,YACW,SACA,MACT;AACA;AAAA,MACE,UAAU,IAAI,iBAAiB,OAAO,4CACtB,eAAe;AAAA,IAGjC;AARS;AACA;AAQT,SAAK,OAAO;AAAA,EACd;AAAA,EAVW;AAAA,EACA;AAUb;","names":["ParentClosePolicy","HandlerRegistry"]}
@@ -1,19 +1,19 @@
1
+ import {
2
+ MemoryJobStore
3
+ } from "./chunk-SNQ3TOWP.js";
4
+ import {
5
+ MissingTenantIdError
6
+ } from "./chunk-T4BIIU5E.js";
1
7
  import {
2
8
  clampLimit,
3
9
  decodeKeysetCursor,
4
10
  encodeKeysetCursor,
5
11
  toJobRunSummary
6
12
  } from "./chunk-L3LZWWSX.js";
7
- import {
8
- MemoryJobStore
9
- } from "./chunk-SNQ3TOWP.js";
10
13
  import {
11
14
  JOBS_MULTI_TENANT,
12
15
  JOB_ORCHESTRATOR
13
16
  } from "./chunk-ZPL74UQN.js";
14
- import {
15
- MissingTenantIdError
16
- } from "./chunk-T4BIIU5E.js";
17
17
  import {
18
18
  __decorateClass,
19
19
  __decorateParam
@@ -209,4 +209,4 @@ function compareBy(a, b, order) {
209
209
  export {
210
210
  MemoryJobRunService
211
211
  };
212
- //# sourceMappingURL=chunk-QSJ3J4HE.js.map
212
+ //# sourceMappingURL=chunk-BHZP6LOV.js.map
@@ -1,12 +1,12 @@
1
- import {
2
- MemoryStorageBackend
3
- } from "./chunk-3SZFUTXE.js";
4
1
  import {
5
2
  STORAGE
6
3
  } from "./chunk-NYBCQZC7.js";
7
4
  import {
8
5
  LocalStorageBackend
9
6
  } from "./chunk-JWNHNUYL.js";
7
+ import {
8
+ MemoryStorageBackend
9
+ } from "./chunk-3SZFUTXE.js";
10
10
  import {
11
11
  __decorateClass
12
12
  } from "./chunk-2E224ZSN.js";
@@ -37,4 +37,4 @@ StorageModule = __decorateClass([
37
37
  export {
38
38
  StorageModule
39
39
  };
40
- //# sourceMappingURL=chunk-RUSUZZAF.js.map
40
+ //# sourceMappingURL=chunk-BK5ICA2F.js.map
@@ -4,9 +4,6 @@ import {
4
4
  import {
5
5
  MemoryJobStore
6
6
  } from "./chunk-SNQ3TOWP.js";
7
- import {
8
- JOBS_MULTI_TENANT
9
- } from "./chunk-ZPL74UQN.js";
10
7
  import {
11
8
  JobCollisionError,
12
9
  JobNotReplayableError,
@@ -14,6 +11,14 @@ import {
14
11
  JobTypeNotFoundError,
15
12
  MissingTenantIdError
16
13
  } from "./chunk-T4BIIU5E.js";
14
+ import {
15
+ JOBS_MULTI_TENANT
16
+ } from "./chunk-ZPL74UQN.js";
17
+ import {
18
+ FN_KEY_SENTINEL,
19
+ JobKeyFunctionUnavailableError,
20
+ keySelectorToTemplate
21
+ } from "./chunk-7P5ODGLA.js";
17
22
  import {
18
23
  __decorateClass,
19
24
  __decorateParam
@@ -98,8 +103,6 @@ var MemoryJobOrchestrator = class {
98
103
  * unit tests that want to seed the registry without NestJS.
99
104
  */
100
105
  registerHandler(type, meta, handlerClass) {
101
- const concurrencyKeyTemplate = meta.concurrency?.key ?? null;
102
- const dedupeKeyTemplate = meta.dedupe?.key ?? null;
103
106
  const dedupeWindowMs = meta.dedupe?.windowMs ?? null;
104
107
  const now = /* @__PURE__ */ new Date();
105
108
  const def = {
@@ -113,9 +116,13 @@ var MemoryJobOrchestrator = class {
113
116
  baseMs: 0
114
117
  },
115
118
  timeoutMs: meta.timeoutMs ?? null,
116
- concurrencyKeyTemplate: typeof concurrencyKeyTemplate === "string" ? concurrencyKeyTemplate : null,
119
+ concurrencyKeyTemplate: keySelectorToTemplate(
120
+ meta.concurrency?.key
121
+ ),
117
122
  collisionMode: meta.concurrency?.collisionMode ?? "queue",
118
- dedupeKeyTemplate: typeof dedupeKeyTemplate === "string" ? dedupeKeyTemplate : null,
123
+ dedupeKeyTemplate: keySelectorToTemplate(
124
+ meta.dedupe?.key
125
+ ),
119
126
  dedupeWindowMs,
120
127
  priorityDefault: 0,
121
128
  replayFrom: meta.replayFrom ?? "last_checkpoint",
@@ -133,6 +140,26 @@ var MemoryJobOrchestrator = class {
133
140
  getHandlerRegistration(type) {
134
141
  return this.handlerRegistry.get(type);
135
142
  }
143
+ /**
144
+ * JOB-FN-KEY (0.16.2): resolve a persisted key template against the payload.
145
+ * Delegates to the shared `resolveJobKey`, but binds the FUNCTION-sentinel
146
+ * lookup to THIS backend's local `handlerRegistry` (the memory backend keeps
147
+ * its own meta map seeded by `registerHandler`, distinct from the global
148
+ * `JOB_HANDLER_REGISTRY` that the Drizzle/worker path uses — memory tests
149
+ * register handlers directly, never via the `@JobHandler` decorator). The
150
+ * `evaluateTemplate` callback handles the ordinary `{{field}}` path.
151
+ */
152
+ resolveKey(kind, type, template, payload) {
153
+ if (template == null) return null;
154
+ if (template !== FN_KEY_SENTINEL) {
155
+ return evaluateKeyTemplate(template, payload);
156
+ }
157
+ const key = this.handlerRegistry.get(type)?.meta?.[kind]?.key;
158
+ if (typeof key !== "function") {
159
+ throw new JobKeyFunctionUnavailableError(type, kind);
160
+ }
161
+ return key(payload);
162
+ }
136
163
  /**
137
164
  * Boot-time upsert per `IJobOrchestrator.upsertJobRows`. Memory backend
138
165
  * just funnels each entry through `registerHandler`. The validator is
@@ -160,10 +187,7 @@ var MemoryJobOrchestrator = class {
160
187
  const definition = this.store.jobs.get(type);
161
188
  if (!definition) throw new JobTypeNotFoundError(type);
162
189
  if (definition.dedupeKeyTemplate && definition.dedupeWindowMs) {
163
- const dedupeKey2 = evaluateKeyTemplate(
164
- definition.dedupeKeyTemplate,
165
- payload
166
- );
190
+ const dedupeKey2 = this.resolveKey("dedupe", type, definition.dedupeKeyTemplate, payload);
167
191
  const windowStart = Date.now() - definition.dedupeWindowMs;
168
192
  const existing = this.findDedupeCandidate(type, dedupeKey2, windowStart);
169
193
  if (existing) return existing;
@@ -171,7 +195,9 @@ var MemoryJobOrchestrator = class {
171
195
  let concurrencyKey = null;
172
196
  let queueBlockedBy = null;
173
197
  if (definition.concurrencyKeyTemplate) {
174
- concurrencyKey = evaluateKeyTemplate(
198
+ concurrencyKey = this.resolveKey(
199
+ "concurrency",
200
+ type,
175
201
  definition.concurrencyKeyTemplate,
176
202
  payload
177
203
  );
@@ -204,7 +230,12 @@ var MemoryJobOrchestrator = class {
204
230
  }
205
231
  rootRunId = parent.rootRunId;
206
232
  }
207
- const dedupeKey = definition.dedupeKeyTemplate ? evaluateKeyTemplate(definition.dedupeKeyTemplate, payload) : null;
233
+ const dedupeKey = this.resolveKey(
234
+ "dedupe",
235
+ type,
236
+ definition.dedupeKeyTemplate,
237
+ payload
238
+ );
208
239
  const now = /* @__PURE__ */ new Date();
209
240
  const runAt = queueBlockedBy ? QUEUED_RUN_AT : opts.runAt ?? now;
210
241
  const row = {
@@ -632,4 +663,4 @@ function serialiseError(err, attempt, retryable) {
632
663
  export {
633
664
  MemoryJobOrchestrator
634
665
  };
635
- //# sourceMappingURL=chunk-T4YJRD22.js.map
666
+ //# sourceMappingURL=chunk-DUMI2J5M.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../runtime/subsystems/jobs/job-orchestrator.memory-backend.ts"],"sourcesContent":["/**\n * MemoryJobOrchestrator — in-process implementation of `IJobOrchestrator`\n * (ADR-022, JOB-4).\n *\n * Exists solely for the unit test suite: reproduces the Drizzle backend's\n * observable behaviour (claim ordering, collision modes, dedupe collapse,\n * memoization cache, replay row-clearing, cascade cancel) without a\n * database. Not production — the single-process mutex is a substitute for\n * Postgres' `FOR UPDATE SKIP LOCKED`; acceptable non-parity is listed in\n * `docs/specs/JOB-4.md` (fsync, query perf, multi-process claim).\n *\n * The `MemoryJobStore` is shared with `MemoryJobRunService` /\n * `MemoryJobStepService` — all three services mutate the same Maps under\n * the orchestrator's mutex.\n */\nimport { randomUUID } from 'node:crypto';\nimport { Inject, Injectable, Logger, Optional } from '@nestjs/common';\nimport { ModuleRef } from '@nestjs/core';\nimport type {\n JobDefinitionRow,\n JobRunRow,\n} from './job-orchestration.schema';\nimport type {\n CancelOptions,\n IJobOrchestrator,\n JobPoolDef,\n JobRun,\n JobUpsertEntry,\n StartOptions,\n} from './job-orchestrator.protocol';\nimport type {\n JobContext,\n JobHandlerBase,\n JobHandlerMeta,\n JobKeySelector,\n RetryPolicy,\n SpawnChildOptions,\n StepOptions,\n} from './job-handler.base';\nimport {\n ParentClosePolicy,\n keySelectorToTemplate,\n FN_KEY_SENTINEL,\n JobKeyFunctionUnavailableError,\n} from './job-handler.base';\nimport {\n JobCollisionError,\n JobNotReplayableError,\n JobTemplateFieldMissingError,\n JobTypeNotFoundError,\n MissingTenantIdError,\n} from './jobs-errors';\nimport { MemoryJobStore } from './memory-job-store';\nimport { MemoryJobStepService } from './job-step-service.memory-backend';\nimport { JOBS_MULTI_TENANT } from './jobs-domain.tokens';\n\n/**\n * Sentinel `run_at` for runs that lost the `queue` collision — they stay\n * unclaimable until the incumbent transitions terminal and the orchestrator\n * advances their `run_at` back to `now()`. Mirrors the Drizzle backend's\n * `claim-time gate` behaviour without requiring a separate claim query.\n */\nconst QUEUED_RUN_AT = new Date(8_640_000_000_000_000); // \"distant future\"\nconst TERMINAL_STATUSES: JobRunRow['status'][] = [\n 'completed',\n 'failed',\n 'timed_out',\n 'canceled',\n];\nconst DEDUPE_EXCLUDED_STATUSES: JobRunRow['status'][] = ['canceled', 'failed'];\nconst IN_FLIGHT_STATUSES: JobRunRow['status'][] = ['pending', 'running'];\n\nfunction isTerminal(status: JobRunRow['status']): boolean {\n return TERMINAL_STATUSES.includes(status);\n}\n\n/**\n * Mirror of `evaluateKeyTemplate` in the Drizzle backend. Kept private here\n * rather than exported so the memory backend has no dependency on the\n * Drizzle module.\n */\nfunction evaluateKeyTemplate(\n template: string,\n input: Record<string, unknown>,\n): string {\n return template.replace(\n /\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g,\n (_m, field: string) => {\n const value = input[field];\n if (value === undefined || value === null) {\n throw new JobTemplateFieldMissingError(template, field);\n }\n return String(value);\n },\n );\n}\n\n/**\n * Single-promise-chain mutex. Every mutating op on the store goes through\n * `run(...)` so two concurrent `start` calls observe the same sequential\n * consistency Postgres gives us via `FOR UPDATE SKIP LOCKED`. Error\n * swallowing on the chain pointer prevents one failed call from poisoning\n * the queue for subsequent callers.\n *\n * Kept private to this file on purpose — the spec explicitly forbids\n * exporting this; it exists only for the memory backend's internal\n * serialisation.\n */\nclass PromiseMutex {\n private queue: Promise<void> = Promise.resolve();\n\n async run<T>(fn: () => Promise<T>): Promise<T> {\n const next = this.queue.then(() => fn());\n // Swallow errors on the chain pointer so a throwing `fn` doesn't\n // permanently reject every future caller.\n this.queue = next.then(\n () => undefined,\n () => undefined,\n );\n return next;\n }\n}\n\n/** Handler registry entry — class + frozen metadata. */\ninterface HandlerRegistration {\n type: string;\n meta: JobHandlerMeta<unknown>;\n handlerClass: new (...args: unknown[]) => JobHandlerBase<unknown>;\n}\n\n@Injectable()\nexport class MemoryJobOrchestrator implements IJobOrchestrator {\n private readonly logger = new Logger(MemoryJobOrchestrator.name);\n private readonly mutex = new PromiseMutex();\n private readonly handlerRegistry = new Map<string, HandlerRegistration>();\n\n /**\n * `runId → dependent runId[]` — when a run with `concurrencyKey = K`\n * blocks on an incumbent, its id is added here under the incumbent's id.\n * On incumbent terminal transition we advance every dependent's `runAt`\n * back to `now()` so it becomes claimable.\n */\n private readonly queueBlockers = new Map<string, string[]>();\n\n constructor(\n // ADR-037 (package-mode DI): explicit `@Inject` tokens on every param —\n // the published bundle has no `design:paramtypes` metadata (built without\n // `emitDecoratorMetadata`), so by-type injection would resolve to\n // `undefined` in package mode. Class tokens (`MemoryJobStore`,\n // `MemoryJobStepService`, `ModuleRef`) are passed to `@Inject` explicitly.\n @Inject(MemoryJobStore) private readonly store: MemoryJobStore,\n @Inject(MemoryJobStepService) private readonly stepService: MemoryJobStepService,\n @Inject(JOBS_MULTI_TENANT) private readonly multiTenant: boolean,\n @Optional() @Inject(ModuleRef) private readonly moduleRef?: ModuleRef,\n ) {}\n\n /**\n * JOB-8 — mirror of the Drizzle backend's `resolveTenantId`. Returns the\n * value to stamp on `tenant_id` / compare against in memory predicates.\n * Off → always `null`. On + `undefined` → throw. On + `null`/string → pass.\n */\n private resolveTenantId(\n method: string,\n tenantId: string | null | undefined,\n ): string | null {\n if (!this.multiTenant) return null;\n if (tenantId === undefined) throw new MissingTenantIdError(method);\n return tenantId;\n }\n\n // ==========================================================================\n // registerHandler — replaces Drizzle's `job` table upsert\n // ==========================================================================\n\n /**\n * Populate the in-memory job definition row plus handler class lookup.\n * Called by `JobWorkerModule.onModuleInit` in memory mode, or directly by\n * unit tests that want to seed the registry without NestJS.\n */\n registerHandler<TInput>(\n type: string,\n meta: JobHandlerMeta<TInput>,\n handlerClass: new (...args: unknown[]) => JobHandlerBase<TInput>,\n ): void {\n // JOB-FN-KEY (0.16.2): mirror the Drizzle backend — collapse a function\n // key to `FN_KEY_SENTINEL` so the def row stays non-null (collision/dedupe\n // path engages); `start()` re-resolves the live fn from `handlerRegistry`.\n const dedupeWindowMs = meta.dedupe?.windowMs ?? null;\n const now = new Date();\n\n const def: JobDefinitionRow = {\n type,\n version: 1,\n pool: meta.pool ?? 'batch',\n scopeEntityType: meta.scope?.entity ?? null,\n retryPolicy: meta.retry ?? {\n attempts: 1,\n backoff: 'fixed',\n baseMs: 0,\n },\n timeoutMs: meta.timeoutMs ?? null,\n concurrencyKeyTemplate: keySelectorToTemplate(\n meta.concurrency?.key as JobKeySelector<unknown> | undefined,\n ),\n collisionMode:\n (meta.concurrency?.collisionMode as JobDefinitionRow['collisionMode']) ??\n 'queue',\n dedupeKeyTemplate: keySelectorToTemplate(\n meta.dedupe?.key as JobKeySelector<unknown> | undefined,\n ),\n dedupeWindowMs,\n priorityDefault: 0,\n replayFrom: meta.replayFrom ?? 'last_checkpoint',\n createdAt: now,\n updatedAt: now,\n };\n\n this.store.jobs.set(type, def);\n this.handlerRegistry.set(type, {\n type,\n meta: meta as JobHandlerMeta<unknown>,\n handlerClass: handlerClass as unknown as new (\n ...args: unknown[]\n ) => JobHandlerBase<unknown>,\n });\n }\n\n /** Test helper — look up a registered handler without exposing the map. */\n getHandlerRegistration(type: string): HandlerRegistration | undefined {\n return this.handlerRegistry.get(type);\n }\n\n /**\n * JOB-FN-KEY (0.16.2): resolve a persisted key template against the payload.\n * Delegates to the shared `resolveJobKey`, but binds the FUNCTION-sentinel\n * lookup to THIS backend's local `handlerRegistry` (the memory backend keeps\n * its own meta map seeded by `registerHandler`, distinct from the global\n * `JOB_HANDLER_REGISTRY` that the Drizzle/worker path uses — memory tests\n * register handlers directly, never via the `@JobHandler` decorator). The\n * `evaluateTemplate` callback handles the ordinary `{{field}}` path.\n */\n private resolveKey(\n kind: 'concurrency' | 'dedupe',\n type: string,\n template: string | null,\n payload: Record<string, unknown>,\n ): string | null {\n if (template == null) return null;\n if (template !== FN_KEY_SENTINEL) {\n return evaluateKeyTemplate(template, payload);\n }\n const key = (this.handlerRegistry.get(type)?.meta?.[kind] as\n | { key?: unknown }\n | undefined)?.key;\n if (typeof key !== 'function') {\n throw new JobKeyFunctionUnavailableError(type, kind);\n }\n return (key as (input: unknown) => string)(payload);\n }\n\n /**\n * Boot-time upsert per `IJobOrchestrator.upsertJobRows`. Memory backend\n * just funnels each entry through `registerHandler`. The validator is\n * skipped entirely in memory mode (Q4 resolution 2026-04-19), so the\n * orphaned list is always empty — there are no DB rows to compare against.\n */\n async upsertJobRows(\n entries: JobUpsertEntry[],\n poolConfig: ReadonlyMap<string, JobPoolDef>,\n ): Promise<{ orphaned: string[] }> {\n void poolConfig; // pool validation is the module's responsibility\n for (const entry of entries) {\n this.registerHandler(\n entry.type,\n entry.meta as JobHandlerMeta<unknown>,\n entry.handlerClass as new (...args: unknown[]) => JobHandlerBase<unknown>,\n );\n }\n return { orphaned: [] };\n }\n\n // ==========================================================================\n // start\n // ==========================================================================\n\n async start(\n type: string,\n input: unknown,\n opts: StartOptions = {},\n // BRIDGE-7: signature parity with Drizzle backend. The memory backend\n // has no real transactions (its \"atomic\" boundary is a process-wide\n // mutex acquired by the body below), so the parameter is intentionally\n // ignored. Accepting it lets EventFlowService unit tests exercise the\n // same code path without two stub orchestrators.\n _tx?: unknown,\n ): Promise<JobRun> {\n // JOB-8 — resolve tenant gate outside the mutex so the error throws\n // synchronously-ish from the caller's stack rather than via the mutex's\n // deferred chain (matches Drizzle backend's pre-transaction guard).\n const tenantId = this.resolveTenantId('start', opts.tenantId);\n\n return this.mutex.run(async () => {\n const payload = (input ?? {}) as Record<string, unknown>;\n const definition = this.store.jobs.get(type);\n if (!definition) throw new JobTypeNotFoundError(type);\n\n // 1. Dedupe — return existing non-excluded run within the window.\n // JOB-FN-KEY: `resolveKey` honors the `{{field}}` template AND a function\n // key persisted as `FN_KEY_SENTINEL` (re-resolved from `handlerRegistry`).\n if (definition.dedupeKeyTemplate && definition.dedupeWindowMs) {\n const dedupeKey = this.resolveKey('dedupe', type, definition.dedupeKeyTemplate, payload) as string;\n const windowStart = Date.now() - definition.dedupeWindowMs;\n const existing = this.findDedupeCandidate(type, dedupeKey, windowStart);\n if (existing) return existing;\n }\n\n // 2. Concurrency collision check.\n let concurrencyKey: string | null = null;\n let queueBlockedBy: string | null = null;\n if (definition.concurrencyKeyTemplate) {\n // Non-null cast: the branch guard proves the template is present.\n concurrencyKey = this.resolveKey(\n 'concurrency',\n type,\n definition.concurrencyKeyTemplate,\n payload,\n ) as string;\n const incumbent = this.findInFlightByConcurrencyKey(concurrencyKey);\n if (incumbent) {\n switch (definition.collisionMode) {\n case 'reject':\n throw new JobCollisionError(type, concurrencyKey, incumbent);\n case 'replace':\n // Cancel incumbent (cascading children). Must happen inside\n // the mutex — call the internal helper, not public `cancel()`\n // (public `cancel` would re-enter the mutex and deadlock).\n // Internal replace path sidesteps the tenant gate — it uses\n // the incumbent's own tenant (same concurrency key implies\n // same tenant in practice, but the gate is bypassed via\n // `incumbent.tenantId` to avoid accidental cross-tenant\n // MissingTenantIdError bubbling from the user's `start` call).\n this.cancelLocked(\n incumbent.id,\n { cascade: true, reason: 'replaced' },\n incumbent.tenantId,\n );\n break;\n case 'queue':\n queueBlockedBy = incumbent.id;\n break;\n }\n }\n }\n\n // 3. Resolve lineage.\n const newId = randomUUID();\n let rootRunId: string = newId;\n if (opts.parentRunId) {\n const parent = this.store.runs.get(opts.parentRunId);\n if (!parent) {\n throw new Error(\n `parentRunId ${opts.parentRunId} does not reference an existing job_run`,\n );\n }\n rootRunId = parent.rootRunId;\n }\n\n // 4. Compute dedupe key for the persisted row (separate from dedupe\n // short-circuit above — we store it even when no prior run matched\n // so future dedupe checks see it).\n const dedupeKey = this.resolveKey(\n 'dedupe',\n type,\n definition.dedupeKeyTemplate,\n payload,\n );\n\n const now = new Date();\n const runAt = queueBlockedBy\n ? QUEUED_RUN_AT\n : (opts.runAt ?? now);\n\n const row: JobRunRow = {\n id: newId,\n jobType: type,\n jobVersion: definition.version,\n parentRunId: opts.parentRunId ?? null,\n rootRunId,\n parentClosePolicy: opts.parentClosePolicy ?? 'terminate',\n scopeEntityType: opts.scope?.entityType ?? null,\n scopeEntityId: opts.scope?.entityId ?? null,\n tenantId,\n tags: opts.tags ?? {},\n pool: opts.pool ?? definition.pool,\n priority: opts.priority ?? definition.priorityDefault,\n concurrencyKey,\n dedupeKey,\n status: 'pending',\n input: payload,\n output: null,\n error: null,\n triggerSource: opts.triggerSource ?? 'manual',\n triggerRef: opts.triggerRef ?? null,\n runAt,\n startedAt: null,\n finishedAt: null,\n claimedAt: null,\n attempts: 0,\n waitKind: null,\n resumeToken: null,\n waitDeadline: null,\n createdAt: now,\n updatedAt: now,\n };\n\n this.store.runs.set(newId, row);\n if (queueBlockedBy) {\n const list = this.queueBlockers.get(queueBlockedBy) ?? [];\n list.push(newId);\n this.queueBlockers.set(queueBlockedBy, list);\n }\n return row;\n });\n }\n\n // ==========================================================================\n // cancel\n // ==========================================================================\n\n async cancel(runId: string, opts: CancelOptions = {}): Promise<void> {\n // JOB-8 — strict tenant gate outside the mutex (matches Drizzle path).\n const tenantId = this.resolveTenantId('cancel', opts.tenantId);\n await this.mutex.run(async () => {\n this.cancelLocked(runId, opts, tenantId);\n });\n }\n\n /**\n * Internal cancel that assumes the caller already holds the mutex.\n * Synchronous because all store ops are in-memory. Idempotent.\n *\n * `tenantForGate` is the already-validated tenant id (or `null`). When\n * non-null it gates the initial cancellation to that tenant's run; the\n * cascade step then sweeps descendants on the same `rootRunId` without\n * re-checking — children of a tenant-gated parent always share the\n * tenant (enforced at `start` time).\n */\n private cancelLocked(\n runId: string,\n opts: CancelOptions,\n tenantForGate: string | null,\n ): void {\n const run = this.store.runs.get(runId);\n if (!run) return;\n // JOB-8 — cross-tenant cancel is silent no-op.\n if (this.multiTenant && run.tenantId !== tenantForGate) return;\n if (isTerminal(run.status)) return;\n\n const now = new Date();\n\n // Collect descendants up front so Cancel-policy parents can wait on\n // children (their `finished_at` is set after children transition).\n const descendants =\n opts.cascade === false\n ? []\n : Array.from(this.store.runs.values()).filter(\n (r) =>\n r.rootRunId === run.rootRunId &&\n r.id !== runId &&\n !isTerminal(r.status),\n );\n\n // Group by policy stored on the child.\n const terminateChildren = descendants.filter(\n (d) => d.parentClosePolicy === ParentClosePolicy.Terminate,\n );\n const cancelChildren = descendants.filter(\n (d) => d.parentClosePolicy === ParentClosePolicy.Cancel,\n );\n // 'abandon' → do nothing.\n\n // Terminate policy: cancel children, then parent.\n for (const child of terminateChildren) {\n this.transitionToCanceled(child.id, now);\n }\n\n // Cancel policy: cancel children first, then parent (so parent's\n // finished_at is set only after children transitioned).\n for (const child of cancelChildren) {\n this.transitionToCanceled(child.id, now);\n }\n\n this.transitionToCanceled(runId, now);\n\n void opts.reason; // reserved for future audit logging\n }\n\n private transitionToCanceled(runId: string, at: Date): void {\n const run = this.store.runs.get(runId);\n if (!run) return;\n if (isTerminal(run.status)) return;\n const next: JobRunRow = {\n ...run,\n status: 'canceled',\n finishedAt: at,\n updatedAt: at,\n };\n this.store.runs.set(runId, next);\n this.unblockQueuedDependents(runId);\n }\n\n /**\n * When `runId` transitions to a terminal state, advance every dependent\n * `queue`-blocked run's `run_at` back to `now()` so `claimNext` picks\n * them up.\n */\n private unblockQueuedDependents(runId: string): void {\n const dependents = this.queueBlockers.get(runId);\n if (!dependents || dependents.length === 0) return;\n const now = new Date();\n for (const dep of dependents) {\n const depRun = this.store.runs.get(dep);\n if (!depRun) continue;\n if (depRun.status !== 'pending') continue;\n this.store.runs.set(dep, { ...depRun, runAt: now, updatedAt: now });\n }\n this.queueBlockers.delete(runId);\n }\n\n // ==========================================================================\n // claimNext — consumed by JobWorker in memory mode (tests exercise directly)\n // ==========================================================================\n\n async claimNext(pool: string): Promise<JobRunRow | null> {\n return this.mutex.run(async () => {\n const now = Date.now();\n const candidates = Array.from(this.store.runs.values()).filter(\n (r) =>\n r.status === 'pending' &&\n r.pool === pool &&\n r.runAt.getTime() <= now,\n );\n if (candidates.length === 0) return null;\n\n // ORDER BY priority DESC, run_at ASC (Drizzle parity).\n candidates.sort((a, b) => {\n if (a.priority !== b.priority) return b.priority - a.priority;\n return a.runAt.getTime() - b.runAt.getTime();\n });\n\n const winner = candidates[0]!;\n const claimedAt = new Date();\n const next: JobRunRow = {\n ...winner,\n status: 'running',\n claimedAt,\n startedAt: claimedAt,\n updatedAt: claimedAt,\n };\n this.store.runs.set(winner.id, next);\n return next;\n });\n }\n\n // ==========================================================================\n // replay\n // ==========================================================================\n\n async replay(runId: string): Promise<JobRun> {\n return this.mutex.run(async () => {\n const run = this.store.runs.get(runId);\n if (!run) throw new Error(`replay: run ${runId} not found`);\n if (!isTerminal(run.status)) {\n throw new JobNotReplayableError(runId, run.status);\n }\n const def = this.store.jobs.get(run.jobType);\n if (!def) throw new JobTypeNotFoundError(run.jobType);\n\n const mode = def.replayFrom;\n if (mode === 'scratch') {\n this.stepService.clearStepsForRun(runId);\n } else {\n // `last_step` and `last_checkpoint` collapse to the same semantic\n // in Phase 1 — delete non-completed rows, preserve memoized ones.\n // Matches the Drizzle backend exactly (see JOB-3 notes).\n this.stepService.clearIncompleteSteps(runId);\n }\n\n const now = new Date();\n const next: JobRunRow = {\n ...run,\n status: 'pending',\n attempts: 0,\n runAt: now,\n startedAt: null,\n finishedAt: null,\n claimedAt: null,\n error: null,\n output: null,\n updatedAt: now,\n };\n this.store.runs.set(runId, next);\n return next;\n });\n }\n\n // ==========================================================================\n // tick — used by unit tests + memory-mode JobWorker\n // ==========================================================================\n\n /**\n * Execute a single claimed run to completion, retry, or failure. Not on\n * `IJobOrchestrator` — it's the memory equivalent of the Drizzle\n * `JobWorker.processRun` code path. The unit tests drive it directly so\n * they can assert memoization across ticks without spinning up a worker.\n */\n async tick(runId: string): Promise<void> {\n // We load state outside the mutex because handler execution cannot\n // hold the serialisation lock — `fn()` inside `ctx.step` can call back\n // into `start` / `spawnChild` which would deadlock. Mutation points\n // (recordStep, status transition) go through the services or the\n // orchestrator entry points and re-enter the mutex there.\n const run = this.store.runs.get(runId);\n if (!run) throw new Error(`tick: run ${runId} not found`);\n if (run.status !== 'running') {\n throw new Error(\n `tick: run ${runId} must be 'running' (got '${run.status}')`,\n );\n }\n\n const registration = this.handlerRegistry.get(run.jobType);\n if (!registration) {\n await this.markFailed(run, new Error(\n `No handler registered for jobType='${run.jobType}'`,\n ), (run.attempts ?? 0) + 1);\n return;\n }\n const meta = registration.meta;\n const HandlerClass = registration.handlerClass;\n // Match the Drizzle backend: resolve the handler through Nest's\n // ModuleRef so `@Inject` constructor params work. ModuleRef is\n // @Optional() — zero-dep test stubs that construct this orchestrator\n // manually still hit the legacy `new HandlerClass()` path.\n // `get({ strict: false })` (not `create()`) — the handler must be a\n // provider in its owning module so cross-module @Inject dependencies\n // resolve. See job-worker.ts for the full rationale.\n const handler = this.moduleRef\n ? (this.moduleRef.get(\n HandlerClass as unknown as new (...args: unknown[]) => unknown,\n { strict: false },\n ) as JobHandlerBase<unknown>)\n : new HandlerClass();\n\n const ctx: JobContext<unknown> = {\n input: run.input,\n run: run as JobRun,\n step: this.makeStepFn(run),\n spawnChild: this.makeSpawnFn(run),\n logger: new Logger(`JobRun:${run.id}`),\n };\n\n const attemptsBefore = run.attempts ?? 0;\n try {\n const output = (await handler.run(ctx)) as Record<string, unknown> | undefined;\n await this.markCompleted(run, output ?? {}, attemptsBefore + 1);\n } catch (err) {\n const policy = meta.retry;\n const decision = classifyError(err, policy, attemptsBefore);\n const nextAttempts = attemptsBefore + 1;\n if (decision === 'retry' && policy) {\n const delay = computeBackoff(policy, nextAttempts);\n await this.rescheduleForRetry(run, err, nextAttempts, delay);\n } else {\n await this.markFailed(run, err, nextAttempts);\n }\n }\n }\n\n private makeStepFn(run: JobRunRow) {\n return async <TOutput>(\n stepId: string,\n fn: () => Promise<TOutput>,\n _opts?: StepOptions,\n ): Promise<TOutput> => {\n void _opts;\n const existing = await this.stepService.findStep(run.id, stepId);\n if (existing?.status === 'completed') {\n return existing.output as TOutput;\n }\n const seq = this.nextStepSeq(run.id);\n const startedAt = new Date();\n const nextAttempts = (existing?.attempts ?? 0) + 1;\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'running',\n startedAt,\n attempts: nextAttempts,\n });\n try {\n const output = await fn();\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'completed',\n output: output as Record<string, unknown> | undefined,\n finishedAt: new Date(),\n attempts: nextAttempts,\n });\n return output;\n } catch (err) {\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'failed',\n error: serialiseError(err, nextAttempts, false),\n finishedAt: new Date(),\n attempts: nextAttempts,\n });\n throw err;\n }\n };\n }\n\n private makeSpawnFn(run: JobRunRow) {\n return async (\n type: string,\n input: unknown,\n opts?: SpawnChildOptions,\n ): Promise<JobRun> => {\n return this.start(type, input, {\n parentRunId: run.id,\n parentClosePolicy: opts?.closePolicy,\n runAt: opts?.runAt,\n priority: opts?.priority,\n tags: opts?.tags,\n triggerSource: 'parent',\n triggerRef: run.id,\n });\n };\n }\n\n private nextStepSeq(runId: string): number {\n const rows = this.store.steps.get(runId);\n if (!rows || rows.length === 0) return 1;\n let max = 0;\n for (const r of rows) if (r.seq > max) max = r.seq;\n return max + 1;\n }\n\n private async markCompleted(\n run: JobRunRow,\n output: Record<string, unknown>,\n attempts: number,\n ): Promise<void> {\n await this.mutex.run(async () => {\n const current = this.store.runs.get(run.id);\n if (!current || isTerminal(current.status)) return;\n const now = new Date();\n this.store.runs.set(run.id, {\n ...current,\n status: 'completed',\n output,\n finishedAt: now,\n updatedAt: now,\n attempts,\n });\n this.unblockQueuedDependents(run.id);\n });\n }\n\n private async markFailed(\n run: JobRunRow,\n err: unknown,\n attempts: number,\n ): Promise<void> {\n await this.mutex.run(async () => {\n const current = this.store.runs.get(run.id);\n if (!current || isTerminal(current.status)) return;\n const now = new Date();\n this.store.runs.set(run.id, {\n ...current,\n status: 'failed',\n finishedAt: now,\n updatedAt: now,\n attempts,\n error: serialiseError(err, attempts, false),\n });\n this.unblockQueuedDependents(run.id);\n });\n\n // parent_close_policy = 'terminate' cascade mirrors the Drizzle worker\n // (cancel runs outside its own terminal transition). We pass the run's\n // own `tenantId` so the cancel passes the multi-tenant gate — this is\n // system-internal cascade, not a user-initiated call.\n if (run.parentClosePolicy === 'terminate') {\n try {\n await this.cancel(run.id, {\n cascade: true,\n reason: 'parent-failed',\n tenantId: run.tenantId,\n });\n } catch (cascadeErr) {\n this.logger.warn(\n `cascade on failed run ${run.id}: ${(cascadeErr as Error).message}`,\n );\n }\n }\n }\n\n private async rescheduleForRetry(\n run: JobRunRow,\n err: unknown,\n attempts: number,\n delayMs: number,\n ): Promise<void> {\n await this.mutex.run(async () => {\n const current = this.store.runs.get(run.id);\n if (!current || isTerminal(current.status)) return;\n const now = new Date();\n this.store.runs.set(run.id, {\n ...current,\n status: 'pending',\n attempts,\n runAt: new Date(Date.now() + delayMs),\n startedAt: null,\n claimedAt: null,\n updatedAt: now,\n error: serialiseError(err, attempts, true),\n });\n });\n }\n\n // ==========================================================================\n // Internal queries — used by start / cancel\n // ==========================================================================\n\n private findDedupeCandidate(\n jobType: string,\n dedupeKey: string,\n windowStartMs: number,\n ): JobRunRow | null {\n let best: JobRunRow | null = null;\n for (const r of this.store.runs.values()) {\n if (r.jobType !== jobType) continue;\n if (r.dedupeKey !== dedupeKey) continue;\n if (DEDUPE_EXCLUDED_STATUSES.includes(r.status)) continue;\n if (r.createdAt.getTime() <= windowStartMs) continue;\n if (!best || r.createdAt.getTime() > best.createdAt.getTime()) {\n best = r;\n }\n }\n return best;\n }\n\n private findInFlightByConcurrencyKey(key: string): JobRunRow | null {\n for (const r of this.store.runs.values()) {\n if (r.concurrencyKey !== key) continue;\n if (!IN_FLIGHT_STATUSES.includes(r.status)) continue;\n return r;\n }\n return null;\n }\n}\n\n// ─── Pure helpers (mirrored from JobWorker so memory mode is standalone) ────\n\nfunction classifyError(\n err: unknown,\n policy: RetryPolicy | undefined,\n currentAttempts: number,\n): 'retry' | 'fail' {\n if (!policy) return 'fail';\n const errObj = err as { name?: string; code?: string } | undefined;\n const name = errObj?.name;\n const code = errObj?.code;\n const nonRetryable = policy.nonRetryableErrors ?? [];\n if (nonRetryable.some((n) => n === name || n === code)) return 'fail';\n if (currentAttempts + 1 >= policy.attempts) return 'fail';\n return 'retry';\n}\n\nfunction computeBackoff(policy: RetryPolicy, attempts: number): number {\n const base = Math.max(policy.baseMs, 0);\n if (policy.backoff === 'fixed') return base;\n const exponent = Math.max(attempts - 1, 0);\n if (exponent >= 53) return Number.MAX_SAFE_INTEGER;\n const raw = base * Math.pow(2, exponent);\n if (!Number.isFinite(raw) || raw >= Number.MAX_SAFE_INTEGER) {\n return Number.MAX_SAFE_INTEGER;\n }\n return raw;\n}\n\nfunction serialiseError(err: unknown, attempt: number, retryable: boolean) {\n const e = err as { message?: string; stack?: string } | undefined;\n return {\n message: (e?.message ?? String(err)) as string,\n stack: e?.stack,\n retryable,\n attempt,\n };\n}\n"],"mappings":"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","names":["dedupeKey"]}
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-GM3RMJIJ.js";
4
4
  import {
5
5
  DrizzleEventBus
6
- } from "./chunk-H6FO2ZDJ.js";
6
+ } from "./chunk-4PFF3ED4.js";
7
7
  import {
8
8
  MemoryEventBus
9
9
  } from "./chunk-LQ6PYFU6.js";
@@ -152,4 +152,4 @@ EventsModule = __decorateClass([
152
152
  export {
153
153
  EventsModule
154
154
  };
155
- //# sourceMappingURL=chunk-TKVTEUBD.js.map
155
+ //# sourceMappingURL=chunk-EJBK7I4F.js.map