@pattern-stack/codegen 0.4.4 → 0.4.5
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.
- package/dist/runtime/subsystems/index.d.ts +7 -0
- package/dist/runtime/subsystems/index.js +905 -208
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/observability/index.d.ts +10 -0
- package/dist/runtime/subsystems/observability/index.js +895 -0
- package/dist/runtime/subsystems/observability/index.js.map +1 -0
- package/dist/runtime/subsystems/observability/observability.drizzle-backend.d.ts +15 -0
- package/dist/runtime/subsystems/observability/observability.drizzle-backend.js +465 -0
- package/dist/runtime/subsystems/observability/observability.drizzle-backend.js.map +1 -0
- package/dist/runtime/subsystems/observability/observability.memory-backend.d.ts +28 -0
- package/dist/runtime/subsystems/observability/observability.memory-backend.js +75 -0
- package/dist/runtime/subsystems/observability/observability.memory-backend.js.map +1 -0
- package/dist/runtime/subsystems/observability/observability.module.d.ts +56 -0
- package/dist/runtime/subsystems/observability/observability.module.js +887 -0
- package/dist/runtime/subsystems/observability/observability.module.js.map +1 -0
- package/dist/runtime/subsystems/observability/observability.protocol.d.ts +155 -0
- package/dist/runtime/subsystems/observability/observability.protocol.js +1 -0
- package/dist/runtime/subsystems/observability/observability.protocol.js.map +1 -0
- package/dist/runtime/subsystems/observability/observability.tokens.d.ts +19 -0
- package/dist/runtime/subsystems/observability/observability.tokens.js +8 -0
- package/dist/runtime/subsystems/observability/observability.tokens.js.map +1 -0
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +79 -0
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.js +425 -0
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.js.map +1 -0
- package/dist/runtime/subsystems/sync/sync-audit.schema.d.ts +4 -4
- package/package.json +6 -1
- package/runtime/subsystems/index.ts +23 -0
- package/runtime/subsystems/observability/index.ts +35 -0
- package/runtime/subsystems/observability/observability.drizzle-backend.ts +223 -0
- package/runtime/subsystems/observability/observability.memory-backend.ts +111 -0
- package/runtime/subsystems/observability/observability.module.ts +115 -0
- package/runtime/subsystems/observability/observability.protocol.ts +167 -0
- package/runtime/subsystems/observability/observability.tokens.ts +18 -0
- package/runtime/subsystems/observability/reporters/bridge-metrics.reporter.ts +222 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
4
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
5
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
6
|
+
if (decorator = decorators[i])
|
|
7
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
8
|
+
if (kind && result) __defProp(target, key, result);
|
|
9
|
+
return result;
|
|
10
|
+
};
|
|
11
|
+
var __decorateParam = (index4, decorator) => (target, key) => decorator(target, key, index4);
|
|
12
|
+
|
|
13
|
+
// runtime/subsystems/observability/reporters/bridge-metrics.reporter.ts
|
|
14
|
+
import {
|
|
15
|
+
Inject,
|
|
16
|
+
Injectable,
|
|
17
|
+
Logger
|
|
18
|
+
} from "@nestjs/common";
|
|
19
|
+
import { and, eq, gt, sql as sql3 } from "drizzle-orm";
|
|
20
|
+
|
|
21
|
+
// runtime/constants/tokens.ts
|
|
22
|
+
var DRIZZLE = "DRIZZLE";
|
|
23
|
+
|
|
24
|
+
// runtime/subsystems/bridge/bridge-delivery.schema.ts
|
|
25
|
+
import {
|
|
26
|
+
index as index3,
|
|
27
|
+
jsonb as jsonb3,
|
|
28
|
+
pgEnum as pgEnum2,
|
|
29
|
+
pgTable as pgTable3,
|
|
30
|
+
text as text3,
|
|
31
|
+
timestamp as timestamp3,
|
|
32
|
+
unique,
|
|
33
|
+
uuid as uuid3
|
|
34
|
+
} from "drizzle-orm/pg-core";
|
|
35
|
+
import { sql as sql2 } from "drizzle-orm";
|
|
36
|
+
|
|
37
|
+
// runtime/subsystems/events/domain-events.schema.ts
|
|
38
|
+
import {
|
|
39
|
+
index,
|
|
40
|
+
jsonb,
|
|
41
|
+
pgTable,
|
|
42
|
+
text,
|
|
43
|
+
timestamp,
|
|
44
|
+
uuid
|
|
45
|
+
} from "drizzle-orm/pg-core";
|
|
46
|
+
var domainEvents = pgTable(
|
|
47
|
+
"domain_events",
|
|
48
|
+
{
|
|
49
|
+
id: uuid("id").primaryKey(),
|
|
50
|
+
type: text("type").notNull(),
|
|
51
|
+
aggregateId: text("aggregate_id").notNull(),
|
|
52
|
+
aggregateType: text("aggregate_type").notNull(),
|
|
53
|
+
payload: jsonb("payload").notNull().$type(),
|
|
54
|
+
occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
|
|
55
|
+
processedAt: timestamp("processed_at", { withTimezone: true }),
|
|
56
|
+
/** Lifecycle status: pending | processed | failed */
|
|
57
|
+
status: text("status").notNull().default("pending"),
|
|
58
|
+
/** Error message from the last failed dispatch attempt. */
|
|
59
|
+
error: text("error"),
|
|
60
|
+
metadata: jsonb("metadata").$type(),
|
|
61
|
+
/** Routing pool (e.g. `events_inbound`, `events_change`, `events_outbound`). Populated by DrizzleEventBus.publish() in EVT-4. */
|
|
62
|
+
pool: text("pool"),
|
|
63
|
+
/** Routing direction: `inbound` | `change` | `outbound`. Populated by DrizzleEventBus.publish() in EVT-4. */
|
|
64
|
+
direction: text("direction"),
|
|
65
|
+
// conditional: emitted only when events.multi_tenant: true
|
|
66
|
+
tenantId: text("tenant_id")
|
|
67
|
+
},
|
|
68
|
+
(t) => ({
|
|
69
|
+
/** Polling drain filter (existing — promoted from comment to declaration in EVT-1). */
|
|
70
|
+
idxDomainEventsStatusOccurredAt: index("idx_domain_events_status_occurred_at").on(
|
|
71
|
+
t.status,
|
|
72
|
+
t.occurredAt
|
|
73
|
+
),
|
|
74
|
+
/** Event replay per aggregate (existing — promoted from comment to declaration in EVT-1). */
|
|
75
|
+
idxDomainEventsAggregate: index("idx_domain_events_aggregate").on(
|
|
76
|
+
t.aggregateId,
|
|
77
|
+
t.aggregateType
|
|
78
|
+
),
|
|
79
|
+
/** Per-pool drain filter (EVT-1). Enables DrizzleEventBus to drain a single pool without scanning all events. */
|
|
80
|
+
idxDomainEventsPoolStatusOccurredAt: index(
|
|
81
|
+
"idx_domain_events_pool_status_occurred_at"
|
|
82
|
+
).on(t.pool, t.status, t.occurredAt)
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// runtime/subsystems/jobs/job-orchestration.schema.ts
|
|
87
|
+
import {
|
|
88
|
+
pgEnum,
|
|
89
|
+
pgTable as pgTable2,
|
|
90
|
+
uuid as uuid2,
|
|
91
|
+
text as text2,
|
|
92
|
+
jsonb as jsonb2,
|
|
93
|
+
integer,
|
|
94
|
+
timestamp as timestamp2,
|
|
95
|
+
index as index2,
|
|
96
|
+
uniqueIndex
|
|
97
|
+
} from "drizzle-orm/pg-core";
|
|
98
|
+
import { sql } from "drizzle-orm";
|
|
99
|
+
var jobRunStatusEnum = pgEnum("job_run_status", [
|
|
100
|
+
"pending",
|
|
101
|
+
"running",
|
|
102
|
+
"waiting",
|
|
103
|
+
"completed",
|
|
104
|
+
"failed",
|
|
105
|
+
"timed_out",
|
|
106
|
+
"canceled"
|
|
107
|
+
]);
|
|
108
|
+
var jobStepKindEnum = pgEnum("job_step_kind", ["task"]);
|
|
109
|
+
var jobStepStatusEnum = pgEnum("job_step_status", [
|
|
110
|
+
"pending",
|
|
111
|
+
"running",
|
|
112
|
+
"completed",
|
|
113
|
+
"failed",
|
|
114
|
+
"skipped"
|
|
115
|
+
]);
|
|
116
|
+
var collisionModeEnum = pgEnum("job_collision_mode", [
|
|
117
|
+
"queue",
|
|
118
|
+
"reject",
|
|
119
|
+
"replace"
|
|
120
|
+
]);
|
|
121
|
+
var replayFromEnum = pgEnum("job_replay_from", [
|
|
122
|
+
"scratch",
|
|
123
|
+
"last_step",
|
|
124
|
+
"last_checkpoint"
|
|
125
|
+
]);
|
|
126
|
+
var parentClosePolicyEnum = pgEnum("job_parent_close_policy", [
|
|
127
|
+
"terminate",
|
|
128
|
+
"cancel",
|
|
129
|
+
"abandon"
|
|
130
|
+
]);
|
|
131
|
+
var waitKindEnum = pgEnum("job_wait_kind", ["signal"]);
|
|
132
|
+
var triggerSourceEnum = pgEnum("job_trigger_source", [
|
|
133
|
+
"manual",
|
|
134
|
+
"schedule",
|
|
135
|
+
"event",
|
|
136
|
+
"parent"
|
|
137
|
+
]);
|
|
138
|
+
var jobs = pgTable2("job", {
|
|
139
|
+
type: text2("type").primaryKey(),
|
|
140
|
+
version: integer("version").notNull().default(1),
|
|
141
|
+
pool: text2("pool").notNull(),
|
|
142
|
+
scopeEntityType: text2("scope_entity_type"),
|
|
143
|
+
retryPolicy: jsonb2("retry_policy").notNull().$type(),
|
|
144
|
+
timeoutMs: integer("timeout_ms"),
|
|
145
|
+
concurrencyKeyTemplate: text2("concurrency_key_template"),
|
|
146
|
+
collisionMode: collisionModeEnum("collision_mode").notNull().default("queue"),
|
|
147
|
+
dedupeKeyTemplate: text2("dedupe_key_template"),
|
|
148
|
+
dedupeWindowMs: integer("dedupe_window_ms"),
|
|
149
|
+
priorityDefault: integer("priority_default").notNull().default(0),
|
|
150
|
+
replayFrom: replayFromEnum("replay_from").notNull().default("last_checkpoint"),
|
|
151
|
+
createdAt: timestamp2("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
152
|
+
updatedAt: timestamp2("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
153
|
+
});
|
|
154
|
+
var jobRuns = pgTable2(
|
|
155
|
+
"job_run",
|
|
156
|
+
{
|
|
157
|
+
id: uuid2("id").primaryKey().defaultRandom(),
|
|
158
|
+
jobType: text2("job_type").notNull().references(() => jobs.type),
|
|
159
|
+
jobVersion: integer("job_version").notNull(),
|
|
160
|
+
parentRunId: uuid2("parent_run_id").references(() => jobRuns.id),
|
|
161
|
+
/**
|
|
162
|
+
* Service generates `id` client-side via randomUUID() and sets
|
|
163
|
+
* root_run_id = id for root runs (single INSERT, no self-FK race).
|
|
164
|
+
*/
|
|
165
|
+
rootRunId: uuid2("root_run_id").notNull(),
|
|
166
|
+
parentClosePolicy: parentClosePolicyEnum("parent_close_policy").notNull().default("terminate"),
|
|
167
|
+
scopeEntityType: text2("scope_entity_type"),
|
|
168
|
+
scopeEntityId: text2("scope_entity_id"),
|
|
169
|
+
tenantId: text2("tenant_id"),
|
|
170
|
+
tags: jsonb2("tags").notNull().default({}).$type(),
|
|
171
|
+
pool: text2("pool").notNull(),
|
|
172
|
+
priority: integer("priority").notNull().default(0),
|
|
173
|
+
concurrencyKey: text2("concurrency_key"),
|
|
174
|
+
dedupeKey: text2("dedupe_key"),
|
|
175
|
+
status: jobRunStatusEnum("status").notNull().default("pending"),
|
|
176
|
+
input: jsonb2("input").notNull().$type(),
|
|
177
|
+
output: jsonb2("output").$type(),
|
|
178
|
+
error: jsonb2("error").$type(),
|
|
179
|
+
triggerSource: triggerSourceEnum("trigger_source").notNull(),
|
|
180
|
+
triggerRef: text2("trigger_ref"),
|
|
181
|
+
runAt: timestamp2("run_at", { withTimezone: true }).notNull().defaultNow(),
|
|
182
|
+
startedAt: timestamp2("started_at", { withTimezone: true }),
|
|
183
|
+
finishedAt: timestamp2("finished_at", { withTimezone: true }),
|
|
184
|
+
claimedAt: timestamp2("claimed_at", { withTimezone: true }),
|
|
185
|
+
attempts: integer("attempts").notNull().default(0),
|
|
186
|
+
// Phase 3 placeholder — see ADR-025
|
|
187
|
+
waitKind: waitKindEnum("wait_kind"),
|
|
188
|
+
// Phase 3 placeholder — see ADR-025
|
|
189
|
+
resumeToken: text2("resume_token"),
|
|
190
|
+
// Phase 3 placeholder — see ADR-025
|
|
191
|
+
waitDeadline: timestamp2("wait_deadline", { withTimezone: true }),
|
|
192
|
+
createdAt: timestamp2("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
193
|
+
updatedAt: timestamp2("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
194
|
+
},
|
|
195
|
+
(t) => ({
|
|
196
|
+
/** Claim query: ORDER BY priority DESC, run_at ASC. */
|
|
197
|
+
idxJobRunClaim: index2("idx_job_run_claim").on(t.status, t.pool, t.runAt),
|
|
198
|
+
/** Tree traversal / cascade cancel. */
|
|
199
|
+
idxJobRunRoot: index2("idx_job_run_root").on(t.rootRunId),
|
|
200
|
+
/** listForScope query. */
|
|
201
|
+
idxJobRunScope: index2("idx_job_run_scope").on(t.scopeEntityType, t.scopeEntityId),
|
|
202
|
+
/** Idempotency collapse — partial index. */
|
|
203
|
+
idxJobRunDedupe: index2("idx_job_run_dedupe").on(t.jobType, t.dedupeKey).where(sql`${t.dedupeKey} IS NOT NULL`),
|
|
204
|
+
/** Collision check — partial index. */
|
|
205
|
+
idxJobRunConcurrency: index2("idx_job_run_concurrency").on(t.concurrencyKey).where(
|
|
206
|
+
sql`${t.concurrencyKey} IS NOT NULL AND ${t.status} IN ('pending','running')`
|
|
207
|
+
)
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
var jobSteps = pgTable2(
|
|
211
|
+
"job_step",
|
|
212
|
+
{
|
|
213
|
+
id: uuid2("id").primaryKey().defaultRandom(),
|
|
214
|
+
jobRunId: uuid2("job_run_id").notNull().references(() => jobRuns.id),
|
|
215
|
+
stepId: text2("step_id").notNull(),
|
|
216
|
+
kind: jobStepKindEnum("kind").notNull().default("task"),
|
|
217
|
+
/**
|
|
218
|
+
* Monotonic within run. integer (max ~2B per run) is sufficient —
|
|
219
|
+
* downgraded from ADR-022's bigint; revisit only if a single run
|
|
220
|
+
* ever exceeds 2 billion steps.
|
|
221
|
+
*/
|
|
222
|
+
seq: integer("seq").notNull(),
|
|
223
|
+
status: jobStepStatusEnum("status").notNull().default("pending"),
|
|
224
|
+
input: jsonb2("input").$type(),
|
|
225
|
+
/** Memoised on success for replay. */
|
|
226
|
+
output: jsonb2("output").$type(),
|
|
227
|
+
error: jsonb2("error").$type(),
|
|
228
|
+
attempts: integer("attempts").notNull().default(0),
|
|
229
|
+
startedAt: timestamp2("started_at", { withTimezone: true }),
|
|
230
|
+
finishedAt: timestamp2("finished_at", { withTimezone: true })
|
|
231
|
+
},
|
|
232
|
+
(t) => ({
|
|
233
|
+
/** No duplicate step IDs per run. */
|
|
234
|
+
idxJobStepRunStep: uniqueIndex("idx_job_step_run_step").on(t.jobRunId, t.stepId),
|
|
235
|
+
/** Ordered timeline reads. */
|
|
236
|
+
idxJobStepTimeline: index2("idx_job_step_timeline").on(t.jobRunId, t.seq)
|
|
237
|
+
})
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// runtime/subsystems/bridge/bridge-delivery.schema.ts
|
|
241
|
+
var bridgeDeliveryStatusEnum = pgEnum2("bridge_delivery_status", [
|
|
242
|
+
"pending",
|
|
243
|
+
"delivered",
|
|
244
|
+
"skipped",
|
|
245
|
+
"failed"
|
|
246
|
+
]);
|
|
247
|
+
var bridgeDelivery = pgTable3(
|
|
248
|
+
"bridge_delivery",
|
|
249
|
+
{
|
|
250
|
+
id: uuid3("id").primaryKey().defaultRandom(),
|
|
251
|
+
/** FK to the source event in the outbox. */
|
|
252
|
+
eventId: uuid3("event_id").notNull().references(() => domainEvents.id),
|
|
253
|
+
/**
|
|
254
|
+
* Stable codegen-emitted identifier for the (job, trigger) pair, of the
|
|
255
|
+
* form `<job_type>#<triggerIndex>` (BRIDGE-6). Forms the second half of
|
|
256
|
+
* the UNIQUE idempotency key.
|
|
257
|
+
*/
|
|
258
|
+
triggerId: text3("trigger_id").notNull(),
|
|
259
|
+
/**
|
|
260
|
+
* Wrapper `job_run.id` (the framework `@framework/bridge_delivery` run
|
|
261
|
+
* that drove this delivery). Nullable: the facade-eager path
|
|
262
|
+
* (`publishAndStart` Case B) pre-writes a delivered row with no wrapper.
|
|
263
|
+
*/
|
|
264
|
+
wrapperRunId: uuid3("wrapper_run_id").references(() => jobRuns.id),
|
|
265
|
+
/**
|
|
266
|
+
* Spawned user `job_run.id`. Null until status is `delivered`; remains
|
|
267
|
+
* null for `skipped` and `failed` deliveries.
|
|
268
|
+
*/
|
|
269
|
+
userRunId: uuid3("user_run_id").references(() => jobRuns.id),
|
|
270
|
+
status: bridgeDeliveryStatusEnum("status").notNull().default("pending"),
|
|
271
|
+
/** Populated when status=`skipped` (e.g. `'when_returned_false'`, `'trigger_unregistered'`). */
|
|
272
|
+
skipReason: text3("skip_reason"),
|
|
273
|
+
/** Populated when status=`failed`. Mirrors `job_run.error` shape. */
|
|
274
|
+
error: jsonb3("error").$type(),
|
|
275
|
+
/**
|
|
276
|
+
* Emitted unconditionally and nullable (JOB-8 / SYNC-6 precedent).
|
|
277
|
+
* Enforcement gated on `BRIDGE_MULTI_TENANT` at the service layer
|
|
278
|
+
* (BRIDGE-8); no DB constraint.
|
|
279
|
+
*/
|
|
280
|
+
tenantId: text3("tenant_id"),
|
|
281
|
+
attemptedAt: timestamp3("attempted_at", { withTimezone: true }).notNull().defaultNow(),
|
|
282
|
+
deliveredAt: timestamp3("delivered_at", { withTimezone: true })
|
|
283
|
+
},
|
|
284
|
+
(t) => ({
|
|
285
|
+
/**
|
|
286
|
+
* Idempotency ledger. Outbox replays and facade-vs-drain collisions both
|
|
287
|
+
* dedup through this constraint.
|
|
288
|
+
*/
|
|
289
|
+
uqBridgeDeliveryEventTrigger: unique("uq_bridge_delivery_event_trigger").on(
|
|
290
|
+
t.eventId,
|
|
291
|
+
t.triggerId
|
|
292
|
+
),
|
|
293
|
+
/** Lookup all deliveries for an event (fanout report, debugging). */
|
|
294
|
+
idxBridgeDeliveryEvent: index3("idx_bridge_delivery_event").on(t.eventId),
|
|
295
|
+
/**
|
|
296
|
+
* Ops dashboard filter — only the actionable states. Partial index keeps
|
|
297
|
+
* it small at scale (the bulk of rows will be `delivered`).
|
|
298
|
+
*/
|
|
299
|
+
idxBridgeDeliveryStatus: index3("idx_bridge_delivery_status").on(t.status).where(sql2`${t.status} IN ('pending','failed')`),
|
|
300
|
+
/**
|
|
301
|
+
* Reverse lookup from a spawned user run back to its delivery row.
|
|
302
|
+
* Partial — most rows in the bridge ledger but only successful
|
|
303
|
+
* deliveries have a `user_run_id`.
|
|
304
|
+
*/
|
|
305
|
+
idxBridgeDeliveryUserRun: index3("idx_bridge_delivery_user_run").on(t.userRunId).where(sql2`${t.userRunId} IS NOT NULL`)
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// runtime/subsystems/observability/reporters/bridge-metrics.reporter.ts
|
|
310
|
+
var INTERVAL_NAME = "bridge-metrics-tick";
|
|
311
|
+
var DEFAULT_INTERVAL_MS = 6e4;
|
|
312
|
+
var MIN_INTERVAL_MS = 1e3;
|
|
313
|
+
var BridgeMetricsReporter = class {
|
|
314
|
+
constructor(db, scheduler) {
|
|
315
|
+
this.db = db;
|
|
316
|
+
this.scheduler = scheduler;
|
|
317
|
+
this.intervalMs = this.resolveIntervalMs();
|
|
318
|
+
this.lastTickAt = /* @__PURE__ */ new Date();
|
|
319
|
+
}
|
|
320
|
+
db;
|
|
321
|
+
scheduler;
|
|
322
|
+
logger = new Logger(BridgeMetricsReporter.name);
|
|
323
|
+
intervalMs;
|
|
324
|
+
lastTickAt;
|
|
325
|
+
onModuleInit() {
|
|
326
|
+
this.logger.log(
|
|
327
|
+
`BridgeMetricsReporter starting (intervalMs=${this.intervalMs}).`
|
|
328
|
+
);
|
|
329
|
+
const timer = setInterval(() => {
|
|
330
|
+
void this.tick();
|
|
331
|
+
}, this.intervalMs);
|
|
332
|
+
timer.unref?.();
|
|
333
|
+
this.scheduler.addInterval(INTERVAL_NAME, timer);
|
|
334
|
+
}
|
|
335
|
+
onModuleDestroy() {
|
|
336
|
+
if (this.scheduler.getIntervals().includes(INTERVAL_NAME)) {
|
|
337
|
+
this.scheduler.deleteInterval(INTERVAL_NAME);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Run one sampling tick. Public so tests can drive it deterministically
|
|
342
|
+
* without waiting on the timer.
|
|
343
|
+
*/
|
|
344
|
+
async tick() {
|
|
345
|
+
const windowStart = this.lastTickAt;
|
|
346
|
+
const windowEnd = /* @__PURE__ */ new Date();
|
|
347
|
+
this.lastTickAt = windowEnd;
|
|
348
|
+
let rows = [];
|
|
349
|
+
try {
|
|
350
|
+
rows = await this.sample(windowStart, windowEnd);
|
|
351
|
+
} catch (err) {
|
|
352
|
+
this.logger.error(
|
|
353
|
+
`bridge metrics sample failed: ${err.message}`
|
|
354
|
+
);
|
|
355
|
+
return { windowStart, windowEnd, rows: [] };
|
|
356
|
+
}
|
|
357
|
+
this.emit({ windowStart, windowEnd, rows });
|
|
358
|
+
return { windowStart, windowEnd, rows };
|
|
359
|
+
}
|
|
360
|
+
async sample(windowStart, windowEnd) {
|
|
361
|
+
const lastTransition = sql3`COALESCE(${bridgeDelivery.deliveredAt}, ${bridgeDelivery.attemptedAt})`;
|
|
362
|
+
const result = await this.db.select({
|
|
363
|
+
status: bridgeDelivery.status,
|
|
364
|
+
eventType: domainEvents.type,
|
|
365
|
+
skipReason: bridgeDelivery.skipReason,
|
|
366
|
+
count: sql3`COUNT(*)::int`
|
|
367
|
+
}).from(bridgeDelivery).innerJoin(domainEvents, eq(bridgeDelivery.eventId, domainEvents.id)).where(
|
|
368
|
+
and(
|
|
369
|
+
gt(lastTransition, windowStart),
|
|
370
|
+
sql3`${lastTransition} <= ${windowEnd}`
|
|
371
|
+
)
|
|
372
|
+
).groupBy(
|
|
373
|
+
bridgeDelivery.status,
|
|
374
|
+
domainEvents.type,
|
|
375
|
+
bridgeDelivery.skipReason
|
|
376
|
+
);
|
|
377
|
+
return result.map((r) => ({
|
|
378
|
+
status: r.status,
|
|
379
|
+
eventType: r.eventType,
|
|
380
|
+
skipReason: r.skipReason,
|
|
381
|
+
count: r.count
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
emit(tick) {
|
|
385
|
+
if (tick.rows.length === 0) {
|
|
386
|
+
this.logger.log(
|
|
387
|
+
`bridge_metrics tick=empty window=[${tick.windowStart.toISOString()}..${tick.windowEnd.toISOString()}]`
|
|
388
|
+
);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const totals = tick.rows.reduce(
|
|
392
|
+
(acc, r) => {
|
|
393
|
+
acc[r.status] = (acc[r.status] ?? 0) + r.count;
|
|
394
|
+
return acc;
|
|
395
|
+
},
|
|
396
|
+
{}
|
|
397
|
+
);
|
|
398
|
+
const detail = tick.rows.map(
|
|
399
|
+
(r) => `${r.eventType}|${r.status}${r.skipReason ? `:${r.skipReason}` : ""}=${r.count}`
|
|
400
|
+
).join(" ");
|
|
401
|
+
this.logger.log(
|
|
402
|
+
`bridge_metrics tick window=[${tick.windowStart.toISOString()}..${tick.windowEnd.toISOString()}] totals=${JSON.stringify(totals)} detail=[${detail}]`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
resolveIntervalMs() {
|
|
406
|
+
const raw = process.env["BRIDGE_METRICS_INTERVAL_MS"];
|
|
407
|
+
if (!raw) return DEFAULT_INTERVAL_MS;
|
|
408
|
+
const parsed = Number.parseInt(raw, 10);
|
|
409
|
+
if (!Number.isFinite(parsed) || parsed < MIN_INTERVAL_MS) {
|
|
410
|
+
new Logger(BridgeMetricsReporter.name).warn(
|
|
411
|
+
`Ignoring BRIDGE_METRICS_INTERVAL_MS='${raw}' (invalid or < ${MIN_INTERVAL_MS}ms); using default ${DEFAULT_INTERVAL_MS}ms.`
|
|
412
|
+
);
|
|
413
|
+
return DEFAULT_INTERVAL_MS;
|
|
414
|
+
}
|
|
415
|
+
return parsed;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
BridgeMetricsReporter = __decorateClass([
|
|
419
|
+
Injectable(),
|
|
420
|
+
__decorateParam(0, Inject(DRIZZLE))
|
|
421
|
+
], BridgeMetricsReporter);
|
|
422
|
+
export {
|
|
423
|
+
BridgeMetricsReporter
|
|
424
|
+
};
|
|
425
|
+
//# sourceMappingURL=bridge-metrics.reporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../runtime/subsystems/observability/reporters/bridge-metrics.reporter.ts","../../../../../runtime/constants/tokens.ts","../../../../../runtime/subsystems/bridge/bridge-delivery.schema.ts","../../../../../runtime/subsystems/events/domain-events.schema.ts","../../../../../runtime/subsystems/jobs/job-orchestration.schema.ts"],"sourcesContent":["/**\n * BridgeMetricsReporter — periodic structured-log sampler for the\n * `bridge_delivery` ledger.\n *\n * Runs on a timer (default 60s, configurable via\n * `BRIDGE_METRICS_INTERVAL_MS`) and emits ONE `Logger.log` line per tick\n * describing counts of rows that transitioned through each terminal\n * status in the last tick window, grouped by `(status, event_type,\n * skip_reason)`.\n *\n * # Placement\n *\n * Lives under `observability/reporters/` rather than in the bridge\n * subsystem itself because:\n * 1. It's not part of the bridge's functional surface — a reporter is\n * an observability concern composed on top.\n * 2. Future reporters (Prometheus exporter, OTel bridge, etc.) slot in\n * here with no cross-subsystem import churn.\n *\n * # Opt-in via ObservabilityModule\n *\n * The reporter is NOT provided automatically. Opt in via\n * `ObservabilityModule.forRoot({ backend, reporters: { bridgeMetrics: true } })`\n * — the module only registers the reporter when that flag is set, which\n * keeps consumers without the bridge subsystem free of its schema import\n * tax (tree-shaken; see `observability.module.ts` for the gate).\n *\n * # Why a sampler instead of in-handler logs\n *\n * The bridge subsystem writes the `bridge_delivery` ledger directly; adding\n * per-transition log lines inside the handler would double every row at\n * 1:1 cardinality. Aggregating per-tick produces the \"counts per event\n * type of delivered/skipped/failed\" shape that ops dashboards want,\n * without touching the bridge runtime.\n *\n * # Why aggregate-per-tick rather than per-row\n *\n * Deliveries flow at bulk-sync cadence (one event per persisted CRM\n * record). Per-row logs would be noisy and duplicative of the ledger\n * itself; aggregates match the \"counts per event type of\n * delivered/skipped/failed\" operator surface.\n */\nimport {\n Inject,\n Injectable,\n Logger,\n type OnModuleDestroy,\n type OnModuleInit,\n} from '@nestjs/common';\nimport { SchedulerRegistry } from '@nestjs/schedule';\nimport { and, eq, gt, sql } from 'drizzle-orm';\n\nimport { DRIZZLE } from '../../../constants/tokens';\nimport type { DrizzleClient } from '../../../types/drizzle';\nimport { bridgeDelivery } from '../../bridge/bridge-delivery.schema';\nimport { domainEvents } from '../../events/domain-events.schema';\n\nconst INTERVAL_NAME = 'bridge-metrics-tick';\n\n/** Default sampling interval (1 minute). */\nconst DEFAULT_INTERVAL_MS = 60_000;\n\n/** Minimum allowed interval — guards against env misconfig producing a hot loop. */\nconst MIN_INTERVAL_MS = 1_000;\n\nexport interface BridgeMetricsRow {\n status: 'pending' | 'delivered' | 'skipped' | 'failed';\n eventType: string;\n skipReason: string | null;\n count: number;\n}\n\nexport interface BridgeMetricsTick {\n windowStart: Date;\n windowEnd: Date;\n rows: BridgeMetricsRow[];\n}\n\n@Injectable()\nexport class BridgeMetricsReporter implements OnModuleInit, OnModuleDestroy {\n private readonly logger = new Logger(BridgeMetricsReporter.name);\n private readonly intervalMs: number;\n private lastTickAt: Date;\n\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n private readonly scheduler: SchedulerRegistry,\n ) {\n this.intervalMs = this.resolveIntervalMs();\n // Initialize the window tail at boot so the first tick reports only\n // deliveries that transitioned after the reporter started.\n this.lastTickAt = new Date();\n }\n\n onModuleInit(): void {\n this.logger.log(\n `BridgeMetricsReporter starting (intervalMs=${this.intervalMs}).`,\n );\n const timer = setInterval(() => {\n void this.tick();\n }, this.intervalMs);\n // Allow the process to exit naturally in test runs — setInterval\n // otherwise pins the event loop open.\n timer.unref?.();\n this.scheduler.addInterval(INTERVAL_NAME, timer);\n }\n\n onModuleDestroy(): void {\n if (this.scheduler.getIntervals().includes(INTERVAL_NAME)) {\n this.scheduler.deleteInterval(INTERVAL_NAME);\n }\n }\n\n /**\n * Run one sampling tick. Public so tests can drive it deterministically\n * without waiting on the timer.\n */\n async tick(): Promise<BridgeMetricsTick> {\n const windowStart = this.lastTickAt;\n const windowEnd = new Date();\n this.lastTickAt = windowEnd;\n\n let rows: BridgeMetricsRow[] = [];\n try {\n rows = await this.sample(windowStart, windowEnd);\n } catch (err) {\n this.logger.error(\n `bridge metrics sample failed: ${(err as Error).message}`,\n );\n return { windowStart, windowEnd, rows: [] };\n }\n\n this.emit({ windowStart, windowEnd, rows });\n return { windowStart, windowEnd, rows };\n }\n\n private async sample(\n windowStart: Date,\n windowEnd: Date,\n ): Promise<BridgeMetricsRow[]> {\n // Terminal transitions land `delivered_at` for `delivered`, and leave\n // `attempted_at` as the most recent timestamp for `skipped`/`failed`.\n // Window on COALESCE so terminal skipped/failed rows are captured\n // alongside delivered. Upper edge bounded by windowEnd so a long tick\n // can't double-count rows that transitioned between sample and emit.\n const lastTransition = sql<Date>`COALESCE(${bridgeDelivery.deliveredAt}, ${bridgeDelivery.attemptedAt})`;\n\n const result = await this.db\n .select({\n status: bridgeDelivery.status,\n eventType: domainEvents.type,\n skipReason: bridgeDelivery.skipReason,\n count: sql<number>`COUNT(*)::int`,\n })\n .from(bridgeDelivery)\n .innerJoin(domainEvents, eq(bridgeDelivery.eventId, domainEvents.id))\n .where(\n and(\n gt(lastTransition, windowStart),\n sql`${lastTransition} <= ${windowEnd}`,\n ),\n )\n .groupBy(\n bridgeDelivery.status,\n domainEvents.type,\n bridgeDelivery.skipReason,\n );\n\n return result.map((r) => ({\n status: r.status as BridgeMetricsRow['status'],\n eventType: r.eventType,\n skipReason: r.skipReason,\n count: r.count,\n }));\n }\n\n private emit(tick: BridgeMetricsTick): void {\n if (tick.rows.length === 0) {\n // Heartbeat — confirms the sampler is alive when deliveries are idle.\n // Cheap enough at default 60s cadence; operators rely on this signal\n // to distinguish \"bridge quiet\" from \"reporter dead\".\n this.logger.log(\n `bridge_metrics tick=empty window=[${tick.windowStart.toISOString()}..${tick.windowEnd.toISOString()}]`,\n );\n return;\n }\n\n const totals = tick.rows.reduce(\n (acc, r) => {\n acc[r.status] = (acc[r.status] ?? 0) + r.count;\n return acc;\n },\n {} as Record<string, number>,\n );\n\n const detail = tick.rows\n .map(\n (r) =>\n `${r.eventType}|${r.status}${r.skipReason ? `:${r.skipReason}` : ''}=${r.count}`,\n )\n .join(' ');\n\n this.logger.log(\n `bridge_metrics tick window=[${tick.windowStart.toISOString()}..${tick.windowEnd.toISOString()}] ` +\n `totals=${JSON.stringify(totals)} detail=[${detail}]`,\n );\n }\n\n private resolveIntervalMs(): number {\n const raw = process.env['BRIDGE_METRICS_INTERVAL_MS'];\n if (!raw) return DEFAULT_INTERVAL_MS;\n const parsed = Number.parseInt(raw, 10);\n if (!Number.isFinite(parsed) || parsed < MIN_INTERVAL_MS) {\n new Logger(BridgeMetricsReporter.name).warn(\n `Ignoring BRIDGE_METRICS_INTERVAL_MS='${raw}' (invalid or < ${MIN_INTERVAL_MS}ms); ` +\n `using default ${DEFAULT_INTERVAL_MS}ms.`,\n );\n return DEFAULT_INTERVAL_MS;\n }\n return parsed;\n }\n}\n","/**\n * NestJS injection tokens\n *\n * Used with @Inject() decorator in concrete repository constructors.\n */\n\n/**\n * Injection token for the Drizzle ORM database client.\n *\n * Usage in concrete repositories:\n * ```typescript\n * constructor(@Inject(DRIZZLE) db: DrizzleClient) { super(db); }\n * ```\n */\nexport const DRIZZLE = 'DRIZZLE' as const;\n\n/**\n * Injection token for the event bus (IEventBus).\n *\n * Optional — only resolved when EventsModule.forRoot() is registered.\n * BaseService uses this with @Optional() to emit lifecycle events\n * without requiring the events subsystem to be installed.\n *\n * Usage in services/use cases:\n * ```typescript\n * @Optional() @Inject(EVENT_BUS) eventBus?: IEventBus\n * ```\n */\nexport const EVENT_BUS = 'EVENT_BUS' as const;\n","/**\n * Drizzle schema for the `bridge_delivery` ledger (ADR-023 Phase 2, BRIDGE-1).\n *\n * The `bridge_delivery` table is the idempotency ledger for the event-to-job\n * bridge. Every (event, trigger) pair the bridge is asked to spawn produces\n * exactly one row; the `UNIQUE (event_id, trigger_id)` constraint guarantees\n * that:\n *\n * 1. Outbox replays of an event do not double-spawn user job runs — the\n * drain attempts to insert the duplicate, the constraint trips, and the\n * drain skips that trigger.\n * 2. The `IEventFlow.publishAndStart` facade can pre-write a\n * `(status='delivered')` row before the drain runs (Case B from ADR-023\n * §`publishAndStart` + existing `triggers:` collision); the drain then\n * hits UNIQUE on that trigger and skips it while still spawning any\n * other triggers for the same event normally.\n *\n * Status values:\n * - `pending` — wrapper run exists; user job not yet started.\n * - `delivered` — user job started; `user_run_id` populated.\n * - `skipped` — intentional no-op (`when:` returned false, or\n * facade-eager path pre-empted the bridge spawn).\n * - `failed` — wrapper exhausted retry policy; no auto-retry past that\n * (mirrors events outbox stance — ops eyes only).\n *\n * `wrapper_run_id` is **nullable**: the facade-eager path (Case B) pre-writes\n * `bridge_delivery` with no wrapper. The bridge-drain path always populates\n * it.\n *\n * `tenant_id` is emitted **unconditionally and nullable** (per JOB-8\n * 2026-04-20 reversal); enforcement is service-layer (BRIDGE-8) gated on the\n * `BRIDGE_MULTI_TENANT` DI token, not a DB constraint.\n *\n * Indexes:\n * - `bridge_delivery_event_idx` — lookup all deliveries for an event.\n * - `bridge_delivery_status_idx` — partial; ops dashboards filter by\n * `pending | failed`.\n * - `bridge_delivery_user_run_idx` — partial; reverse lookup from a\n * spawned user run back to its delivery row.\n *\n * No service logic, no DI wiring — this is the schema foundation. Backends\n * (memory + drizzle) and the framework handler land in BRIDGE-3 / BRIDGE-4 /\n * BRIDGE-5.\n */\nimport {\n index,\n jsonb,\n pgEnum,\n pgTable,\n text,\n timestamp,\n unique,\n uuid,\n} from 'drizzle-orm/pg-core';\nimport { sql } from 'drizzle-orm';\nimport type { InferSelectModel } from 'drizzle-orm';\n\nimport { domainEvents } from '../events/domain-events.schema';\nimport { jobRuns } from '../jobs/job-orchestration.schema';\n\n// ─── Enum ───────────────────────────────────────────────────────────────────\n\nexport const bridgeDeliveryStatusEnum = pgEnum('bridge_delivery_status', [\n 'pending',\n 'delivered',\n 'skipped',\n 'failed',\n]);\n\n// ─── Table ──────────────────────────────────────────────────────────────────\n\nexport const bridgeDelivery = pgTable(\n 'bridge_delivery',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n /** FK to the source event in the outbox. */\n eventId: uuid('event_id')\n .notNull()\n .references(() => domainEvents.id),\n /**\n * Stable codegen-emitted identifier for the (job, trigger) pair, of the\n * form `<job_type>#<triggerIndex>` (BRIDGE-6). Forms the second half of\n * the UNIQUE idempotency key.\n */\n triggerId: text('trigger_id').notNull(),\n /**\n * Wrapper `job_run.id` (the framework `@framework/bridge_delivery` run\n * that drove this delivery). Nullable: the facade-eager path\n * (`publishAndStart` Case B) pre-writes a delivered row with no wrapper.\n */\n wrapperRunId: uuid('wrapper_run_id').references(() => jobRuns.id),\n /**\n * Spawned user `job_run.id`. Null until status is `delivered`; remains\n * null for `skipped` and `failed` deliveries.\n */\n userRunId: uuid('user_run_id').references(() => jobRuns.id),\n status: bridgeDeliveryStatusEnum('status').notNull().default('pending'),\n /** Populated when status=`skipped` (e.g. `'when_returned_false'`, `'trigger_unregistered'`). */\n skipReason: text('skip_reason'),\n /** Populated when status=`failed`. Mirrors `job_run.error` shape. */\n error: jsonb('error').$type<Record<string, unknown>>(),\n /**\n * Emitted unconditionally and nullable (JOB-8 / SYNC-6 precedent).\n * Enforcement gated on `BRIDGE_MULTI_TENANT` at the service layer\n * (BRIDGE-8); no DB constraint.\n */\n tenantId: text('tenant_id'),\n attemptedAt: timestamp('attempted_at', { withTimezone: true })\n .notNull()\n .defaultNow(),\n deliveredAt: timestamp('delivered_at', { withTimezone: true }),\n },\n (t) => ({\n /**\n * Idempotency ledger. Outbox replays and facade-vs-drain collisions both\n * dedup through this constraint.\n */\n uqBridgeDeliveryEventTrigger: unique('uq_bridge_delivery_event_trigger').on(\n t.eventId,\n t.triggerId,\n ),\n /** Lookup all deliveries for an event (fanout report, debugging). */\n idxBridgeDeliveryEvent: index('idx_bridge_delivery_event').on(t.eventId),\n /**\n * Ops dashboard filter — only the actionable states. Partial index keeps\n * it small at scale (the bulk of rows will be `delivered`).\n */\n idxBridgeDeliveryStatus: index('idx_bridge_delivery_status')\n .on(t.status)\n .where(sql`${t.status} IN ('pending','failed')`),\n /**\n * Reverse lookup from a spawned user run back to its delivery row.\n * Partial — most rows in the bridge ledger but only successful\n * deliveries have a `user_run_id`.\n */\n idxBridgeDeliveryUserRun: index('idx_bridge_delivery_user_run')\n .on(t.userRunId)\n .where(sql`${t.userRunId} IS NOT NULL`),\n }),\n);\n\nexport type BridgeDeliveryRecord = InferSelectModel<typeof bridgeDelivery>;\n","/**\n * Drizzle schema for the domain_events outbox table.\n *\n * This table backs the DrizzleEventBus. Events are inserted within the\n * same database transaction as the domain write (outbox pattern). A\n * polling process reads unprocessed rows and dispatches to subscribers.\n *\n * First-class routing columns (EVT-1):\n * - `pool` — populated by DrizzleEventBus.publish() (EVT-4); enables\n * pool-filtered drain queries without unpacking metadata JSON.\n * - `direction` — `inbound` | `change` | `outbound`; mirrors the routing\n * dimension used by jobs' reserved `events_inbound` /\n * `events_change` / `events_outbound` pools.\n * - `tenant_id` — conditional: emitted only when `events.multi_tenant: true`\n * in `codegen.config.yaml`. The runtime source declares it\n * unconditionally; EVT-8's scaffold template handles the\n * config-driven include/exclude.\n *\n * The `metadata` JSON column continues to carry these values for protocol\n * stability; the first-class columns are an optimization for drain filtering.\n *\n * Indexes (declared below in the index callback):\n * - (status, occurred_at) — polling drain filter\n * - (aggregate_id, aggregate_type) — event replay per aggregate\n * - (pool, status, occurred_at) — per-pool drain filter (EVT-1)\n */\nimport {\n index,\n jsonb,\n pgTable,\n text,\n timestamp,\n uuid,\n} from 'drizzle-orm/pg-core';\nimport type { InferSelectModel } from 'drizzle-orm';\n\nexport const domainEvents = pgTable(\n 'domain_events',\n {\n id: uuid('id').primaryKey(),\n type: text('type').notNull(),\n aggregateId: text('aggregate_id').notNull(),\n aggregateType: text('aggregate_type').notNull(),\n payload: jsonb('payload').notNull().$type<Record<string, unknown>>(),\n occurredAt: timestamp('occurred_at', { withTimezone: true }).notNull(),\n processedAt: timestamp('processed_at', { withTimezone: true }),\n /** Lifecycle status: pending | processed | failed */\n status: text('status').notNull().default('pending'),\n /** Error message from the last failed dispatch attempt. */\n error: text('error'),\n metadata: jsonb('metadata').$type<Record<string, unknown>>(),\n /** Routing pool (e.g. `events_inbound`, `events_change`, `events_outbound`). Populated by DrizzleEventBus.publish() in EVT-4. */\n pool: text('pool'),\n /** Routing direction: `inbound` | `change` | `outbound`. Populated by DrizzleEventBus.publish() in EVT-4. */\n direction: text('direction'),\n // conditional: emitted only when events.multi_tenant: true\n tenantId: text('tenant_id'),\n },\n (t) => ({\n /** Polling drain filter (existing — promoted from comment to declaration in EVT-1). */\n idxDomainEventsStatusOccurredAt: index('idx_domain_events_status_occurred_at').on(\n t.status,\n t.occurredAt,\n ),\n /** Event replay per aggregate (existing — promoted from comment to declaration in EVT-1). */\n idxDomainEventsAggregate: index('idx_domain_events_aggregate').on(\n t.aggregateId,\n t.aggregateType,\n ),\n /** Per-pool drain filter (EVT-1). Enables DrizzleEventBus to drain a single pool without scanning all events. */\n idxDomainEventsPoolStatusOccurredAt: index(\n 'idx_domain_events_pool_status_occurred_at',\n ).on(t.pool, t.status, t.occurredAt),\n }),\n);\n\nexport type DomainEventRecord = InferSelectModel<typeof domainEvents>;\n","/**\n * Drizzle schema for the job orchestration domain (ADR-022).\n *\n * Three tables model the lifecycle of a durable job:\n * - `job` — definitions keyed by handler type (e.g. 'onboarding').\n * - `job_run` — one row per attempt to execute a job; worker claims\n * rows directly via SELECT ... FOR UPDATE SKIP LOCKED.\n * - `job_step` — individual steps within a run; memoises output for replay.\n *\n * Phase 1 ships only this layer. There is no `job_queue` table, no executor\n * port — see ADR-022 and `.claude/skills/jobs/SKILL.md` for the rationale.\n */\nimport {\n pgEnum,\n pgTable,\n uuid,\n text,\n jsonb,\n integer,\n timestamp,\n index,\n uniqueIndex,\n} from 'drizzle-orm/pg-core';\nimport { sql } from 'drizzle-orm';\nimport type { InferSelectModel } from 'drizzle-orm';\n\n// ─── Internal $type<> helpers ───────────────────────────────────────────────\n// Annotation types for jsonb columns only. JOB-2 defines the public protocol\n// types; these remain private to this file.\n\ntype RetryPolicy = {\n attempts: number;\n backoff: 'fixed' | 'exponential';\n baseMs: number;\n nonRetryableErrors?: string[];\n};\n\ntype JobRunError = {\n message: string;\n stack?: string;\n retryable: boolean;\n attempt: number;\n};\n\n// ─── Enums ──────────────────────────────────────────────────────────────────\n\nexport const jobRunStatusEnum = pgEnum('job_run_status', [\n 'pending',\n 'running',\n 'waiting',\n 'completed',\n 'failed',\n 'timed_out',\n 'canceled',\n]);\n\n// extended in ADR-027: tool_call | llm_call | wait | checkpoint | message\nexport const jobStepKindEnum = pgEnum('job_step_kind', ['task']);\n\nexport const jobStepStatusEnum = pgEnum('job_step_status', [\n 'pending',\n 'running',\n 'completed',\n 'failed',\n 'skipped',\n]);\n\nexport const collisionModeEnum = pgEnum('job_collision_mode', [\n 'queue',\n 'reject',\n 'replace',\n]);\n\nexport const replayFromEnum = pgEnum('job_replay_from', [\n 'scratch',\n 'last_step',\n 'last_checkpoint',\n]);\n\nexport const parentClosePolicyEnum = pgEnum('job_parent_close_policy', [\n 'terminate',\n 'cancel',\n 'abandon',\n]);\n\n// Phase 3 placeholder — see ADR-025\nexport const waitKindEnum = pgEnum('job_wait_kind', ['signal']);\n\n// Phase 2 may add more sources; requires Atlas migration\nexport const triggerSourceEnum = pgEnum('job_trigger_source', [\n 'manual',\n 'schedule',\n 'event',\n 'parent',\n]);\n\n// ─── job ────────────────────────────────────────────────────────────────────\n\nexport const jobs = pgTable('job', {\n type: text('type').primaryKey(),\n version: integer('version').notNull().default(1),\n pool: text('pool').notNull(),\n scopeEntityType: text('scope_entity_type'),\n retryPolicy: jsonb('retry_policy').notNull().$type<RetryPolicy>(),\n timeoutMs: integer('timeout_ms'),\n concurrencyKeyTemplate: text('concurrency_key_template'),\n collisionMode: collisionModeEnum('collision_mode').notNull().default('queue'),\n dedupeKeyTemplate: text('dedupe_key_template'),\n dedupeWindowMs: integer('dedupe_window_ms'),\n priorityDefault: integer('priority_default').notNull().default(0),\n replayFrom: replayFromEnum('replay_from').notNull().default('last_checkpoint'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n});\n\nexport type JobDefinitionRow = InferSelectModel<typeof jobs>;\n\n// ─── job_run ────────────────────────────────────────────────────────────────\n\nexport const jobRuns = pgTable(\n 'job_run',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n jobType: text('job_type').notNull().references(() => jobs.type),\n jobVersion: integer('job_version').notNull(),\n parentRunId: uuid('parent_run_id').references((): any => jobRuns.id),\n /**\n * Service generates `id` client-side via randomUUID() and sets\n * root_run_id = id for root runs (single INSERT, no self-FK race).\n */\n rootRunId: uuid('root_run_id').notNull(),\n parentClosePolicy: parentClosePolicyEnum('parent_close_policy')\n .notNull()\n .default('terminate'),\n scopeEntityType: text('scope_entity_type'),\n scopeEntityId: text('scope_entity_id'),\n tenantId: text('tenant_id'),\n tags: jsonb('tags').notNull().default({}).$type<Record<string, string>>(),\n pool: text('pool').notNull(),\n priority: integer('priority').notNull().default(0),\n concurrencyKey: text('concurrency_key'),\n dedupeKey: text('dedupe_key'),\n status: jobRunStatusEnum('status').notNull().default('pending'),\n input: jsonb('input').notNull().$type<Record<string, unknown>>(),\n output: jsonb('output').$type<Record<string, unknown>>(),\n error: jsonb('error').$type<JobRunError>(),\n triggerSource: triggerSourceEnum('trigger_source').notNull(),\n triggerRef: text('trigger_ref'),\n runAt: timestamp('run_at', { withTimezone: true }).notNull().defaultNow(),\n startedAt: timestamp('started_at', { withTimezone: true }),\n finishedAt: timestamp('finished_at', { withTimezone: true }),\n claimedAt: timestamp('claimed_at', { withTimezone: true }),\n attempts: integer('attempts').notNull().default(0),\n // Phase 3 placeholder — see ADR-025\n waitKind: waitKindEnum('wait_kind'),\n // Phase 3 placeholder — see ADR-025\n resumeToken: text('resume_token'),\n // Phase 3 placeholder — see ADR-025\n waitDeadline: timestamp('wait_deadline', { withTimezone: true }),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (t) => ({\n /** Claim query: ORDER BY priority DESC, run_at ASC. */\n idxJobRunClaim: index('idx_job_run_claim').on(t.status, t.pool, t.runAt),\n /** Tree traversal / cascade cancel. */\n idxJobRunRoot: index('idx_job_run_root').on(t.rootRunId),\n /** listForScope query. */\n idxJobRunScope: index('idx_job_run_scope').on(t.scopeEntityType, t.scopeEntityId),\n /** Idempotency collapse — partial index. */\n idxJobRunDedupe: index('idx_job_run_dedupe')\n .on(t.jobType, t.dedupeKey)\n .where(sql`${t.dedupeKey} IS NOT NULL`),\n /** Collision check — partial index. */\n idxJobRunConcurrency: index('idx_job_run_concurrency')\n .on(t.concurrencyKey)\n .where(\n sql`${t.concurrencyKey} IS NOT NULL AND ${t.status} IN ('pending','running')`,\n ),\n }),\n);\n\nexport type JobRunRow = InferSelectModel<typeof jobRuns>;\n\n// ─── job_step ───────────────────────────────────────────────────────────────\n\nexport const jobSteps = pgTable(\n 'job_step',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n jobRunId: uuid('job_run_id').notNull().references(() => jobRuns.id),\n stepId: text('step_id').notNull(),\n kind: jobStepKindEnum('kind').notNull().default('task'),\n /**\n * Monotonic within run. integer (max ~2B per run) is sufficient —\n * downgraded from ADR-022's bigint; revisit only if a single run\n * ever exceeds 2 billion steps.\n */\n seq: integer('seq').notNull(),\n status: jobStepStatusEnum('status').notNull().default('pending'),\n input: jsonb('input').$type<Record<string, unknown>>(),\n /** Memoised on success for replay. */\n output: jsonb('output').$type<Record<string, unknown>>(),\n error: jsonb('error').$type<JobRunError>(),\n attempts: integer('attempts').notNull().default(0),\n startedAt: timestamp('started_at', { withTimezone: true }),\n finishedAt: timestamp('finished_at', { withTimezone: true }),\n },\n (t) => ({\n /** No duplicate step IDs per run. */\n idxJobStepRunStep: uniqueIndex('idx_job_step_run_step').on(t.jobRunId, t.stepId),\n /** Ordered timeline reads. */\n idxJobStepTimeline: index('idx_job_step_timeline').on(t.jobRunId, t.seq),\n }),\n);\n\nexport type JobStepRow = InferSelectModel<typeof jobSteps>;\n"],"mappings":"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","names":["sql","index","jsonb","pgEnum","pgTable","text","timestamp","uuid","sql","pgTable","uuid","text","jsonb","timestamp","index","pgEnum","pgTable","uuid","text","jsonb","timestamp","index","sql","sql"]}
|
|
@@ -340,7 +340,7 @@ declare const syncRuns: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
|
340
340
|
tableName: "sync_runs";
|
|
341
341
|
dataType: "string";
|
|
342
342
|
columnType: "PgEnumColumn";
|
|
343
|
-
data: "
|
|
343
|
+
data: "manual" | "poll" | "cdc" | "webhook" | "writeback";
|
|
344
344
|
driverParam: string;
|
|
345
345
|
notNull: true;
|
|
346
346
|
hasDefault: false;
|
|
@@ -357,7 +357,7 @@ declare const syncRuns: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
|
357
357
|
tableName: "sync_runs";
|
|
358
358
|
dataType: "string";
|
|
359
359
|
columnType: "PgEnumColumn";
|
|
360
|
-
data: "
|
|
360
|
+
data: "running" | "failed" | "success" | "no_changes";
|
|
361
361
|
driverParam: string;
|
|
362
362
|
notNull: true;
|
|
363
363
|
hasDefault: true;
|
|
@@ -639,7 +639,7 @@ declare const syncRunItems: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
|
639
639
|
tableName: "sync_run_items";
|
|
640
640
|
dataType: "string";
|
|
641
641
|
columnType: "PgEnumColumn";
|
|
642
|
-
data: "
|
|
642
|
+
data: "updated" | "noop" | "created" | "deleted";
|
|
643
643
|
driverParam: string;
|
|
644
644
|
notNull: true;
|
|
645
645
|
hasDefault: false;
|
|
@@ -656,7 +656,7 @@ declare const syncRunItems: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
|
656
656
|
tableName: "sync_run_items";
|
|
657
657
|
dataType: "string";
|
|
658
658
|
columnType: "PgEnumColumn";
|
|
659
|
-
data: "
|
|
659
|
+
data: "failed" | "skipped" | "success";
|
|
660
660
|
driverParam: string;
|
|
661
661
|
notNull: true;
|
|
662
662
|
hasDefault: false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pattern-stack/codegen",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Entity-driven code generation for full-stack TypeScript applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"@cubejs-client/core": ">=1.0.0",
|
|
76
76
|
"@nestjs/common": "^10",
|
|
77
77
|
"@nestjs/core": "^10",
|
|
78
|
+
"@nestjs/schedule": "^4.0.0 || ^5.0.0",
|
|
78
79
|
"@nestjs/swagger": "^7.0.0 || ^8.0.0",
|
|
79
80
|
"bullmq": ">=5.0.0",
|
|
80
81
|
"class-transformer": ">=0.5.0",
|
|
@@ -91,6 +92,9 @@
|
|
|
91
92
|
"@cubejs-client/core": {
|
|
92
93
|
"optional": true
|
|
93
94
|
},
|
|
95
|
+
"@nestjs/schedule": {
|
|
96
|
+
"optional": true
|
|
97
|
+
},
|
|
94
98
|
"@nestjs/swagger": {
|
|
95
99
|
"optional": true
|
|
96
100
|
},
|
|
@@ -115,6 +119,7 @@
|
|
|
115
119
|
"@cubejs-client/core": "^1.0.0",
|
|
116
120
|
"@nestjs/common": "10",
|
|
117
121
|
"@nestjs/core": "10",
|
|
122
|
+
"@nestjs/schedule": "^4.0.0",
|
|
118
123
|
"@nestjs/testing": "^10",
|
|
119
124
|
"@types/bun": "latest",
|
|
120
125
|
"@types/ejs": "^3.1.5",
|
|
@@ -33,6 +33,29 @@ export { STORAGE } from './storage';
|
|
|
33
33
|
export type { IStorageService } from './storage';
|
|
34
34
|
export { StorageModule, LocalStorageBackend, MemoryStorageBackend } from './storage';
|
|
35
35
|
|
|
36
|
+
// Observability (ADR-008, 5th subsystem)
|
|
37
|
+
export { OBSERVABILITY, OBSERVABILITY_REPORTERS } from './observability';
|
|
38
|
+
export type {
|
|
39
|
+
CursorSnapshot,
|
|
40
|
+
IObservabilityService,
|
|
41
|
+
JobRunFailure,
|
|
42
|
+
PoolDepth,
|
|
43
|
+
StatusHistogram,
|
|
44
|
+
SyncRunSummary,
|
|
45
|
+
} from './observability';
|
|
46
|
+
export {
|
|
47
|
+
ObservabilityModule,
|
|
48
|
+
DrizzleObservabilityService,
|
|
49
|
+
MemoryObservabilityService,
|
|
50
|
+
BridgeMetricsReporter,
|
|
51
|
+
} from './observability';
|
|
52
|
+
export type {
|
|
53
|
+
ObservabilityModuleOptions,
|
|
54
|
+
ObservabilityReporterOptions,
|
|
55
|
+
BridgeMetricsRow,
|
|
56
|
+
BridgeMetricsTick,
|
|
57
|
+
} from './observability';
|
|
58
|
+
|
|
36
59
|
// Auth
|
|
37
60
|
export {
|
|
38
61
|
ENCRYPTION_KEY,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability subsystem public API (ADR-008, 5th subsystem).
|
|
3
|
+
*
|
|
4
|
+
* Import token + protocol in services/controllers:
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { OBSERVABILITY, type IObservabilityService } from '@pattern-stack/codegen/runtime/subsystems/observability';
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* Import the module in AppModule:
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { ObservabilityModule } from '@pattern-stack/codegen/runtime/subsystems/observability';
|
|
12
|
+
* ObservabilityModule.forRoot({ backend: 'drizzle', reporters: { bridgeMetrics: true } })
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export { OBSERVABILITY, OBSERVABILITY_REPORTERS } from './observability.tokens';
|
|
16
|
+
export type {
|
|
17
|
+
CursorSnapshot,
|
|
18
|
+
IObservabilityService,
|
|
19
|
+
JobRunFailure,
|
|
20
|
+
PoolDepth,
|
|
21
|
+
StatusHistogram,
|
|
22
|
+
SyncRunSummary,
|
|
23
|
+
} from './observability.protocol';
|
|
24
|
+
export {
|
|
25
|
+
ObservabilityModule,
|
|
26
|
+
type ObservabilityModuleOptions,
|
|
27
|
+
type ObservabilityReporterOptions,
|
|
28
|
+
} from './observability.module';
|
|
29
|
+
export { DrizzleObservabilityService } from './observability.drizzle-backend';
|
|
30
|
+
export { MemoryObservabilityService } from './observability.memory-backend';
|
|
31
|
+
export {
|
|
32
|
+
BridgeMetricsReporter,
|
|
33
|
+
type BridgeMetricsRow,
|
|
34
|
+
type BridgeMetricsTick,
|
|
35
|
+
} from './reporters/bridge-metrics.reporter';
|