@sguild/dispatcher 2.0.0
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 +354 -0
- package/contracts/credit-reservation-funding-state/schema/payloads/reservation.funded-v1.json +59 -0
- package/contracts/credit-reservation-funding-state/schema/payloads/reservation.refunded-v1.json +80 -0
- package/contracts/credit-reservation-funding-state/schema/payloads/reservation.refunding-v1.json +74 -0
- package/contracts/credit-reservation-lock/schema/payloads/credit.consumed-v1.json +33 -0
- package/contracts/credit-reservation-lock/schema/payloads/credit.forfeited-v1.json +41 -0
- package/contracts/credit-reservation-lock/schema/payloads/credit.funded-v1.json +31 -0
- package/contracts/credit-reservation-lock/schema/payloads/credit.locked-v1.json +42 -0
- package/contracts/credit-reservation-lock/schema/payloads/credit.purchased-v1.json +39 -0
- package/contracts/credit-reservation-lock/schema/payloads/credit.released-v1.json +61 -0
- package/contracts/credit-reservation-lock/schema/payloads/credit.released-v2.json +77 -0
- package/contracts/credit-reservation-lock/schema/payloads/credit.reserved-v1.json +60 -0
- package/contracts/credit-reservation-lock/schema/payloads/customer.handoff-v1.json +35 -0
- package/contracts/event-envelope/schema/envelope-v1.json +79 -0
- package/contracts/event-types-registry.json +541 -0
- package/contracts/identity/schema/payloads/intake.amended-v1.json +124 -0
- package/contracts/identity/schema/payloads/intake.captured-v2.json +114 -0
- package/contracts/identity/schema/payloads/person.updated-v1.json +99 -0
- package/contracts/lead-lifecycle/schema/payloads/lead.attempt.exhausted-v1.json +36 -0
- package/contracts/lead-lifecycle/schema/payloads/lead.callback.scheduled-v1.json +39 -0
- package/contracts/lead-lifecycle/schema/payloads/lead.created-v1.json +50 -0
- package/contracts/lead-lifecycle/schema/payloads/lead.reached-v1.json +39 -0
- package/contracts/lead-lifecycle/schema/payloads/lead.stage.changed-v1.json +44 -0
- package/contracts/payment-flow/schema/payloads/payment.failed-v1.json +88 -0
- package/contracts/payment-flow/schema/payloads/payment.received-v1.json +69 -0
- package/contracts/refund-flow/schema/payloads/refund.completed-v1.json +75 -0
- package/contracts/refund-flow/schema/payloads/refund.initiated-v1.json +69 -0
- package/dist/config.d.ts +67 -0
- package/dist/config.js +81 -0
- package/dist/dispatcher-errors.d.ts +20 -0
- package/dist/dispatcher-errors.js +42 -0
- package/dist/dispatcher.d.ts +123 -0
- package/dist/dispatcher.js +171 -0
- package/dist/dlq.d.ts +173 -0
- package/dist/dlq.js +391 -0
- package/dist/fanout-drain.d.ts +11 -0
- package/dist/fanout-drain.js +31 -0
- package/dist/fanout.d.ts +144 -0
- package/dist/fanout.js +321 -0
- package/dist/inbox.d.ts +125 -0
- package/dist/inbox.js +120 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +70 -0
- package/dist/internal/id.d.ts +38 -0
- package/dist/internal/id.js +78 -0
- package/dist/internal/pg-search-path.d.ts +34 -0
- package/dist/internal/pg-search-path.js +55 -0
- package/dist/internal/resolve-contract-path.d.ts +41 -0
- package/dist/internal/resolve-contract-path.js +65 -0
- package/dist/observability.d.ts +24 -0
- package/dist/observability.js +37 -0
- package/dist/postgres-consumer.d.ts +175 -0
- package/dist/postgres-consumer.js +561 -0
- package/dist/postgres-transport.d.ts +70 -0
- package/dist/postgres-transport.js +144 -0
- package/dist/producer-db.d.ts +80 -0
- package/dist/producer-db.js +115 -0
- package/dist/registry.d.ts +94 -0
- package/dist/registry.js +99 -0
- package/dist/signature.d.ts +44 -0
- package/dist/signature.js +79 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.js +13 -0
- package/dist/validator.d.ts +60 -0
- package/dist/validator.js +171 -0
- package/package.json +48 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres-queue transport: consumer-side polling worker.
|
|
3
|
+
*
|
|
4
|
+
* Implements the consume path of the bus dispatcher per ADR-0009
|
|
5
|
+
* §"Decision". Long-lived process (typically run from
|
|
6
|
+
* `<repo>/scripts/<consumer>-subscriber.ts` per the per-domain module
|
|
7
|
+
* pattern) that polls `dispatcher_event` past a per-(consumer,
|
|
8
|
+
* event_type) cursor, dedups against `dispatcher_dedup`, invokes the
|
|
9
|
+
* registered handler, retries on exception with exponential backoff and
|
|
10
|
+
* jitter, dead-letters on exhaustion, and advances the cursor atomically
|
|
11
|
+
* with the dedup write on success (or the dead-letter write on
|
|
12
|
+
* exhaustion).
|
|
13
|
+
*
|
|
14
|
+
* Phase 2 Slice 3 (this file): poll-only consumer. The LISTEN/NOTIFY
|
|
15
|
+
* wake-up primitive ADR-0009 §"Decision" calls for is the Slice 3b
|
|
16
|
+
* deliverable; Slice 3 ships poll-only because the polling loop is the
|
|
17
|
+
* durable fallback regardless of whether LISTEN/NOTIFY is wired, and
|
|
18
|
+
* shipping poll-only first gives consumers a working subscriber surface
|
|
19
|
+
* to scaffold against. Consumers cut over to the same call shape when
|
|
20
|
+
* LISTEN/NOTIFY lands; only the latency profile changes (sub-second on
|
|
21
|
+
* the happy path, polling cadence as the fallback under either model).
|
|
22
|
+
*
|
|
23
|
+
* Per-consumer scope: each consumer instance is identified by a
|
|
24
|
+
* stable string (e.g., "coaching-availability-subscriber") that goes
|
|
25
|
+
* onto the cursor and dedup rows. A given consumer string MUST be
|
|
26
|
+
* unique across deployments; running two processes against the same
|
|
27
|
+
* consumer string causes them to compete for cursor advancement and
|
|
28
|
+
* the dedup window collapses to per-consumer-id rather than
|
|
29
|
+
* per-process. Per the build plan §3, the typical shape is one
|
|
30
|
+
* consumer string per long-lived worker process per domain.
|
|
31
|
+
*
|
|
32
|
+
* Per-event-type fan-out: a single ConsumerLoop instance processes
|
|
33
|
+
* all subscribed event_types serially within a poll cycle (one
|
|
34
|
+
* batch per event_type, in registration order). For workloads where
|
|
35
|
+
* one event_type has substantially higher volume than others and the
|
|
36
|
+
* serial drain becomes a latency bottleneck, run multiple
|
|
37
|
+
* ConsumerLoop instances against the same database with different
|
|
38
|
+
* consumer strings — but typical Sguild volume per consumer fits
|
|
39
|
+
* comfortably inside a single loop's drain cadence.
|
|
40
|
+
*/
|
|
41
|
+
import { Pool } from "pg";
|
|
42
|
+
import type { SubscribeHandler } from "./types";
|
|
43
|
+
export type ConsumerLoopOptions = {
|
|
44
|
+
/**
|
|
45
|
+
* Stable identifier for this consumer. Goes onto cursor, dedup, and
|
|
46
|
+
* dead-letter rows. MUST be unique across deployments; the convention
|
|
47
|
+
* is one consumer string per long-lived worker process per domain
|
|
48
|
+
* (e.g., "coaching-availability-subscriber",
|
|
49
|
+
* "delivery-customer-tracking-subscriber").
|
|
50
|
+
*/
|
|
51
|
+
consumer: string;
|
|
52
|
+
/** Default 50. Larger values amortize cursor I/O over more rows; smaller values reduce per-cycle latency at the tail. */
|
|
53
|
+
batchSize?: number;
|
|
54
|
+
/** Default 5000 ms. Slice 3b's LISTEN/NOTIFY wake-up will make this the fallback cadence rather than the typical wake-up time. */
|
|
55
|
+
pollIntervalMs?: number;
|
|
56
|
+
/** Default [1000, 5000, 15000] ms. Each value is the base; actual sleep adds 0-30 percent jitter. */
|
|
57
|
+
retryDelaysMs?: number[];
|
|
58
|
+
/**
|
|
59
|
+
* pg.Pool this loop reads cursor/event/dedup/dead-letter rows from
|
|
60
|
+
* via raw SQL. Raw SQL (unqualified table names) lets Postgres
|
|
61
|
+
* resolve the schema via the connection's search_path, which
|
|
62
|
+
* `pgConfigWithSearchPath` derives from the producer DSN's
|
|
63
|
+
* `?schema=<domain>` query param. This
|
|
64
|
+
* keeps the loop schema-agnostic so the same code works against
|
|
65
|
+
* Platform's `public` schema, Growth's `growth` schema, Sales' `sales`
|
|
66
|
+
* schema, etc., without Prisma's hard-coded `public.` qualification
|
|
67
|
+
* getting in the way.
|
|
68
|
+
*
|
|
69
|
+
* Defaults to a pool constructed against `process.env.DATABASE_URL` so
|
|
70
|
+
* the in-process Platform dispatcher path keeps working unchanged. The
|
|
71
|
+
* multi-producer fanout overrides with a producer-specific pool from
|
|
72
|
+
* `producer-db.ts`.
|
|
73
|
+
*/
|
|
74
|
+
pool?: Pool;
|
|
75
|
+
/**
|
|
76
|
+
* DSN used for the LISTEN/NOTIFY wake-up connection. The pg.Client
|
|
77
|
+
* created here MUST connect to the same database as `pool` above so
|
|
78
|
+
* the NOTIFY payload corresponds to the dispatcher_event rows the
|
|
79
|
+
* polling path reads. Defaults to `process.env.DATABASE_URL`; the
|
|
80
|
+
* multi-producer fanout overrides with the producer's DSN.
|
|
81
|
+
*/
|
|
82
|
+
databaseUrl?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Schema name parsed from the DSN's `?schema=<name>` query param
|
|
85
|
+
* (null/undefined means use the default search_path, typically `public`).
|
|
86
|
+
* When set, ConsumerLoop schema-qualifies every SQL query
|
|
87
|
+
* (`"<schema>".dispatcher_cursor` instead of `dispatcher_cursor`).
|
|
88
|
+
*
|
|
89
|
+
* Required for Supabase pooler DSNs because the pgbouncer transaction-mode
|
|
90
|
+
* pooler silently drops the libpq `options=-c search_path=...` startup
|
|
91
|
+
* parameter — meaning search_path is effectively whatever the connection's
|
|
92
|
+
* role default is, not what the DSN declared. Schema-qualified table names
|
|
93
|
+
* bypass search_path entirely.
|
|
94
|
+
*/
|
|
95
|
+
schema?: string | null;
|
|
96
|
+
};
|
|
97
|
+
export type RegisteredHandler = {
|
|
98
|
+
eventType: string;
|
|
99
|
+
handler: SubscribeHandler<unknown>;
|
|
100
|
+
};
|
|
101
|
+
export declare class ConsumerLoop {
|
|
102
|
+
private readonly handlers;
|
|
103
|
+
private running;
|
|
104
|
+
private inflight;
|
|
105
|
+
private readonly options;
|
|
106
|
+
private readonly pool;
|
|
107
|
+
private readonly ownsPool;
|
|
108
|
+
private readonly databaseUrl;
|
|
109
|
+
/**
|
|
110
|
+
* Quoted table-name prefix. Empty string when no schema is configured;
|
|
111
|
+
* `"<schema>".` (with trailing dot) when a schema is set. Prepended to
|
|
112
|
+
* every dispatcher_X table reference in the SQL queries so they work
|
|
113
|
+
* even when Supabase's pooler drops the search_path option.
|
|
114
|
+
*/
|
|
115
|
+
private readonly tablePrefix;
|
|
116
|
+
private readonly subscribedEventTypes;
|
|
117
|
+
private listenClient;
|
|
118
|
+
private listenReconnectScheduled;
|
|
119
|
+
private listenReconnectAttempt;
|
|
120
|
+
private sleepAbort;
|
|
121
|
+
constructor(handlers: ReadonlyArray<RegisteredHandler>, options: ConsumerLoopOptions);
|
|
122
|
+
/**
|
|
123
|
+
* Start the polling loop with LISTEN/NOTIFY wake-up. Resolves when
|
|
124
|
+
* `stop()` is called and the current in-flight batch (if any) finishes
|
|
125
|
+
* draining; otherwise runs forever.
|
|
126
|
+
*
|
|
127
|
+
* The LISTEN connection is best-effort: if it fails to set up or drops
|
|
128
|
+
* mid-flight, the polling loop still drains at the configured cadence
|
|
129
|
+
* and reconnect attempts run with backoff. Sub-second wake-up is the
|
|
130
|
+
* happy path; degraded poll-cadence wake-up is the durable fallback.
|
|
131
|
+
*/
|
|
132
|
+
start(): Promise<void>;
|
|
133
|
+
/**
|
|
134
|
+
* Graceful shutdown. Sets the running flag false, wakes the polling
|
|
135
|
+
* loop early so it exits the current sleep without waiting for the
|
|
136
|
+
* full poll interval, then waits for the in-flight batch to drain
|
|
137
|
+
* before resolving. Closes the LISTEN connection on the way out.
|
|
138
|
+
*/
|
|
139
|
+
stop(): Promise<void>;
|
|
140
|
+
private getCursorLastSeq;
|
|
141
|
+
private findEventsAfter;
|
|
142
|
+
private getDedupHit;
|
|
143
|
+
/**
|
|
144
|
+
* Single-transaction dedup-insert + cursor-advance. The pg.PoolClient
|
|
145
|
+
* is checked out for the transaction so the BEGIN/COMMIT pair is
|
|
146
|
+
* scoped to one connection. Errors trigger a ROLLBACK and the loop
|
|
147
|
+
* retries via the normal retry path.
|
|
148
|
+
*/
|
|
149
|
+
private commitSuccessfulDelivery;
|
|
150
|
+
private commitDeadLetter;
|
|
151
|
+
private processBatchForEventType;
|
|
152
|
+
private deliverEvent;
|
|
153
|
+
private advanceCursor;
|
|
154
|
+
private sleep;
|
|
155
|
+
/**
|
|
156
|
+
* Detect whether a DSN points at a transaction-mode pooler (Supabase's
|
|
157
|
+
* pgbouncer at port 6543). LISTEN holds a connection open waiting for
|
|
158
|
+
* NOTIFY; transaction-mode pooling reuses connections across queries
|
|
159
|
+
* and rejects LISTEN with ENOIDENTIFIER. The fix is to skip the LISTEN
|
|
160
|
+
* client entirely against pooler URLs and rely on polling. Operators
|
|
161
|
+
* who want sub-second wake-up can either provide a session-mode URL
|
|
162
|
+
* (port 5432 / direct connection) via a future databaseUrl override
|
|
163
|
+
* or run a non-pooler DSN.
|
|
164
|
+
*
|
|
165
|
+
* Signal: `pgbouncer=true` query param OR port 6543 anywhere in the
|
|
166
|
+
* URL. Both are conventions Supabase generates for pooled URLs.
|
|
167
|
+
*/
|
|
168
|
+
private isPoolerDsn;
|
|
169
|
+
private startListenConnection;
|
|
170
|
+
private scheduleListenReconnect;
|
|
171
|
+
private stopListenConnection;
|
|
172
|
+
private onNotification;
|
|
173
|
+
private wakeUp;
|
|
174
|
+
private sleepOrWakeUp;
|
|
175
|
+
}
|