@sguild/dispatcher 2.0.0 → 2.0.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.
- package/README.md +4 -1
- package/contracts/README.md +30 -0
- package/contracts/coach-availability/README.md +355 -0
- package/contracts/coach-availability/README.v2.md +263 -0
- package/contracts/coach-availability/schema/payloads/coach.assigned-v1.json +91 -0
- package/contracts/coach-availability/validation/delivery-assignment.md +89 -0
- package/contracts/coach-availability/validation/sales-offer-construction.md +76 -0
- package/contracts/coaching-confirmation/README.md +96 -0
- package/contracts/coaching-confirmation/schema/payloads/coaching.lesson.confirmation_decided-v1.json +142 -0
- package/contracts/coaching-confirmation/schema/payloads/lead.coach.confirmation.requested-v1.json +124 -0
- package/contracts/credit-reservation-funding-state/README.md +147 -0
- package/contracts/credit-reservation-lock/README.md +433 -0
- package/contracts/credit-reservation-lock/delivery-state-vocabulary.md +73 -0
- package/contracts/credit-reservation-lock/reservation-create-api.md +191 -0
- package/contracts/credit-reservation-lock/reservation-release-api.md +171 -0
- package/contracts/credit-reservation-lock/schema/payloads/credit.locked-v1.json +1 -1
- package/contracts/credit-reservation-lock/schema/payloads/credit.reserved-v1.json +2 -3
- package/contracts/credit-reservation-lock/validation/lesson-lifecycle.md +318 -0
- package/contracts/event-envelope/README.md +205 -0
- package/contracts/event-envelope/schema/envelope-v1.json +2 -2
- package/contracts/event-envelope/validation/event-vocabulary.md +270 -0
- package/contracts/event-types-registry.json +337 -24
- package/contracts/external-actions/README.md +338 -0
- package/contracts/finance-mart/README.md +238 -0
- package/contracts/finance-mart/cac-payback.md +113 -0
- package/contracts/finance-mart/cash-position.md +98 -0
- package/contracts/finance-mart/cohort-summary.md +72 -0
- package/contracts/finance-mart/customer-journey-audit.md +92 -0
- package/contracts/finance-mart/ltv.md +92 -0
- package/contracts/finance-mart/margin.md +87 -0
- package/contracts/finance-mart/pnl.md +83 -0
- package/contracts/finance-mart/reconciliation.md +98 -0
- package/contracts/finance-mart/revenue-recognition-rollup.md +87 -0
- package/contracts/finance-mart/unit-economics.md +94 -0
- package/contracts/growth-warehouse-api/README.md +162 -0
- package/contracts/identity/README.md +184 -0
- package/contracts/identity/person-canonical-fields.md +120 -0
- package/contracts/identity/person-externals.md +267 -0
- package/contracts/identity/person-resolution-semantics.md +144 -0
- package/contracts/identity/person-role-taxonomy.md +120 -0
- package/contracts/identity/schema/payloads/intake.captured-v2.json +60 -0
- package/contracts/identity/schema/payloads/intake.matched-v2.json +123 -0
- package/contracts/identity/schema/payloads/person.updated-v1.json +8 -2
- package/contracts/identity/schema/payloads/role.assigned-v1.json +50 -0
- package/contracts/identity/schema/payloads/role.retired-v1.json +54 -0
- package/contracts/identity/validation/client-table.md +131 -0
- package/contracts/identity/validation/coach-handling.md +100 -0
- package/contracts/identity/validation/person-graph.md +140 -0
- package/contracts/lead-lifecycle/README.md +187 -0
- package/contracts/lead-lifecycle/schema/payloads/lead.handoff.context.recorded-v1.json +108 -0
- package/contracts/lead-lifecycle/schema/payloads/lead.qualified-v1.json +54 -0
- package/contracts/lead-lifecycle/schema/payloads/sales.lead.onboarded-v1.json +120 -0
- package/contracts/lesson-lifecycle/README.md +118 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.cancelled-v1.json +30 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.delivered-v1.json +29 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.rescheduled-v1.json +157 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.scheduled-v1.json +107 -0
- package/contracts/lesson-lifecycle/validation/README.md +5 -0
- package/contracts/mart-consumer-api/README.md +108 -0
- package/contracts/order-flow/README.md +106 -0
- package/contracts/order-flow/schema/payloads/order.created-v1.json +58 -0
- package/contracts/order-flow/schema/payloads/order.updated-v1.json +63 -0
- package/contracts/payment-flow/README.md +157 -0
- package/contracts/platform-comms/README.md +84 -0
- package/contracts/platform-comms/schema/payloads/platform.comms.inbound-v1.json +83 -0
- package/contracts/platform-geography-snapshot/README.md +205 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.market.archived-v1.json +36 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.market.upserted-v1.json +59 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.service-area.archived-v1.json +36 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.service-area.upserted-v1.json +65 -0
- package/contracts/portfolio-mart/README.md +133 -0
- package/contracts/portfolio-mart/cohort-funnel-panel.md +76 -0
- package/contracts/portfolio-mart/cross-discipline-performance.md +91 -0
- package/contracts/portfolio-mart/cross-market-performance.md +84 -0
- package/contracts/portfolio-mart/health-composites.md +88 -0
- package/contracts/portfolio-mart/org-topology.md +70 -0
- package/contracts/portfolio-mart/portfolio-level-funnel-health.md +92 -0
- package/contracts/portfolio-mart/validation/consumer-isolation.md +33 -0
- package/contracts/portfolio-mart/validation/decoupling-discipline.md +34 -0
- package/contracts/refund-flow/README.md +136 -0
- package/contracts/refund-flow/sales-callable-refund-initiation-api.md +218 -0
- package/contracts/sales-scheduling-surface/README.md +532 -0
- package/contracts/sales-scheduling-surface/schema/payloads/delivery.lesson-hold.cancelled-v1.json +42 -0
- package/contracts/sales-scheduling-surface/schema/payloads/delivery.lesson-hold.created-v1.json +115 -0
- package/contracts/sales-scheduling-surface/validation/composite-hold-create.md +97 -0
- package/contracts/sales-scheduling-surface/validation/lock-state-machine-conformance.md +84 -0
- package/contracts/sales-scheduling-surface/validation/sales-close-orchestration.md +77 -0
- package/contracts/warehouse-silver/README.md +118 -0
- package/contracts/warehouse-silver/coaching-utilization-columns.md +105 -0
- package/dist/events.d.ts +63 -0
- package/dist/events.js +293 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -1
- package/dist/postgres-consumer.js +2 -1
- package/dist/validator.js +1 -0
- package/package.json +1 -1
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Prisma } from "@prisma/client";
|
|
2
|
+
export type DispatcherEventWindow = "30m" | "6h" | "24h" | "7d";
|
|
3
|
+
export type DispatcherDeliveryState = "published" | "delivered" | "resolved" | "dead-lettered";
|
|
4
|
+
export type DispatcherEventCursor = {
|
|
5
|
+
inserted_at: string;
|
|
6
|
+
seq: string;
|
|
7
|
+
producer: string;
|
|
8
|
+
};
|
|
9
|
+
export type DispatcherEventSummary = {
|
|
10
|
+
producer: string;
|
|
11
|
+
event_id: string;
|
|
12
|
+
seq: string;
|
|
13
|
+
event_type: string;
|
|
14
|
+
schema_version: number;
|
|
15
|
+
tenant_id: string;
|
|
16
|
+
subject: string | null;
|
|
17
|
+
actor: string;
|
|
18
|
+
occurred_at: string;
|
|
19
|
+
inserted_at: string;
|
|
20
|
+
correlation_id: string | null;
|
|
21
|
+
causation_id: string | null;
|
|
22
|
+
delivery_state: DispatcherDeliveryState;
|
|
23
|
+
dedup_consumers: string[];
|
|
24
|
+
active_dead_letter_count: number;
|
|
25
|
+
resolved_dead_letter_count: number;
|
|
26
|
+
};
|
|
27
|
+
export type DispatcherDeadLetterEvidence = {
|
|
28
|
+
dead_letter_id: string;
|
|
29
|
+
consumer: string;
|
|
30
|
+
attempt_count: number;
|
|
31
|
+
last_error: string;
|
|
32
|
+
created_at: string;
|
|
33
|
+
resolved_at: string | null;
|
|
34
|
+
};
|
|
35
|
+
export type DispatcherEventDetail = DispatcherEventSummary & {
|
|
36
|
+
payload: Prisma.JsonValue;
|
|
37
|
+
metadata: Prisma.JsonValue | null;
|
|
38
|
+
dead_letters: DispatcherDeadLetterEvidence[];
|
|
39
|
+
};
|
|
40
|
+
export type ListDispatcherEventsFilter = {
|
|
41
|
+
window?: DispatcherEventWindow;
|
|
42
|
+
producer?: string;
|
|
43
|
+
eventType?: string;
|
|
44
|
+
tenantId?: string;
|
|
45
|
+
subject?: string;
|
|
46
|
+
q?: string;
|
|
47
|
+
limit?: number;
|
|
48
|
+
cursor?: string;
|
|
49
|
+
now?: Date;
|
|
50
|
+
};
|
|
51
|
+
export type ListDispatcherEventsResult = {
|
|
52
|
+
events: DispatcherEventSummary[];
|
|
53
|
+
next_cursor: string | null;
|
|
54
|
+
has_more: boolean;
|
|
55
|
+
};
|
|
56
|
+
export type GetDispatcherEventOptions = {
|
|
57
|
+
producer?: string;
|
|
58
|
+
};
|
|
59
|
+
export declare const DISPATCHER_EVENT_WINDOWS: DispatcherEventWindow[];
|
|
60
|
+
export declare function knownEventTypes(): string[];
|
|
61
|
+
export declare function parseDispatcherEventWindow(value: string | null | undefined): DispatcherEventWindow;
|
|
62
|
+
export declare function listDispatcherEvents(filter?: ListDispatcherEventsFilter): Promise<ListDispatcherEventsResult>;
|
|
63
|
+
export declare function getDispatcherEvent(eventId: string, options?: GetDispatcherEventOptions): Promise<DispatcherEventDetail | null>;
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DISPATCHER_EVENT_WINDOWS = void 0;
|
|
4
|
+
exports.knownEventTypes = knownEventTypes;
|
|
5
|
+
exports.parseDispatcherEventWindow = parseDispatcherEventWindow;
|
|
6
|
+
exports.listDispatcherEvents = listDispatcherEvents;
|
|
7
|
+
exports.getDispatcherEvent = getDispatcherEvent;
|
|
8
|
+
const producer_db_1 = require("./producer-db");
|
|
9
|
+
const registry_1 = require("./registry");
|
|
10
|
+
const dlq_1 = require("./dlq");
|
|
11
|
+
exports.DISPATCHER_EVENT_WINDOWS = [
|
|
12
|
+
"30m",
|
|
13
|
+
"6h",
|
|
14
|
+
"24h",
|
|
15
|
+
"7d",
|
|
16
|
+
];
|
|
17
|
+
const WINDOW_MS = {
|
|
18
|
+
"30m": 30 * 60 * 1000,
|
|
19
|
+
"6h": 6 * 60 * 60 * 1000,
|
|
20
|
+
"24h": 24 * 60 * 60 * 1000,
|
|
21
|
+
"7d": 7 * 24 * 60 * 60 * 1000,
|
|
22
|
+
};
|
|
23
|
+
const DEFAULT_LIST_LIMIT = 100;
|
|
24
|
+
const MAX_LIST_LIMIT = 500;
|
|
25
|
+
function knownEventTypes() {
|
|
26
|
+
return Object.keys((0, registry_1.loadRegistry)().events).sort();
|
|
27
|
+
}
|
|
28
|
+
function parseDispatcherEventWindow(value) {
|
|
29
|
+
if (!value)
|
|
30
|
+
return "30m";
|
|
31
|
+
if (exports.DISPATCHER_EVENT_WINDOWS.includes(value)) {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Unsupported dispatcher event window '${value}'`);
|
|
35
|
+
}
|
|
36
|
+
async function listDispatcherEvents(filter = {}) {
|
|
37
|
+
const window = filter.window ?? "30m";
|
|
38
|
+
const now = filter.now ?? new Date();
|
|
39
|
+
const since = new Date(now.getTime() - WINDOW_MS[window]);
|
|
40
|
+
const limit = normalizeLimit(filter.limit);
|
|
41
|
+
const producers = filter.producer ? [filter.producer] : knownEventProducers();
|
|
42
|
+
const aggregateRead = !filter.producer;
|
|
43
|
+
const cursor = decodeCursor(filter.cursor);
|
|
44
|
+
const merged = [];
|
|
45
|
+
for (const producer of producers) {
|
|
46
|
+
const db = (0, producer_db_1.getProducerDb)(producer);
|
|
47
|
+
if (!db) {
|
|
48
|
+
if (filter.producer)
|
|
49
|
+
throw new dlq_1.ProducerDbNotConfiguredError(producer);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const tablePrefix = db.schema ? `"${db.schema}".` : "";
|
|
53
|
+
const whereClauses = ["inserted_at >= $1"];
|
|
54
|
+
const params = [since];
|
|
55
|
+
if (filter.eventType?.trim()) {
|
|
56
|
+
params.push(filter.eventType.trim());
|
|
57
|
+
whereClauses.push(`event_type = $${params.length}`);
|
|
58
|
+
}
|
|
59
|
+
if (filter.tenantId?.trim()) {
|
|
60
|
+
params.push(filter.tenantId.trim());
|
|
61
|
+
whereClauses.push(`tenant_id = $${params.length}`);
|
|
62
|
+
}
|
|
63
|
+
if (filter.subject?.trim()) {
|
|
64
|
+
params.push(filter.subject.trim());
|
|
65
|
+
whereClauses.push(`subject = $${params.length}`);
|
|
66
|
+
}
|
|
67
|
+
if (filter.q?.trim()) {
|
|
68
|
+
params.push(`%${filter.q.trim()}%`);
|
|
69
|
+
const qParam = `$${params.length}`;
|
|
70
|
+
whereClauses.push(`(id ILIKE ${qParam} OR event_type ILIKE ${qParam} OR producer ILIKE ${qParam} OR tenant_id ILIKE ${qParam} OR subject ILIKE ${qParam} OR actor ILIKE ${qParam} OR correlation_id ILIKE ${qParam} OR causation_id ILIKE ${qParam})`);
|
|
71
|
+
}
|
|
72
|
+
if (cursor) {
|
|
73
|
+
params.push(cursor.inserted_at, cursor.seq, producer, cursor.producer);
|
|
74
|
+
const insertedParam = `$${params.length - 3}`;
|
|
75
|
+
const seqParam = `$${params.length - 2}`;
|
|
76
|
+
const producerParam = `$${params.length - 1}`;
|
|
77
|
+
const cursorProducerParam = `$${params.length}`;
|
|
78
|
+
whereClauses.push(`(inserted_at < ${insertedParam} OR (inserted_at = ${insertedParam} AND (seq < ${seqParam}::bigint OR (seq = ${seqParam}::bigint AND ${producerParam} > ${cursorProducerParam}))))`);
|
|
79
|
+
}
|
|
80
|
+
params.push(limit + 1);
|
|
81
|
+
const sql = `SELECT id, seq, event_type, schema_version, tenant_id,
|
|
82
|
+
producer, subject, actor, occurred_at, inserted_at,
|
|
83
|
+
correlation_id, causation_id
|
|
84
|
+
FROM ${tablePrefix}dispatcher_event
|
|
85
|
+
WHERE ${whereClauses.join(" AND ")}
|
|
86
|
+
ORDER BY inserted_at DESC, seq DESC
|
|
87
|
+
LIMIT $${params.length}`;
|
|
88
|
+
let rows;
|
|
89
|
+
try {
|
|
90
|
+
const result = await db.pool.query(sql, params);
|
|
91
|
+
rows = result.rows;
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
if (!aggregateRead)
|
|
95
|
+
throw e;
|
|
96
|
+
console.warn(`[dispatcher.events] producer ${producer} excluded from aggregate read: ${e instanceof Error ? e.message : String(e)}`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const evidence = await deliveryEvidenceForEvents(producer, tablePrefix, rows.map((row) => String(row.id)));
|
|
100
|
+
for (const row of rows) {
|
|
101
|
+
merged.push(toEventSummary(producer, row, evidence.get(String(row.id))));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
merged.sort(compareEventsDesc);
|
|
105
|
+
const page = merged.slice(0, limit);
|
|
106
|
+
const hasMore = merged.length > limit;
|
|
107
|
+
const last = page[page.length - 1];
|
|
108
|
+
return {
|
|
109
|
+
events: page,
|
|
110
|
+
next_cursor: hasMore && last ? encodeCursor(last) : null,
|
|
111
|
+
has_more: hasMore,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async function getDispatcherEvent(eventId, options = {}) {
|
|
115
|
+
if (!eventId)
|
|
116
|
+
return null;
|
|
117
|
+
const producers = options.producer ? [options.producer] : knownEventProducers();
|
|
118
|
+
const aggregateRead = !options.producer;
|
|
119
|
+
for (const producer of producers) {
|
|
120
|
+
const db = (0, producer_db_1.getProducerDb)(producer);
|
|
121
|
+
if (!db) {
|
|
122
|
+
if (options.producer)
|
|
123
|
+
throw new dlq_1.ProducerDbNotConfiguredError(producer);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const tablePrefix = db.schema ? `"${db.schema}".` : "";
|
|
127
|
+
const sql = `SELECT id, seq, event_type, schema_version, tenant_id,
|
|
128
|
+
producer, subject, actor, occurred_at, inserted_at,
|
|
129
|
+
payload, metadata, correlation_id, causation_id
|
|
130
|
+
FROM ${tablePrefix}dispatcher_event
|
|
131
|
+
WHERE id = $1
|
|
132
|
+
LIMIT 1`;
|
|
133
|
+
let rows;
|
|
134
|
+
try {
|
|
135
|
+
const result = await db.pool.query(sql, [eventId]);
|
|
136
|
+
rows = result.rows;
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
if (!aggregateRead)
|
|
140
|
+
throw e;
|
|
141
|
+
console.warn(`[dispatcher.events] producer ${producer} excluded from aggregate detail read: ${e instanceof Error ? e.message : String(e)}`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const row = rows[0];
|
|
145
|
+
if (!row)
|
|
146
|
+
continue;
|
|
147
|
+
const evidence = await deliveryEvidenceForEvents(producer, tablePrefix, [eventId]);
|
|
148
|
+
const summary = toEventSummary(producer, row, evidence.get(eventId));
|
|
149
|
+
const deadLetters = await deadLettersForEvent(producer, tablePrefix, eventId);
|
|
150
|
+
return {
|
|
151
|
+
...summary,
|
|
152
|
+
payload: row.payload ?? null,
|
|
153
|
+
metadata: row.metadata ?? null,
|
|
154
|
+
dead_letters: deadLetters,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function knownEventProducers() {
|
|
160
|
+
const manifest = (0, registry_1.loadRegistry)();
|
|
161
|
+
const set = new Set();
|
|
162
|
+
for (const entry of Object.values(manifest.events)) {
|
|
163
|
+
set.add(entry.producer);
|
|
164
|
+
}
|
|
165
|
+
return Array.from(set);
|
|
166
|
+
}
|
|
167
|
+
async function deliveryEvidenceForEvents(producer, tablePrefix, eventIds) {
|
|
168
|
+
const map = new Map();
|
|
169
|
+
if (eventIds.length === 0)
|
|
170
|
+
return map;
|
|
171
|
+
const db = (0, producer_db_1.getProducerDb)(producer);
|
|
172
|
+
if (!db)
|
|
173
|
+
return map;
|
|
174
|
+
const dedupResult = await db.pool.query(`SELECT event_id, array_agg(consumer ORDER BY consumer) AS consumers
|
|
175
|
+
FROM ${tablePrefix}dispatcher_dedup
|
|
176
|
+
WHERE event_id = ANY($1::text[])
|
|
177
|
+
GROUP BY event_id`, [eventIds]);
|
|
178
|
+
for (const row of dedupResult.rows) {
|
|
179
|
+
map.set(row.event_id, {
|
|
180
|
+
dedup_consumers: row.consumers ?? [],
|
|
181
|
+
active_dead_letter_count: 0,
|
|
182
|
+
resolved_dead_letter_count: 0,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const dlqResult = await db.pool.query(`SELECT event_id,
|
|
186
|
+
COUNT(*) FILTER (WHERE resolved_at IS NULL) AS active_count,
|
|
187
|
+
COUNT(*) FILTER (WHERE resolved_at IS NOT NULL) AS resolved_count
|
|
188
|
+
FROM ${tablePrefix}dispatcher_dead_letter
|
|
189
|
+
WHERE event_id = ANY($1::text[])
|
|
190
|
+
GROUP BY event_id`, [eventIds]);
|
|
191
|
+
for (const row of dlqResult.rows) {
|
|
192
|
+
const current = map.get(row.event_id) ?? {
|
|
193
|
+
dedup_consumers: [],
|
|
194
|
+
active_dead_letter_count: 0,
|
|
195
|
+
resolved_dead_letter_count: 0,
|
|
196
|
+
};
|
|
197
|
+
current.active_dead_letter_count = Number(row.active_count);
|
|
198
|
+
current.resolved_dead_letter_count = Number(row.resolved_count);
|
|
199
|
+
map.set(row.event_id, current);
|
|
200
|
+
}
|
|
201
|
+
return map;
|
|
202
|
+
}
|
|
203
|
+
async function deadLettersForEvent(producer, tablePrefix, eventId) {
|
|
204
|
+
const db = (0, producer_db_1.getProducerDb)(producer);
|
|
205
|
+
if (!db)
|
|
206
|
+
return [];
|
|
207
|
+
const result = await db.pool.query(`SELECT id, consumer, attempt_count, last_error, created_at, resolved_at
|
|
208
|
+
FROM ${tablePrefix}dispatcher_dead_letter
|
|
209
|
+
WHERE event_id = $1
|
|
210
|
+
ORDER BY created_at DESC`, [eventId]);
|
|
211
|
+
return result.rows.map((row) => ({
|
|
212
|
+
dead_letter_id: row.id,
|
|
213
|
+
consumer: row.consumer,
|
|
214
|
+
attempt_count: Number(row.attempt_count),
|
|
215
|
+
last_error: row.last_error,
|
|
216
|
+
created_at: toIso(row.created_at),
|
|
217
|
+
resolved_at: row.resolved_at ? toIso(row.resolved_at) : null,
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
function toEventSummary(fallbackProducer, row, evidence) {
|
|
221
|
+
const dedupConsumers = evidence?.dedup_consumers ?? [];
|
|
222
|
+
const activeDeadLetterCount = evidence?.active_dead_letter_count ?? 0;
|
|
223
|
+
const resolvedDeadLetterCount = evidence?.resolved_dead_letter_count ?? 0;
|
|
224
|
+
return {
|
|
225
|
+
producer: row.producer || fallbackProducer,
|
|
226
|
+
event_id: row.id,
|
|
227
|
+
seq: String(row.seq),
|
|
228
|
+
event_type: row.event_type,
|
|
229
|
+
schema_version: Number(row.schema_version),
|
|
230
|
+
tenant_id: row.tenant_id,
|
|
231
|
+
subject: row.subject ?? null,
|
|
232
|
+
actor: row.actor,
|
|
233
|
+
occurred_at: toIso(row.occurred_at),
|
|
234
|
+
inserted_at: toIso(row.inserted_at),
|
|
235
|
+
correlation_id: row.correlation_id ?? null,
|
|
236
|
+
causation_id: row.causation_id ?? null,
|
|
237
|
+
delivery_state: deliveryState(dedupConsumers, activeDeadLetterCount, resolvedDeadLetterCount),
|
|
238
|
+
dedup_consumers: dedupConsumers,
|
|
239
|
+
active_dead_letter_count: activeDeadLetterCount,
|
|
240
|
+
resolved_dead_letter_count: resolvedDeadLetterCount,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function deliveryState(dedupConsumers, activeDeadLetterCount, resolvedDeadLetterCount) {
|
|
244
|
+
if (activeDeadLetterCount > 0)
|
|
245
|
+
return "dead-lettered";
|
|
246
|
+
if (resolvedDeadLetterCount > 0)
|
|
247
|
+
return "resolved";
|
|
248
|
+
if (dedupConsumers.length > 0)
|
|
249
|
+
return "delivered";
|
|
250
|
+
return "published";
|
|
251
|
+
}
|
|
252
|
+
function compareEventsDesc(a, b) {
|
|
253
|
+
const inserted = b.inserted_at.localeCompare(a.inserted_at);
|
|
254
|
+
if (inserted !== 0)
|
|
255
|
+
return inserted;
|
|
256
|
+
const seq = BigInt(b.seq) > BigInt(a.seq) ? 1 : BigInt(b.seq) < BigInt(a.seq) ? -1 : 0;
|
|
257
|
+
if (seq !== 0)
|
|
258
|
+
return seq;
|
|
259
|
+
return a.producer.localeCompare(b.producer);
|
|
260
|
+
}
|
|
261
|
+
function normalizeLimit(limit) {
|
|
262
|
+
if (limit === undefined)
|
|
263
|
+
return DEFAULT_LIST_LIMIT;
|
|
264
|
+
return Math.min(limit, MAX_LIST_LIMIT);
|
|
265
|
+
}
|
|
266
|
+
function encodeCursor(event) {
|
|
267
|
+
const cursor = {
|
|
268
|
+
inserted_at: event.inserted_at,
|
|
269
|
+
seq: event.seq,
|
|
270
|
+
producer: event.producer,
|
|
271
|
+
};
|
|
272
|
+
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
273
|
+
}
|
|
274
|
+
function decodeCursor(value) {
|
|
275
|
+
if (!value)
|
|
276
|
+
return null;
|
|
277
|
+
try {
|
|
278
|
+
const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
279
|
+
if (!parsed.inserted_at || !parsed.seq || !parsed.producer)
|
|
280
|
+
return null;
|
|
281
|
+
return {
|
|
282
|
+
inserted_at: parsed.inserted_at,
|
|
283
|
+
seq: parsed.seq,
|
|
284
|
+
producer: parsed.producer,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function toIso(value) {
|
|
292
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
293
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export type { ConsumerLoopOptions } from "./postgres-consumer";
|
|
|
25
25
|
export { EnvelopeValidationError, PayloadValidationError, PayloadSchemaUnavailableError, } from "./validator";
|
|
26
26
|
export { DeadLetterAlreadyResolvedError, DeadLetterNotFoundError, ProducerDbNotConfiguredError, ReplayConsumerUnknownError, ReplayDeliveryFailedError, getDeadLetter, knownProducers, listDeadLetters, replayDeadLetter, resolveDeadLetter, } from "./dlq";
|
|
27
27
|
export type { DeadLetter, GetDeadLetterOptions, ListDeadLettersFilter, ReplayDeadLetterInput, ReplayDeadLetterOutcome, ResolveDeadLetterInput, } from "./dlq";
|
|
28
|
+
export { DISPATCHER_EVENT_WINDOWS, getDispatcherEvent, knownEventTypes, listDispatcherEvents, parseDispatcherEventWindow, } from "./events";
|
|
29
|
+
export type { DispatcherDeadLetterEvidence, DispatcherDeliveryState, DispatcherEventDetail, DispatcherEventSummary, DispatcherEventWindow, GetDispatcherEventOptions, ListDispatcherEventsFilter, ListDispatcherEventsResult, } from "./events";
|
|
28
30
|
export { configureDispatcherObservability, resetDispatcherObservabilityForTests, } from "./observability";
|
|
29
31
|
export type { DispatcherHistogramName, DispatcherMetricLabels, DispatcherMetricName, DispatcherObservabilityHooks, } from "./observability";
|
|
30
32
|
export type { ActorRef, CanonicalEntityId, EventEmit, EventEnvelope, LiveProducerDomain, ProducerDomain, PublishResult, SubscribeHandler, } from "./types";
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* (Slice 3b) and the per-consumer DLQ read API (Slice 5) remain.
|
|
20
20
|
*/
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.WebhookDispatchError = exports.verifyEnvelopeSignature = exports.signEnvelope = exports.SIGNATURE_HEADER = exports.EVENT_TYPE_HEADER = exports.EVENT_ID_HEADER = exports.verifyInboxRequest = exports.tryInsertOrDetectConflict = exports.loadRegistry = exports.getEventTypeRegistration = exports.getActiveSchemaVersion = exports.resetDispatcherObservabilityForTests = exports.configureDispatcherObservability = exports.resolveDeadLetter = exports.replayDeadLetter = exports.listDeadLetters = exports.knownProducers = exports.getDeadLetter = exports.ReplayDeliveryFailedError = exports.ReplayConsumerUnknownError = exports.ProducerDbNotConfiguredError = exports.DeadLetterNotFoundError = exports.DeadLetterAlreadyResolvedError = exports.PayloadSchemaUnavailableError = exports.PayloadValidationError = exports.EnvelopeValidationError = exports.configureDispatcher = exports.UnregisteredSchemaVersionError = exports.UnregisteredEventTypeError = exports.DispatcherNotImplementedError = exports.dispatcher = void 0;
|
|
22
|
+
exports.WebhookDispatchError = exports.verifyEnvelopeSignature = exports.signEnvelope = exports.SIGNATURE_HEADER = exports.EVENT_TYPE_HEADER = exports.EVENT_ID_HEADER = exports.verifyInboxRequest = exports.tryInsertOrDetectConflict = exports.loadRegistry = exports.getEventTypeRegistration = exports.getActiveSchemaVersion = exports.resetDispatcherObservabilityForTests = exports.configureDispatcherObservability = exports.parseDispatcherEventWindow = exports.listDispatcherEvents = exports.knownEventTypes = exports.getDispatcherEvent = exports.DISPATCHER_EVENT_WINDOWS = exports.resolveDeadLetter = exports.replayDeadLetter = exports.listDeadLetters = exports.knownProducers = exports.getDeadLetter = exports.ReplayDeliveryFailedError = exports.ReplayConsumerUnknownError = exports.ProducerDbNotConfiguredError = exports.DeadLetterNotFoundError = exports.DeadLetterAlreadyResolvedError = exports.PayloadSchemaUnavailableError = exports.PayloadValidationError = exports.EnvelopeValidationError = exports.configureDispatcher = exports.UnregisteredSchemaVersionError = exports.UnregisteredEventTypeError = exports.DispatcherNotImplementedError = exports.dispatcher = void 0;
|
|
23
23
|
var dispatcher_1 = require("./dispatcher");
|
|
24
24
|
Object.defineProperty(exports, "dispatcher", { enumerable: true, get: function () { return dispatcher_1.dispatcher; } });
|
|
25
25
|
var dispatcher_2 = require("./dispatcher");
|
|
@@ -43,6 +43,12 @@ Object.defineProperty(exports, "knownProducers", { enumerable: true, get: functi
|
|
|
43
43
|
Object.defineProperty(exports, "listDeadLetters", { enumerable: true, get: function () { return dlq_1.listDeadLetters; } });
|
|
44
44
|
Object.defineProperty(exports, "replayDeadLetter", { enumerable: true, get: function () { return dlq_1.replayDeadLetter; } });
|
|
45
45
|
Object.defineProperty(exports, "resolveDeadLetter", { enumerable: true, get: function () { return dlq_1.resolveDeadLetter; } });
|
|
46
|
+
var events_1 = require("./events");
|
|
47
|
+
Object.defineProperty(exports, "DISPATCHER_EVENT_WINDOWS", { enumerable: true, get: function () { return events_1.DISPATCHER_EVENT_WINDOWS; } });
|
|
48
|
+
Object.defineProperty(exports, "getDispatcherEvent", { enumerable: true, get: function () { return events_1.getDispatcherEvent; } });
|
|
49
|
+
Object.defineProperty(exports, "knownEventTypes", { enumerable: true, get: function () { return events_1.knownEventTypes; } });
|
|
50
|
+
Object.defineProperty(exports, "listDispatcherEvents", { enumerable: true, get: function () { return events_1.listDispatcherEvents; } });
|
|
51
|
+
Object.defineProperty(exports, "parseDispatcherEventWindow", { enumerable: true, get: function () { return events_1.parseDispatcherEventWindow; } });
|
|
46
52
|
var observability_1 = require("./observability");
|
|
47
53
|
Object.defineProperty(exports, "configureDispatcherObservability", { enumerable: true, get: function () { return observability_1.configureDispatcherObservability; } });
|
|
48
54
|
Object.defineProperty(exports, "resetDispatcherObservabilityForTests", { enumerable: true, get: function () { return observability_1.resetDispatcherObservabilityForTests; } });
|
|
@@ -257,7 +257,8 @@ class ConsumerLoop {
|
|
|
257
257
|
try {
|
|
258
258
|
await client.query("BEGIN");
|
|
259
259
|
await client.query(`INSERT INTO ${this.tablePrefix}dispatcher_dedup (consumer, event_id)
|
|
260
|
-
VALUES ($1, $2)
|
|
260
|
+
VALUES ($1, $2)
|
|
261
|
+
ON CONFLICT (consumer, event_id) DO NOTHING`, [this.options.consumer, eventId]);
|
|
261
262
|
await client.query(`INSERT INTO ${this.tablePrefix}dispatcher_cursor (consumer, event_type, last_seq, updated_at)
|
|
262
263
|
VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
|
|
263
264
|
ON CONFLICT (consumer, event_type)
|
package/dist/validator.js
CHANGED
|
@@ -132,6 +132,7 @@ function validatePayload(eventType, schemaVersion, payload) {
|
|
|
132
132
|
function __resetValidatorCachesForTests() {
|
|
133
133
|
cachedEnvelopeValidator = null;
|
|
134
134
|
payloadValidatorCache.clear();
|
|
135
|
+
ajv.removeSchema();
|
|
135
136
|
}
|
|
136
137
|
// =============================================================================
|
|
137
138
|
// Internal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sguild/dispatcher",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Cross-domain event dispatcher SDK for Sguild domains, per the Sguild Event Envelope contract and ADR-0009. Producer-side transactional emit, consumer-side polling worker with dedup/retry/dead-letter, envelope and payload validation against the bundled contracts tree.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|