@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
package/dist/inbox.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Domain-side dispatcher inbox helpers.
|
|
4
|
+
*
|
|
5
|
+
* Per the 2026-05-12 topology decision (memos/2026-05-12-platform-cross-db-
|
|
6
|
+
* consumer-dsn-upstream): Platform's fanout worker POSTs events to each
|
|
7
|
+
* consumer domain's webhook endpoint. The endpoint pattern is roughly:
|
|
8
|
+
*
|
|
9
|
+
* POST /api/dispatcher/inbox
|
|
10
|
+
*
|
|
11
|
+
* with these headers from Platform's fanout:
|
|
12
|
+
*
|
|
13
|
+
* x-sguild-event-id: evt_<UUIDv7>
|
|
14
|
+
* x-sguild-event-type: <event_type>
|
|
15
|
+
* x-sguild-signature: sha256=<hex>
|
|
16
|
+
*
|
|
17
|
+
* and the JSON-serialized envelope as the request body. This module
|
|
18
|
+
* carries the two pieces every domain inbox needs:
|
|
19
|
+
*
|
|
20
|
+
* 1. `verifyInboxRequest(...)` — validates the HMAC signature against
|
|
21
|
+
* the raw request body using the domain's shared secret, returns the
|
|
22
|
+
* parsed envelope on success or a typed error on failure.
|
|
23
|
+
*
|
|
24
|
+
* 2. `dedupOnEventId(...)` — wraps an idempotency check in the domain's
|
|
25
|
+
* own database. Domains track which event_ids they have already
|
|
26
|
+
* processed; a duplicate hit returns early without re-running the
|
|
27
|
+
* handler. The dedup table is domain-owned, not Platform's.
|
|
28
|
+
*
|
|
29
|
+
* Domains import these helpers from `@/lib/dispatcher/inbox` once they
|
|
30
|
+
* vendor Platform's dispatcher SDK. The handler shape is the domain's
|
|
31
|
+
* own concern — this module does not own routing; it owns signature
|
|
32
|
+
* verification and the dedup contract.
|
|
33
|
+
*
|
|
34
|
+
* The reference route at `app/api/dispatcher/inbox/route.ts` in this
|
|
35
|
+
* repo shows the full assembly: verify, parse, dedup, dispatch, ack.
|
|
36
|
+
*/
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.verifyInboxRequest = verifyInboxRequest;
|
|
39
|
+
exports.tryInsertOrDetectConflict = tryInsertOrDetectConflict;
|
|
40
|
+
const signature_1 = require("./signature");
|
|
41
|
+
/**
|
|
42
|
+
* Verify a Platform-fanout-style POST request against the domain's
|
|
43
|
+
* shared HMAC secret. The signature is computed over the raw request
|
|
44
|
+
* body, so callers MUST pass the exact bytes received (not a re-
|
|
45
|
+
* stringified JSON object). The standard pattern in a Next.js route
|
|
46
|
+
* handler is:
|
|
47
|
+
*
|
|
48
|
+
* const rawBody = await req.text();
|
|
49
|
+
* const result = verifyInboxRequest(rawBody, req.headers, secret);
|
|
50
|
+
*
|
|
51
|
+
* Returns the parsed envelope on success. Failures map to the right
|
|
52
|
+
* HTTP status: 401 for signature mismatch, 400 for missing header or
|
|
53
|
+
* malformed body. The route handler returns that status verbatim.
|
|
54
|
+
*
|
|
55
|
+
* On 5xx-shaped failures (database errors during dedup, handler
|
|
56
|
+
* throws, etc.), the route handler returns 500 separately; Platform's
|
|
57
|
+
* fanout retries on 5xx and dead-letters on permanent failure. 4xx
|
|
58
|
+
* responses also retry once per Platform's ConsumerLoop retry budget
|
|
59
|
+
* (4xx may indicate a deploy-in-progress secret rotation that is
|
|
60
|
+
* transient).
|
|
61
|
+
*/
|
|
62
|
+
function verifyInboxRequest(rawBody, headers, secret) {
|
|
63
|
+
const signature = headers.get(signature_1.SIGNATURE_HEADER);
|
|
64
|
+
if (!signature) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
status: 400,
|
|
68
|
+
reason: `missing ${signature_1.SIGNATURE_HEADER} header`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (!(0, signature_1.verifyEnvelopeSignature)(rawBody, secret, signature)) {
|
|
72
|
+
return { ok: false, status: 401, reason: "invalid HMAC signature" };
|
|
73
|
+
}
|
|
74
|
+
let envelope;
|
|
75
|
+
try {
|
|
76
|
+
envelope = JSON.parse(rawBody);
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
status: 400,
|
|
82
|
+
reason: `request body is not valid JSON: ${e instanceof Error ? e.message : String(e)}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (typeof envelope.event_id !== "string" || envelope.event_id.length === 0) {
|
|
86
|
+
return { ok: false, status: 400, reason: "envelope missing event_id" };
|
|
87
|
+
}
|
|
88
|
+
if (typeof envelope.event_type !== "string" || envelope.event_type.length === 0) {
|
|
89
|
+
return { ok: false, status: 400, reason: "envelope missing event_type" };
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, envelope, rawBody };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Helper for the common case where the dedup INSERT throws on conflict
|
|
95
|
+
* with a recognizable Prisma error code. Wrap a Prisma insert and pass
|
|
96
|
+
* the throwing function in. The helper inspects the thrown error for
|
|
97
|
+
* Prisma's unique-violation code (P2002) and translates to
|
|
98
|
+
* `already_processed`; any other error rethrows.
|
|
99
|
+
*
|
|
100
|
+
* Usage:
|
|
101
|
+
*
|
|
102
|
+
* const outcome = await tryInsertOrDetectConflict(async () => {
|
|
103
|
+
* await prisma.dispatcherInboxDedup.create({
|
|
104
|
+
* data: { eventId, eventType }
|
|
105
|
+
* });
|
|
106
|
+
* });
|
|
107
|
+
* if (outcome === "already_processed") return new Response(null, { status: 200 });
|
|
108
|
+
*/
|
|
109
|
+
async function tryInsertOrDetectConflict(insert) {
|
|
110
|
+
try {
|
|
111
|
+
await insert();
|
|
112
|
+
return "first_time";
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
const code = e?.code;
|
|
116
|
+
if (code === "P2002")
|
|
117
|
+
return "already_processed";
|
|
118
|
+
throw e;
|
|
119
|
+
}
|
|
120
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatcher SDK public surface.
|
|
3
|
+
*
|
|
4
|
+
* Domain code imports from `@/lib/dispatcher`, never from the internal
|
|
5
|
+
* files directly. The barrel re-exports the singleton dispatcher, the
|
|
6
|
+
* envelope types, the registry query functions, the runtime config
|
|
7
|
+
* helpers, the publish/consumer option shapes, and the error classes
|
|
8
|
+
* callers may need to handle.
|
|
9
|
+
*
|
|
10
|
+
* Internal files (transport implementations, dedup tracking, registry
|
|
11
|
+
* cache management, validator internals) are not re-exported. The narrow
|
|
12
|
+
* public surface keeps domains from depending on internal layout that
|
|
13
|
+
* may change as the remaining Slice 3b through Slice 5 land.
|
|
14
|
+
*
|
|
15
|
+
* Phase 2 status: see ./README.md for what's implemented and what's
|
|
16
|
+
* pending. publish wires to the Postgres-queue transport; subscribe +
|
|
17
|
+
* start + stop wire the polling worker; LISTEN/NOTIFY wake-up
|
|
18
|
+
* (Slice 3b) and the per-consumer DLQ read API (Slice 5) remain.
|
|
19
|
+
*/
|
|
20
|
+
export { dispatcher } from "./dispatcher";
|
|
21
|
+
export { DispatcherNotImplementedError, UnregisteredEventTypeError, UnregisteredSchemaVersionError, } from "./dispatcher";
|
|
22
|
+
export { configureDispatcher, type DispatcherConfig, } from "./config";
|
|
23
|
+
export type { PublishOptions } from "./postgres-transport";
|
|
24
|
+
export type { ConsumerLoopOptions } from "./postgres-consumer";
|
|
25
|
+
export { EnvelopeValidationError, PayloadValidationError, PayloadSchemaUnavailableError, } from "./validator";
|
|
26
|
+
export { DeadLetterAlreadyResolvedError, DeadLetterNotFoundError, ProducerDbNotConfiguredError, ReplayConsumerUnknownError, ReplayDeliveryFailedError, getDeadLetter, knownProducers, listDeadLetters, replayDeadLetter, resolveDeadLetter, } from "./dlq";
|
|
27
|
+
export type { DeadLetter, GetDeadLetterOptions, ListDeadLettersFilter, ReplayDeadLetterInput, ReplayDeadLetterOutcome, ResolveDeadLetterInput, } from "./dlq";
|
|
28
|
+
export { configureDispatcherObservability, resetDispatcherObservabilityForTests, } from "./observability";
|
|
29
|
+
export type { DispatcherHistogramName, DispatcherMetricLabels, DispatcherMetricName, DispatcherObservabilityHooks, } from "./observability";
|
|
30
|
+
export type { ActorRef, CanonicalEntityId, EventEmit, EventEnvelope, LiveProducerDomain, ProducerDomain, PublishResult, SubscribeHandler, } from "./types";
|
|
31
|
+
export { getActiveSchemaVersion, getEventTypeRegistration, loadRegistry, } from "./registry";
|
|
32
|
+
export type { EventTypeRegistration, RegistryManifest, SchemaVersionEntry, SchemaVersionStatus, } from "./registry";
|
|
33
|
+
export { tryInsertOrDetectConflict, verifyInboxRequest, } from "./inbox";
|
|
34
|
+
export type { DedupAttempt, DedupOutcome, InboxVerificationResult, } from "./inbox";
|
|
35
|
+
export { EVENT_ID_HEADER, EVENT_TYPE_HEADER, SIGNATURE_HEADER, signEnvelope, verifyEnvelopeSignature, } from "./signature";
|
|
36
|
+
export { WebhookDispatchError } from "./fanout";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dispatcher SDK public surface.
|
|
4
|
+
*
|
|
5
|
+
* Domain code imports from `@/lib/dispatcher`, never from the internal
|
|
6
|
+
* files directly. The barrel re-exports the singleton dispatcher, the
|
|
7
|
+
* envelope types, the registry query functions, the runtime config
|
|
8
|
+
* helpers, the publish/consumer option shapes, and the error classes
|
|
9
|
+
* callers may need to handle.
|
|
10
|
+
*
|
|
11
|
+
* Internal files (transport implementations, dedup tracking, registry
|
|
12
|
+
* cache management, validator internals) are not re-exported. The narrow
|
|
13
|
+
* public surface keeps domains from depending on internal layout that
|
|
14
|
+
* may change as the remaining Slice 3b through Slice 5 land.
|
|
15
|
+
*
|
|
16
|
+
* Phase 2 status: see ./README.md for what's implemented and what's
|
|
17
|
+
* pending. publish wires to the Postgres-queue transport; subscribe +
|
|
18
|
+
* start + stop wire the polling worker; LISTEN/NOTIFY wake-up
|
|
19
|
+
* (Slice 3b) and the per-consumer DLQ read API (Slice 5) remain.
|
|
20
|
+
*/
|
|
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;
|
|
23
|
+
var dispatcher_1 = require("./dispatcher");
|
|
24
|
+
Object.defineProperty(exports, "dispatcher", { enumerable: true, get: function () { return dispatcher_1.dispatcher; } });
|
|
25
|
+
var dispatcher_2 = require("./dispatcher");
|
|
26
|
+
Object.defineProperty(exports, "DispatcherNotImplementedError", { enumerable: true, get: function () { return dispatcher_2.DispatcherNotImplementedError; } });
|
|
27
|
+
Object.defineProperty(exports, "UnregisteredEventTypeError", { enumerable: true, get: function () { return dispatcher_2.UnregisteredEventTypeError; } });
|
|
28
|
+
Object.defineProperty(exports, "UnregisteredSchemaVersionError", { enumerable: true, get: function () { return dispatcher_2.UnregisteredSchemaVersionError; } });
|
|
29
|
+
var config_1 = require("./config");
|
|
30
|
+
Object.defineProperty(exports, "configureDispatcher", { enumerable: true, get: function () { return config_1.configureDispatcher; } });
|
|
31
|
+
var validator_1 = require("./validator");
|
|
32
|
+
Object.defineProperty(exports, "EnvelopeValidationError", { enumerable: true, get: function () { return validator_1.EnvelopeValidationError; } });
|
|
33
|
+
Object.defineProperty(exports, "PayloadValidationError", { enumerable: true, get: function () { return validator_1.PayloadValidationError; } });
|
|
34
|
+
Object.defineProperty(exports, "PayloadSchemaUnavailableError", { enumerable: true, get: function () { return validator_1.PayloadSchemaUnavailableError; } });
|
|
35
|
+
var dlq_1 = require("./dlq");
|
|
36
|
+
Object.defineProperty(exports, "DeadLetterAlreadyResolvedError", { enumerable: true, get: function () { return dlq_1.DeadLetterAlreadyResolvedError; } });
|
|
37
|
+
Object.defineProperty(exports, "DeadLetterNotFoundError", { enumerable: true, get: function () { return dlq_1.DeadLetterNotFoundError; } });
|
|
38
|
+
Object.defineProperty(exports, "ProducerDbNotConfiguredError", { enumerable: true, get: function () { return dlq_1.ProducerDbNotConfiguredError; } });
|
|
39
|
+
Object.defineProperty(exports, "ReplayConsumerUnknownError", { enumerable: true, get: function () { return dlq_1.ReplayConsumerUnknownError; } });
|
|
40
|
+
Object.defineProperty(exports, "ReplayDeliveryFailedError", { enumerable: true, get: function () { return dlq_1.ReplayDeliveryFailedError; } });
|
|
41
|
+
Object.defineProperty(exports, "getDeadLetter", { enumerable: true, get: function () { return dlq_1.getDeadLetter; } });
|
|
42
|
+
Object.defineProperty(exports, "knownProducers", { enumerable: true, get: function () { return dlq_1.knownProducers; } });
|
|
43
|
+
Object.defineProperty(exports, "listDeadLetters", { enumerable: true, get: function () { return dlq_1.listDeadLetters; } });
|
|
44
|
+
Object.defineProperty(exports, "replayDeadLetter", { enumerable: true, get: function () { return dlq_1.replayDeadLetter; } });
|
|
45
|
+
Object.defineProperty(exports, "resolveDeadLetter", { enumerable: true, get: function () { return dlq_1.resolveDeadLetter; } });
|
|
46
|
+
var observability_1 = require("./observability");
|
|
47
|
+
Object.defineProperty(exports, "configureDispatcherObservability", { enumerable: true, get: function () { return observability_1.configureDispatcherObservability; } });
|
|
48
|
+
Object.defineProperty(exports, "resetDispatcherObservabilityForTests", { enumerable: true, get: function () { return observability_1.resetDispatcherObservabilityForTests; } });
|
|
49
|
+
var registry_1 = require("./registry");
|
|
50
|
+
Object.defineProperty(exports, "getActiveSchemaVersion", { enumerable: true, get: function () { return registry_1.getActiveSchemaVersion; } });
|
|
51
|
+
Object.defineProperty(exports, "getEventTypeRegistration", { enumerable: true, get: function () { return registry_1.getEventTypeRegistration; } });
|
|
52
|
+
Object.defineProperty(exports, "loadRegistry", { enumerable: true, get: function () { return registry_1.loadRegistry; } });
|
|
53
|
+
// Inbox helpers consumed by domain webhook endpoints (verify HMAC,
|
|
54
|
+
// dedup on event_id). Platform-side fanout machinery is internal and
|
|
55
|
+
// not re-exported here; instrumentation.ts imports it directly from
|
|
56
|
+
// ./fanout.
|
|
57
|
+
var inbox_1 = require("./inbox");
|
|
58
|
+
Object.defineProperty(exports, "tryInsertOrDetectConflict", { enumerable: true, get: function () { return inbox_1.tryInsertOrDetectConflict; } });
|
|
59
|
+
Object.defineProperty(exports, "verifyInboxRequest", { enumerable: true, get: function () { return inbox_1.verifyInboxRequest; } });
|
|
60
|
+
// HMAC sign/verify primitives live in signature.ts (low-dep, vendorable
|
|
61
|
+
// stand-alone). The fanout module re-exports them for legacy imports;
|
|
62
|
+
// new code imports from the canonical here.
|
|
63
|
+
var signature_1 = require("./signature");
|
|
64
|
+
Object.defineProperty(exports, "EVENT_ID_HEADER", { enumerable: true, get: function () { return signature_1.EVENT_ID_HEADER; } });
|
|
65
|
+
Object.defineProperty(exports, "EVENT_TYPE_HEADER", { enumerable: true, get: function () { return signature_1.EVENT_TYPE_HEADER; } });
|
|
66
|
+
Object.defineProperty(exports, "SIGNATURE_HEADER", { enumerable: true, get: function () { return signature_1.SIGNATURE_HEADER; } });
|
|
67
|
+
Object.defineProperty(exports, "signEnvelope", { enumerable: true, get: function () { return signature_1.signEnvelope; } });
|
|
68
|
+
Object.defineProperty(exports, "verifyEnvelopeSignature", { enumerable: true, get: function () { return signature_1.verifyEnvelopeSignature; } });
|
|
69
|
+
var fanout_1 = require("./fanout");
|
|
70
|
+
Object.defineProperty(exports, "WebhookDispatchError", { enumerable: true, get: function () { return fanout_1.WebhookDispatchError; } });
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical entity ID generation per ADR-0002.
|
|
3
|
+
*
|
|
4
|
+
* Vendored into the dispatcher SDK so the published `@sguild/dispatcher`
|
|
5
|
+
* package carries no dependency on the platform repo's `lib/id.ts`. Kept
|
|
6
|
+
* byte-for-byte in step with that source; if the canonical implementation
|
|
7
|
+
* changes, re-sync this copy. The implementation is dependency-free (only
|
|
8
|
+
* `node:crypto`) per RFC 9562 §5.7, so vendoring is a literal copy.
|
|
9
|
+
*
|
|
10
|
+
* The dispatcher uses `uuidv7()` for envelope `event_id` values and
|
|
11
|
+
* dead-letter row ids; `prefixedId` and `isUuidV7` are carried for parity
|
|
12
|
+
* with the source module and may be used by future SDK code.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Generate a UUID v7 in canonical form per RFC 9562 §5.7.
|
|
16
|
+
*
|
|
17
|
+
* Layout:
|
|
18
|
+
* - 48 bits: Unix timestamp in milliseconds, big-endian (bytes 0-5)
|
|
19
|
+
* - 4 bits: version (set to `0111` = 7) (high nibble of byte 6)
|
|
20
|
+
* - 12 bits: random data (low nibble of byte 6 + byte 7)
|
|
21
|
+
* - 2 bits: variant (set to `10`) (high two bits of byte 8)
|
|
22
|
+
* - 62 bits: random data (remaining 62 bits)
|
|
23
|
+
*
|
|
24
|
+
* The timestamp prefix gives lexicographic sort order matching insertion
|
|
25
|
+
* order, which is the property that makes v7 valuable for database keys.
|
|
26
|
+
*
|
|
27
|
+
* @returns A 36-character UUID string in the canonical 8-4-4-4-12 form.
|
|
28
|
+
*/
|
|
29
|
+
export declare function uuidv7(): string;
|
|
30
|
+
/**
|
|
31
|
+
* Generate a prefixed canonical entity ID per ADR-0002. Convenience wrapper
|
|
32
|
+
* that combines `uuidv7()` with the prefix.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* prefixedId("per_") // "per_018f3c2d-4a5b-7c8d-9e0f-1a2b3c4d5e6f"
|
|
36
|
+
*/
|
|
37
|
+
export declare function prefixedId(prefix: string): string;
|
|
38
|
+
export declare function isUuidV7(value: string): boolean;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical entity ID generation per ADR-0002.
|
|
4
|
+
*
|
|
5
|
+
* Vendored into the dispatcher SDK so the published `@sguild/dispatcher`
|
|
6
|
+
* package carries no dependency on the platform repo's `lib/id.ts`. Kept
|
|
7
|
+
* byte-for-byte in step with that source; if the canonical implementation
|
|
8
|
+
* changes, re-sync this copy. The implementation is dependency-free (only
|
|
9
|
+
* `node:crypto`) per RFC 9562 §5.7, so vendoring is a literal copy.
|
|
10
|
+
*
|
|
11
|
+
* The dispatcher uses `uuidv7()` for envelope `event_id` values and
|
|
12
|
+
* dead-letter row ids; `prefixedId` and `isUuidV7` are carried for parity
|
|
13
|
+
* with the source module and may be used by future SDK code.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.uuidv7 = uuidv7;
|
|
17
|
+
exports.prefixedId = prefixedId;
|
|
18
|
+
exports.isUuidV7 = isUuidV7;
|
|
19
|
+
const node_crypto_1 = require("node:crypto");
|
|
20
|
+
/**
|
|
21
|
+
* Generate a UUID v7 in canonical form per RFC 9562 §5.7.
|
|
22
|
+
*
|
|
23
|
+
* Layout:
|
|
24
|
+
* - 48 bits: Unix timestamp in milliseconds, big-endian (bytes 0-5)
|
|
25
|
+
* - 4 bits: version (set to `0111` = 7) (high nibble of byte 6)
|
|
26
|
+
* - 12 bits: random data (low nibble of byte 6 + byte 7)
|
|
27
|
+
* - 2 bits: variant (set to `10`) (high two bits of byte 8)
|
|
28
|
+
* - 62 bits: random data (remaining 62 bits)
|
|
29
|
+
*
|
|
30
|
+
* The timestamp prefix gives lexicographic sort order matching insertion
|
|
31
|
+
* order, which is the property that makes v7 valuable for database keys.
|
|
32
|
+
*
|
|
33
|
+
* @returns A 36-character UUID string in the canonical 8-4-4-4-12 form.
|
|
34
|
+
*/
|
|
35
|
+
function uuidv7() {
|
|
36
|
+
const timestamp = BigInt(Date.now());
|
|
37
|
+
const bytes = new Uint8Array(16);
|
|
38
|
+
// Bytes 0-5: 48-bit timestamp, big-endian.
|
|
39
|
+
bytes[0] = Number((timestamp >> 40n) & 0xffn);
|
|
40
|
+
bytes[1] = Number((timestamp >> 32n) & 0xffn);
|
|
41
|
+
bytes[2] = Number((timestamp >> 24n) & 0xffn);
|
|
42
|
+
bytes[3] = Number((timestamp >> 16n) & 0xffn);
|
|
43
|
+
bytes[4] = Number((timestamp >> 8n) & 0xffn);
|
|
44
|
+
bytes[5] = Number(timestamp & 0xffn);
|
|
45
|
+
// Bytes 6-15: 80 bits of random data (we'll then overlay version + variant).
|
|
46
|
+
const rand = (0, node_crypto_1.randomBytes)(10);
|
|
47
|
+
for (let i = 0; i < 10; i++) {
|
|
48
|
+
bytes[6 + i] = rand[i];
|
|
49
|
+
}
|
|
50
|
+
// Set version bits (high nibble of byte 6 to 0111 = 7).
|
|
51
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
|
52
|
+
// Set variant bits (high two bits of byte 8 to 10).
|
|
53
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
54
|
+
// Format as canonical UUID string (8-4-4-4-12).
|
|
55
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
56
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Generate a prefixed canonical entity ID per ADR-0002. Convenience wrapper
|
|
60
|
+
* that combines `uuidv7()` with the prefix.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* prefixedId("per_") // "per_018f3c2d-4a5b-7c8d-9e0f-1a2b3c4d5e6f"
|
|
64
|
+
*/
|
|
65
|
+
function prefixedId(prefix) {
|
|
66
|
+
if (!prefix.endsWith("_")) {
|
|
67
|
+
throw new Error(`prefixedId: prefix '${prefix}' must end with '_' per ADR-0002`);
|
|
68
|
+
}
|
|
69
|
+
return `${prefix}${uuidv7()}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Validate the canonical UUID v7 form (lowercase hex, dashed, version
|
|
73
|
+
* nibble = 7, variant nibble = 8/9/a/b). Useful at storage boundaries.
|
|
74
|
+
*/
|
|
75
|
+
const UUID_V7_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
76
|
+
function isUuidV7(value) {
|
|
77
|
+
return UUID_V7_PATTERN.test(value);
|
|
78
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres search_path config helper.
|
|
3
|
+
*
|
|
4
|
+
* Vendored into the dispatcher SDK so the published `@sguild/dispatcher`
|
|
5
|
+
* package carries no dependency on the platform repo's
|
|
6
|
+
* `lib/db/pg-search-path.ts`. Kept byte-for-byte in step with that source;
|
|
7
|
+
* the implementation is dependency-free (only the WHATWG `URL`), so
|
|
8
|
+
* vendoring is a literal copy.
|
|
9
|
+
*
|
|
10
|
+
* Prisma treats `?schema=<name>` in a Postgres URL as the target schema.
|
|
11
|
+
* node-postgres does not. Dispatcher consumers use raw pg pools so they can
|
|
12
|
+
* poll producer schemas without Prisma's generated schema qualification, so
|
|
13
|
+
* they must translate Prisma's URL convention into Postgres search_path.
|
|
14
|
+
*
|
|
15
|
+
* Caveat: Supabase's pgbouncer at port 6543 (transaction-mode pooling) silently
|
|
16
|
+
* drops the `options` startup parameter. Setting search_path via the libpq
|
|
17
|
+
* `options=-c search_path=...` mechanism does not survive the pooler. The
|
|
18
|
+
* options field stays here for direct-connection (port 5432) callers, but the
|
|
19
|
+
* caller MUST NOT rely on it alone — for pooler DSNs, schema-qualify the SQL
|
|
20
|
+
* queries explicitly using the `schema` field returned here.
|
|
21
|
+
*/
|
|
22
|
+
export type PgSearchPathConfig = {
|
|
23
|
+
connectionString: string | undefined;
|
|
24
|
+
/** libpq startup options. Effective on direct connections; dropped by pgbouncer. */
|
|
25
|
+
options?: string;
|
|
26
|
+
/**
|
|
27
|
+
* The schema name parsed from the DSN's `?schema=<name>` query param.
|
|
28
|
+
* Null if absent or `public`. Callers schema-qualify their SQL with this
|
|
29
|
+
* value (e.g., `"${schema}".dispatcher_cursor`) so the query works regardless
|
|
30
|
+
* of whether the pooler dropped the search_path option.
|
|
31
|
+
*/
|
|
32
|
+
schema: string | null;
|
|
33
|
+
};
|
|
34
|
+
export declare function pgConfigWithSearchPath(connectionString: string | undefined): PgSearchPathConfig;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Postgres search_path config helper.
|
|
4
|
+
*
|
|
5
|
+
* Vendored into the dispatcher SDK so the published `@sguild/dispatcher`
|
|
6
|
+
* package carries no dependency on the platform repo's
|
|
7
|
+
* `lib/db/pg-search-path.ts`. Kept byte-for-byte in step with that source;
|
|
8
|
+
* the implementation is dependency-free (only the WHATWG `URL`), so
|
|
9
|
+
* vendoring is a literal copy.
|
|
10
|
+
*
|
|
11
|
+
* Prisma treats `?schema=<name>` in a Postgres URL as the target schema.
|
|
12
|
+
* node-postgres does not. Dispatcher consumers use raw pg pools so they can
|
|
13
|
+
* poll producer schemas without Prisma's generated schema qualification, so
|
|
14
|
+
* they must translate Prisma's URL convention into Postgres search_path.
|
|
15
|
+
*
|
|
16
|
+
* Caveat: Supabase's pgbouncer at port 6543 (transaction-mode pooling) silently
|
|
17
|
+
* drops the `options` startup parameter. Setting search_path via the libpq
|
|
18
|
+
* `options=-c search_path=...` mechanism does not survive the pooler. The
|
|
19
|
+
* options field stays here for direct-connection (port 5432) callers, but the
|
|
20
|
+
* caller MUST NOT rely on it alone — for pooler DSNs, schema-qualify the SQL
|
|
21
|
+
* queries explicitly using the `schema` field returned here.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.pgConfigWithSearchPath = pgConfigWithSearchPath;
|
|
25
|
+
function pgConfigWithSearchPath(connectionString) {
|
|
26
|
+
if (!connectionString) {
|
|
27
|
+
return { connectionString, schema: null };
|
|
28
|
+
}
|
|
29
|
+
const schema = readSchemaParam(connectionString);
|
|
30
|
+
if (!schema) {
|
|
31
|
+
return { connectionString, schema: null };
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
connectionString,
|
|
35
|
+
options: `-c search_path=${[schema, "public", "extensions"].join(",")}`,
|
|
36
|
+
schema,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function readSchemaParam(connectionString) {
|
|
40
|
+
let url;
|
|
41
|
+
try {
|
|
42
|
+
url = new URL(connectionString);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const schema = url.searchParams.get("schema")?.trim();
|
|
48
|
+
if (!schema || schema === "public") {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(schema)) {
|
|
52
|
+
throw new Error(`Invalid Postgres schema name in connection URL: ${schema}`);
|
|
53
|
+
}
|
|
54
|
+
return schema;
|
|
55
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a path inside the bundled contracts tree to an absolute path.
|
|
3
|
+
*
|
|
4
|
+
* The dispatcher SDK ships the contracts tree (`contracts/`) bundled inside
|
|
5
|
+
* the `@sguild/dispatcher` package so the registry manifest and the JSON
|
|
6
|
+
* Schemas are available at runtime with no dependency on a sibling
|
|
7
|
+
* coordination repo.
|
|
8
|
+
*
|
|
9
|
+
* `contractsRelPath` is a path rooted at `contracts/` exactly as the
|
|
10
|
+
* coordination repo stores it (e.g. `contracts/event-types-registry.json`,
|
|
11
|
+
* or a registry `payload_schema` value like
|
|
12
|
+
* `contracts/<contract>/schema/payloads/<event_type>-v<n>.json`).
|
|
13
|
+
*
|
|
14
|
+
* Resolution order (first existing path wins):
|
|
15
|
+
*
|
|
16
|
+
* 1. `priorityPaths` — explicit caller overrides and test fixtures,
|
|
17
|
+
* resolved against the process cwd (absolute paths pass through).
|
|
18
|
+
* 2. Module-relative — `moduleDir` (the caller's `__dirname`) plus the
|
|
19
|
+
* relative path, and one level up. Reliable under plain Node / tsx,
|
|
20
|
+
* which is how the consumer-side subscriber worker runs.
|
|
21
|
+
* 3. cwd-relative defaults — covers the cases where `__dirname` is
|
|
22
|
+
* unreliable because a bundler (e.g. Next.js) rewrote it in a server
|
|
23
|
+
* bundle. `lib/dispatcher/<rel>` is the in-platform-repo source
|
|
24
|
+
* layout; `node_modules/@sguild/dispatcher/<rel>` is the published
|
|
25
|
+
* package; `../coordination/<rel>` is a sibling coordination repo.
|
|
26
|
+
* 4. `fallbackPaths` — last-resort caller overrides.
|
|
27
|
+
*
|
|
28
|
+
* Note for consumers whose producer path runs inside a bundled server
|
|
29
|
+
* (Next.js route handlers): ensure the deploy ships the package's
|
|
30
|
+
* `contracts/` directory. Next.js does not trace `readFileSync` data files
|
|
31
|
+
* automatically; add `node_modules/@sguild/dispatcher/contracts/**` to
|
|
32
|
+
* `outputFileTracingIncludes` (or equivalent) so the cwd-relative
|
|
33
|
+
* `node_modules/@sguild/dispatcher` candidate resolves at runtime.
|
|
34
|
+
*/
|
|
35
|
+
export type ResolveContractPathOptions = {
|
|
36
|
+
/** Tried before any default location. Explicit caller overrides, test fixtures. */
|
|
37
|
+
priorityPaths?: string[];
|
|
38
|
+
/** Tried after all default locations. Last-resort caller overrides. */
|
|
39
|
+
fallbackPaths?: string[];
|
|
40
|
+
};
|
|
41
|
+
export declare function resolveContractPath(moduleDir: string, contractsRelPath: string, options?: ResolveContractPathOptions): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a path inside the bundled contracts tree to an absolute path.
|
|
4
|
+
*
|
|
5
|
+
* The dispatcher SDK ships the contracts tree (`contracts/`) bundled inside
|
|
6
|
+
* the `@sguild/dispatcher` package so the registry manifest and the JSON
|
|
7
|
+
* Schemas are available at runtime with no dependency on a sibling
|
|
8
|
+
* coordination repo.
|
|
9
|
+
*
|
|
10
|
+
* `contractsRelPath` is a path rooted at `contracts/` exactly as the
|
|
11
|
+
* coordination repo stores it (e.g. `contracts/event-types-registry.json`,
|
|
12
|
+
* or a registry `payload_schema` value like
|
|
13
|
+
* `contracts/<contract>/schema/payloads/<event_type>-v<n>.json`).
|
|
14
|
+
*
|
|
15
|
+
* Resolution order (first existing path wins):
|
|
16
|
+
*
|
|
17
|
+
* 1. `priorityPaths` — explicit caller overrides and test fixtures,
|
|
18
|
+
* resolved against the process cwd (absolute paths pass through).
|
|
19
|
+
* 2. Module-relative — `moduleDir` (the caller's `__dirname`) plus the
|
|
20
|
+
* relative path, and one level up. Reliable under plain Node / tsx,
|
|
21
|
+
* which is how the consumer-side subscriber worker runs.
|
|
22
|
+
* 3. cwd-relative defaults — covers the cases where `__dirname` is
|
|
23
|
+
* unreliable because a bundler (e.g. Next.js) rewrote it in a server
|
|
24
|
+
* bundle. `lib/dispatcher/<rel>` is the in-platform-repo source
|
|
25
|
+
* layout; `node_modules/@sguild/dispatcher/<rel>` is the published
|
|
26
|
+
* package; `../coordination/<rel>` is a sibling coordination repo.
|
|
27
|
+
* 4. `fallbackPaths` — last-resort caller overrides.
|
|
28
|
+
*
|
|
29
|
+
* Note for consumers whose producer path runs inside a bundled server
|
|
30
|
+
* (Next.js route handlers): ensure the deploy ships the package's
|
|
31
|
+
* `contracts/` directory. Next.js does not trace `readFileSync` data files
|
|
32
|
+
* automatically; add `node_modules/@sguild/dispatcher/contracts/**` to
|
|
33
|
+
* `outputFileTracingIncludes` (or equivalent) so the cwd-relative
|
|
34
|
+
* `node_modules/@sguild/dispatcher` candidate resolves at runtime.
|
|
35
|
+
*/
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.resolveContractPath = resolveContractPath;
|
|
38
|
+
const node_fs_1 = require("node:fs");
|
|
39
|
+
const node_path_1 = require("node:path");
|
|
40
|
+
/**
|
|
41
|
+
* cwd-relative base directories the bundled contracts tree may live under,
|
|
42
|
+
* tried after the module-relative candidates.
|
|
43
|
+
*/
|
|
44
|
+
const CWD_RELATIVE_BASES = [
|
|
45
|
+
"lib/dispatcher", // in-platform-repo source consumption
|
|
46
|
+
"node_modules/@sguild/dispatcher", // published-package consumption
|
|
47
|
+
"../coordination", // checkout with a sibling coordination repo
|
|
48
|
+
];
|
|
49
|
+
function resolveContractPath(moduleDir, contractsRelPath, options = {}) {
|
|
50
|
+
const priorityPaths = options.priorityPaths ?? [];
|
|
51
|
+
const candidates = [
|
|
52
|
+
...priorityPaths.map((p) => (0, node_path_1.resolve)(process.cwd(), p)),
|
|
53
|
+
(0, node_path_1.join)(moduleDir, contractsRelPath), // in-repo source layout
|
|
54
|
+
(0, node_path_1.join)(moduleDir, "..", contractsRelPath), // published dist/ layout
|
|
55
|
+
...CWD_RELATIVE_BASES.map((base) => (0, node_path_1.resolve)(process.cwd(), base, contractsRelPath)),
|
|
56
|
+
...(options.fallbackPaths ?? []).map((p) => (0, node_path_1.resolve)(process.cwd(), p)),
|
|
57
|
+
];
|
|
58
|
+
for (const candidate of candidates) {
|
|
59
|
+
if ((0, node_fs_1.existsSync)(candidate))
|
|
60
|
+
return candidate;
|
|
61
|
+
}
|
|
62
|
+
// Nothing matched; return the first module-relative candidate so the
|
|
63
|
+
// caller's readFileSync throws an error naming the expected location.
|
|
64
|
+
return candidates[priorityPaths.length];
|
|
65
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatcher observability hooks.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3 intentionally exposes a small adapter rather than binding the
|
|
5
|
+
* SDK to a metrics vendor. Deployments can bridge these hooks to
|
|
6
|
+
* Prometheus, OpenTelemetry, Datadog, logs, or a domain-local test probe.
|
|
7
|
+
* The dispatcher never lets hook failures affect delivery.
|
|
8
|
+
*/
|
|
9
|
+
export type DispatcherMetricName = "dispatcher.publish.count" | "dispatcher.consume.count" | "dispatcher.dedup_hit.count" | "dispatcher.dead_letter.count";
|
|
10
|
+
export type DispatcherHistogramName = "dispatcher.end_to_end_latency_ms" | "dispatcher.handler_latency_ms";
|
|
11
|
+
export type DispatcherMetricLabels = {
|
|
12
|
+
event_type: string;
|
|
13
|
+
consumer?: string;
|
|
14
|
+
producer?: string;
|
|
15
|
+
schema_version?: string;
|
|
16
|
+
};
|
|
17
|
+
export type DispatcherObservabilityHooks = {
|
|
18
|
+
increment?: (name: DispatcherMetricName, value: number, labels: DispatcherMetricLabels) => void;
|
|
19
|
+
observe?: (name: DispatcherHistogramName, valueMs: number, labels: DispatcherMetricLabels) => void;
|
|
20
|
+
};
|
|
21
|
+
export declare function configureDispatcherObservability(next: DispatcherObservabilityHooks): void;
|
|
22
|
+
export declare function resetDispatcherObservabilityForTests(): void;
|
|
23
|
+
export declare function recordDispatcherIncrement(name: DispatcherMetricName, labels: DispatcherMetricLabels, value?: number): void;
|
|
24
|
+
export declare function recordDispatcherObservation(name: DispatcherHistogramName, valueMs: number, labels: DispatcherMetricLabels): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dispatcher observability hooks.
|
|
4
|
+
*
|
|
5
|
+
* Phase 3 intentionally exposes a small adapter rather than binding the
|
|
6
|
+
* SDK to a metrics vendor. Deployments can bridge these hooks to
|
|
7
|
+
* Prometheus, OpenTelemetry, Datadog, logs, or a domain-local test probe.
|
|
8
|
+
* The dispatcher never lets hook failures affect delivery.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.configureDispatcherObservability = configureDispatcherObservability;
|
|
12
|
+
exports.resetDispatcherObservabilityForTests = resetDispatcherObservabilityForTests;
|
|
13
|
+
exports.recordDispatcherIncrement = recordDispatcherIncrement;
|
|
14
|
+
exports.recordDispatcherObservation = recordDispatcherObservation;
|
|
15
|
+
let hooks = {};
|
|
16
|
+
function configureDispatcherObservability(next) {
|
|
17
|
+
hooks = next;
|
|
18
|
+
}
|
|
19
|
+
function resetDispatcherObservabilityForTests() {
|
|
20
|
+
hooks = {};
|
|
21
|
+
}
|
|
22
|
+
function recordDispatcherIncrement(name, labels, value = 1) {
|
|
23
|
+
try {
|
|
24
|
+
hooks.increment?.(name, value, labels);
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
console.warn(`dispatcher observability increment hook failed for ${name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function recordDispatcherObservation(name, valueMs, labels) {
|
|
31
|
+
try {
|
|
32
|
+
hooks.observe?.(name, valueMs, labels);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
console.warn(`dispatcher observability observe hook failed for ${name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
36
|
+
}
|
|
37
|
+
}
|