@pipeline-builder/api-server 0.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/LICENSE +202 -0
- package/README.md +79 -0
- package/lib/api/app-factory.d.ts +95 -0
- package/lib/api/app-factory.js +328 -0
- package/lib/api/check-quota.d.ts +13 -0
- package/lib/api/check-quota.js +74 -0
- package/lib/api/context-middleware.d.ts +34 -0
- package/lib/api/context-middleware.js +42 -0
- package/lib/api/etag-middleware.d.ts +7 -0
- package/lib/api/etag-middleware.js +44 -0
- package/lib/api/get-context.d.ts +22 -0
- package/lib/api/get-context.js +28 -0
- package/lib/api/health-checks.d.ts +62 -0
- package/lib/api/health-checks.js +87 -0
- package/lib/api/idempotency-middleware.d.ts +80 -0
- package/lib/api/idempotency-middleware.js +321 -0
- package/lib/api/index.d.ts +20 -0
- package/lib/api/index.js +34 -0
- package/lib/api/meter-quota.d.ts +32 -0
- package/lib/api/meter-quota.js +70 -0
- package/lib/api/metrics.d.ts +47 -0
- package/lib/api/metrics.js +221 -0
- package/lib/api/middleware-factory.d.ts +56 -0
- package/lib/api/middleware-factory.js +80 -0
- package/lib/api/middleware.d.ts +1 -0
- package/lib/api/middleware.js +8 -0
- package/lib/api/mongo-connect.d.ts +19 -0
- package/lib/api/mongo-connect.js +41 -0
- package/lib/api/quota-helpers.d.ts +23 -0
- package/lib/api/quota-helpers.js +22 -0
- package/lib/api/rate-limit-by-org.d.ts +29 -0
- package/lib/api/rate-limit-by-org.js +64 -0
- package/lib/api/readiness.d.ts +29 -0
- package/lib/api/readiness.js +65 -0
- package/lib/api/request-types.d.ts +55 -0
- package/lib/api/request-types.js +65 -0
- package/lib/api/require-org-id.d.ts +14 -0
- package/lib/api/require-org-id.js +28 -0
- package/lib/api/route-wrapper.d.ts +50 -0
- package/lib/api/route-wrapper.js +84 -0
- package/lib/api/server.d.ts +79 -0
- package/lib/api/server.js +234 -0
- package/lib/api/tenant-context.d.ts +28 -0
- package/lib/api/tenant-context.js +48 -0
- package/lib/api/tracing.d.ts +25 -0
- package/lib/api/tracing.js +69 -0
- package/lib/http/index.d.ts +4 -0
- package/lib/http/index.js +7 -0
- package/lib/http/sse-connection-manager.d.ts +357 -0
- package/lib/http/sse-connection-manager.js +665 -0
- package/lib/http/sse-relay.d.ts +63 -0
- package/lib/http/sse-relay.js +70 -0
- package/lib/http/sse-ticket-channel.d.ts +31 -0
- package/lib/http/sse-ticket-channel.js +52 -0
- package/lib/http/sse-ticket-store.d.ts +75 -0
- package/lib/http/sse-ticket-store.js +160 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +35 -0
- package/lib/otel-bootstrap.d.ts +1 -0
- package/lib/otel-bootstrap.js +75 -0
- package/package.json +183 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import type { SSERelay } from './sse-relay.js';
|
|
3
|
+
import { type SSEStreamTicketStore } from './sse-ticket-store.js';
|
|
4
|
+
/**
|
|
5
|
+
* Event types for SSE logging
|
|
6
|
+
*/
|
|
7
|
+
export type SSEEventType = 'INFO' | 'WARN' | 'ERROR' | 'COMPLETED' | 'ROLLBACK' | 'MESSAGE';
|
|
8
|
+
/**
|
|
9
|
+
* SSE payload structure
|
|
10
|
+
*/
|
|
11
|
+
export interface SSEPayload {
|
|
12
|
+
ts: string;
|
|
13
|
+
type: SSEEventType;
|
|
14
|
+
message: string;
|
|
15
|
+
data?: unknown;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* SSE client with connection tracking
|
|
19
|
+
*/
|
|
20
|
+
export interface SSEClient {
|
|
21
|
+
id: string;
|
|
22
|
+
res: Response;
|
|
23
|
+
connectedAt: number;
|
|
24
|
+
timeout: NodeJS.Timeout;
|
|
25
|
+
/** Number of consecutive backpressure events (write returned false). */
|
|
26
|
+
backpressureCount: number;
|
|
27
|
+
/** Owning org id — populated when the request was authenticated. Used to
|
|
28
|
+
* decrement the per-org counter on disconnect / cleanup. */
|
|
29
|
+
orgId?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* SSE Manager configuration options
|
|
33
|
+
*/
|
|
34
|
+
export interface SSEManagerOptions {
|
|
35
|
+
/** Maximum clients allowed per request ID (default: 10) */
|
|
36
|
+
maxClientsPerRequest?: number;
|
|
37
|
+
/** Client timeout in milliseconds (default: 30 minutes) */
|
|
38
|
+
clientTimeoutMs?: number;
|
|
39
|
+
/** Interval for cleanup checks in milliseconds (default: 5 minutes) */
|
|
40
|
+
cleanupIntervalMs?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Hard cap on total open connections per process. Defaults to 1000 from
|
|
43
|
+
* `SSE_MAX_TOTAL_CLIENTS`. New connections beyond this are rejected at
|
|
44
|
+
* `addClient()`. Tune up if your service serves > 1000 concurrent SSE
|
|
45
|
+
* dashboards, but be aware Node.js fd limits dominate above ~5000.
|
|
46
|
+
*/
|
|
47
|
+
maxTotalClients?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Per-org ceiling on concurrent SSE streams. Defaults to 50 from
|
|
50
|
+
* `SSE_MAX_CLIENTS_PER_ORG`. Protects against one noisy org consuming the
|
|
51
|
+
* process-wide budget — without it, an authenticated user can fan out
|
|
52
|
+
* thousands of streams (one per dashboard / log stream) and starve other
|
|
53
|
+
* orgs of connection slots. The cap is enforced only on `addClient` calls
|
|
54
|
+
* that carry an `orgId`; service-internal / anonymous SSE traffic skips it.
|
|
55
|
+
*/
|
|
56
|
+
maxClientsPerOrg?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Hard cap on total live (unconsumed, unexpired) SSE tickets across the
|
|
59
|
+
* process. Defaults to 1000 from `SSE_MAX_TOTAL_TICKETS`. Bounds memory
|
|
60
|
+
* under abuse — `createTicket` returns `{ ok: false, reason: 'capacity' }`
|
|
61
|
+
* once reached.
|
|
62
|
+
*/
|
|
63
|
+
maxTotalTickets?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Per-org cap on live SSE tickets. Defaults to 10 from
|
|
66
|
+
* `SSE_MAX_TICKETS_PER_ORG`. Prevents a single tenant from saturating the
|
|
67
|
+
* ticket table — `createTicket` returns `{ ok: false, reason: 'org-limit' }`
|
|
68
|
+
* once reached.
|
|
69
|
+
*/
|
|
70
|
+
maxTicketsPerOrg?: number;
|
|
71
|
+
/** Ticket TTL in ms (default: `SSE_TICKET_TTL_MS` from api-core). */
|
|
72
|
+
ticketTtlMs?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Ticket + stream-ownership backend. Defaults to an in-memory store (correct
|
|
75
|
+
* single-replica only). Inject `createEnvRedisTicketStore()` (or a bespoke
|
|
76
|
+
* `SSEStreamTicketStore`) so ticket mint/redeem AND stream ownership work across
|
|
77
|
+
* horizontally-scaled pods and — critically — so the platform stream producer
|
|
78
|
+
* (a different service) can bind ownership that this service reads.
|
|
79
|
+
*/
|
|
80
|
+
ticketStore?: SSEStreamTicketStore;
|
|
81
|
+
/**
|
|
82
|
+
* TTL for a stream-ownership binding (default: `SSE_STREAM_OWNER_TTL_MS` env or
|
|
83
|
+
* 1h). Long enough to outlive a build; the producer re-binds as needed.
|
|
84
|
+
*/
|
|
85
|
+
streamOwnerTtlMs?: number;
|
|
86
|
+
/**
|
|
87
|
+
* Cross-pod fan-out bus for `send()`/`broadcast()`. Optional — when omitted,
|
|
88
|
+
* delivery is LOCAL-ONLY (correct single-replica; today's behavior). Inject
|
|
89
|
+
* `createEnvRedisSSERelay()` so a frame produced on one pod reaches an
|
|
90
|
+
* EventSource attached on a DIFFERENT pod. Degrades to local-only automatically
|
|
91
|
+
* when Redis is unset (the factory returns null) or during a Redis outage.
|
|
92
|
+
*/
|
|
93
|
+
relay?: SSERelay;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A build-log stream subject id: either a dashed UUID (api-server's `uuid()`)
|
|
97
|
+
* or nginx's `$request_id` — 16 random bytes rendered as 32 hex chars with NO
|
|
98
|
+
* dashes. Accept both by making the group separators optional; still strictly
|
|
99
|
+
* 32 hex chars so it can't carry an injection / path-traversal payload into the
|
|
100
|
+
* SSE subject. Shared by the ticket-mint route and the stream middleware so
|
|
101
|
+
* both validate the subject identically.
|
|
102
|
+
*/
|
|
103
|
+
export declare const SSE_REQUEST_ID_RE: RegExp;
|
|
104
|
+
/** Result of {@link SSEManager.createTicket}. */
|
|
105
|
+
export type CreateTicketResult = {
|
|
106
|
+
ok: true;
|
|
107
|
+
ticket: string;
|
|
108
|
+
} | {
|
|
109
|
+
ok: false;
|
|
110
|
+
reason: 'org-limit' | 'capacity' | 'forbidden';
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* SSE Manager statistics
|
|
114
|
+
*/
|
|
115
|
+
export interface SSEManagerStats {
|
|
116
|
+
totalRequests: number;
|
|
117
|
+
totalClients: number;
|
|
118
|
+
oldestConnectionMs: number | null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* SSE helper class with memory leak protection
|
|
122
|
+
*
|
|
123
|
+
* Features:
|
|
124
|
+
* - Client limits per request ID
|
|
125
|
+
* - Automatic timeout for idle connections
|
|
126
|
+
* - Periodic cleanup of stale connections
|
|
127
|
+
* - Connection statistics
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* const sseManager = new SSEManager({ maxClientsPerRequest: 5 });
|
|
132
|
+
* app.get('/logs/:requestId', sseManager.middleware());
|
|
133
|
+
*
|
|
134
|
+
* // Send events
|
|
135
|
+
* sseManager.send('request-123', 'INFO', 'Processing...');
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export declare class SSEManager {
|
|
139
|
+
private clients;
|
|
140
|
+
/** Per-org open-client counters. Decremented on removeClient/cleanup so the
|
|
141
|
+
* map mirrors `clients[].orgId` counts at all times. Orgs reach zero are
|
|
142
|
+
* deleted to keep the map bounded. */
|
|
143
|
+
private orgClientCounts;
|
|
144
|
+
/** Ticket + stream-ownership backend (in-memory default; Redis for multi-pod).
|
|
145
|
+
* Bounded by `maxTotalTickets` / `maxTicketsPerOrg`, enforced in createTicket;
|
|
146
|
+
* expiry is validated at consume time and swept on the cleanup interval. */
|
|
147
|
+
private readonly ticketStore;
|
|
148
|
+
private readonly maxClientsPerRequest;
|
|
149
|
+
private readonly maxTotalClients;
|
|
150
|
+
private readonly maxClientsPerOrg;
|
|
151
|
+
private readonly maxTotalTickets;
|
|
152
|
+
private readonly maxTicketsPerOrg;
|
|
153
|
+
private readonly ticketTtlMs;
|
|
154
|
+
private readonly streamOwnerTtlMs;
|
|
155
|
+
private readonly clientTimeoutMs;
|
|
156
|
+
private cleanupInterval;
|
|
157
|
+
/** Cross-pod fan-out bus (undefined ⇒ local-only delivery). */
|
|
158
|
+
private readonly relay?;
|
|
159
|
+
/** This manager instance's id — tags relayed frames so we ignore our own echo. */
|
|
160
|
+
private readonly nodeId;
|
|
161
|
+
constructor(options?: SSEManagerOptions);
|
|
162
|
+
/**
|
|
163
|
+
* Handle a frame received from another pod via the relay. We already delivered
|
|
164
|
+
* our OWN frames to local clients before publishing, so ignore our echo. A
|
|
165
|
+
* `send` re-emits to the subject's local clients; a `broadcast` to all of them.
|
|
166
|
+
* Purely local — never re-publishes, so there is no fan-out loop.
|
|
167
|
+
*/
|
|
168
|
+
private onRelayMessage;
|
|
169
|
+
/** Total open connections across all requests. */
|
|
170
|
+
private totalClients;
|
|
171
|
+
/**
|
|
172
|
+
* Publish the current live-connection count as a gauge so on-call can see SSE
|
|
173
|
+
* saturation (approach to the per-process cap) on a dashboard — previously
|
|
174
|
+
* these numbers lived only in `getStats()`/logs. Called after every add/remove/
|
|
175
|
+
* close/cleanup. Cheap (O(R)); metric helpers never throw.
|
|
176
|
+
*/
|
|
177
|
+
private updateActiveGauge;
|
|
178
|
+
/** Current open-stream count for an org (0 if unseen). */
|
|
179
|
+
getOrgClientCount(orgId: string): number;
|
|
180
|
+
/**
|
|
181
|
+
* Canonical form of a requestId for equality checks: dashes stripped and
|
|
182
|
+
* lowercased, so the dashed (api-server `uuid()`) and undashed (nginx
|
|
183
|
+
* `$request_id`) renderings of the same id compare equal. Callers are
|
|
184
|
+
* expected to have already format-validated against {@link SSE_REQUEST_ID_RE}.
|
|
185
|
+
*/
|
|
186
|
+
private static normalizeRequestId;
|
|
187
|
+
/**
|
|
188
|
+
* Canonical form of an orgId for ownership equality checks: trimmed and
|
|
189
|
+
* lowercased. The stream PRODUCER (bindStreamOwner) and the ticket-minting
|
|
190
|
+
* CONSUMER (createTicket) run in different services, so a casing / whitespace
|
|
191
|
+
* drift between how each renders the same org would otherwise make the owner
|
|
192
|
+
* comparison in {@link createTicket} false-`forbidden` the real owner.
|
|
193
|
+
* Normalizing both sides — mirroring {@link normalizeRequestId} — closes that.
|
|
194
|
+
*/
|
|
195
|
+
private static normalizeOrgId;
|
|
196
|
+
/**
|
|
197
|
+
* Record the org that OWNS a stream subject. The stream PRODUCER calls this
|
|
198
|
+
* when it creates a build-log stream so that ticket minting can assert the
|
|
199
|
+
* caller's org owns the subject (see {@link createTicket}). Backed by the
|
|
200
|
+
* injected ticket store, so when a Redis store is wired the platform producer
|
|
201
|
+
* (a different service sharing the same Redis) can bind ownership that this
|
|
202
|
+
* service reads — closing the cross-tenant attach gap where any org could mint
|
|
203
|
+
* a ticket for a guessed requestId.
|
|
204
|
+
*
|
|
205
|
+
* @param requestId - The stream subject (format-validated by the caller).
|
|
206
|
+
* @param orgId - The owning org (normalized internally via normalizeOrgId).
|
|
207
|
+
*/
|
|
208
|
+
bindStreamOwner(requestId: string, orgId: string): Promise<void>;
|
|
209
|
+
/**
|
|
210
|
+
* Mint a short-lived, single-use SSE ticket bound to `orgId` AND a specific
|
|
211
|
+
* stream subject (`requestId`). Clients POST to obtain one (JWT-authenticated)
|
|
212
|
+
* for the exact stream they intend to open, then open the EventSource with
|
|
213
|
+
* `?ticket=<t>` so the JWT never lands in a query string / access log. The
|
|
214
|
+
* ticket can then only be consumed to open that one subject's stream — see
|
|
215
|
+
* {@link consumeTicket} — which is what enforces per-subject authorization.
|
|
216
|
+
*
|
|
217
|
+
* ORG-OWNERSHIP: if a stream owner has been bound for this subject (via
|
|
218
|
+
* {@link bindStreamOwner}) and it is a DIFFERENT org, minting is refused
|
|
219
|
+
* (`reason: 'forbidden'`) — an org cannot mint a ticket for another org's
|
|
220
|
+
* stream even if it guesses the requestId. When no owner is bound (producer
|
|
221
|
+
* wiring not yet present), minting falls back to binding the ticket to the
|
|
222
|
+
* caller's own org, preserving current behavior.
|
|
223
|
+
*
|
|
224
|
+
* Bounded by `maxTotalTickets` (process-wide) and `maxTicketsPerOrg`
|
|
225
|
+
* (per-tenant). Mirrors the message-service notifications ticket store.
|
|
226
|
+
*
|
|
227
|
+
* @param orgId - Owning org (normalized internally via normalizeOrgId).
|
|
228
|
+
* @param requestId - The build-log stream subject this ticket authorizes.
|
|
229
|
+
* The caller must have format-validated it (see {@link SSE_REQUEST_ID_RE}).
|
|
230
|
+
* @returns `{ ok: true, ticket }` on success, or `{ ok: false, reason }`
|
|
231
|
+
* where reason is `'org-limit'`, `'capacity'`, or `'forbidden'`.
|
|
232
|
+
*/
|
|
233
|
+
createTicket(orgId: string, requestId: string): Promise<CreateTicketResult>;
|
|
234
|
+
/**
|
|
235
|
+
* Validate and CONSUME a ticket for a specific stream subject. Single-use:
|
|
236
|
+
* the ticket is deleted whether or not it turns out to be valid, so a replay
|
|
237
|
+
* of the same value always fails. Returns the bound org on success, or null
|
|
238
|
+
* when the ticket is unknown / already-used / expired / OR was minted for a
|
|
239
|
+
* DIFFERENT `requestId` than the one being opened.
|
|
240
|
+
*
|
|
241
|
+
* The subject check is the authorization fix: without it, a ticket minted for
|
|
242
|
+
* one stream could be presented to open ANY stream the holder's org could
|
|
243
|
+
* name, letting an authenticated org attach to another org's log stream by
|
|
244
|
+
* guessing its requestId. The generic 401 the caller returns does not
|
|
245
|
+
* distinguish "unknown ticket" from "wrong subject", so it leaks nothing
|
|
246
|
+
* about which requestIds exist.
|
|
247
|
+
*
|
|
248
|
+
* @param ticketId - The opaque ticket value from `?ticket=`.
|
|
249
|
+
* @param requestId - The stream subject from the URL path (`:requestId`),
|
|
250
|
+
* already format-validated by the caller.
|
|
251
|
+
*/
|
|
252
|
+
consumeTicket(ticketId: string, requestId: string): Promise<{
|
|
253
|
+
orgId: string;
|
|
254
|
+
} | null>;
|
|
255
|
+
/**
|
|
256
|
+
* Adds a client to the SSE manager
|
|
257
|
+
*
|
|
258
|
+
* @param requestId - Unique request ID
|
|
259
|
+
* @param res - Express Response object
|
|
260
|
+
* @param orgId - Authenticated org id (optional). When set, enforces the
|
|
261
|
+
* per-org cap and the counter is decremented on disconnect/cleanup.
|
|
262
|
+
* @returns true if client was added, false if rejected (limit reached)
|
|
263
|
+
*/
|
|
264
|
+
addClient(requestId: string, res: Response, orgId?: string): boolean;
|
|
265
|
+
/** Drop one from the per-org counter. Idempotent: a counter at 0 stays at 0
|
|
266
|
+
* and the org key is removed from the map. */
|
|
267
|
+
private decrementOrgCount;
|
|
268
|
+
/**
|
|
269
|
+
* Removes a client from the manager
|
|
270
|
+
*/
|
|
271
|
+
private removeClient;
|
|
272
|
+
/**
|
|
273
|
+
* Sends a message to all SSE clients for a requestId
|
|
274
|
+
*
|
|
275
|
+
* @param requestId - Request ID
|
|
276
|
+
* @param type - Event type
|
|
277
|
+
* @param message - Message string
|
|
278
|
+
* @param data - Optional additional data
|
|
279
|
+
* @returns Number of clients the message was sent to
|
|
280
|
+
*/
|
|
281
|
+
send(requestId: string, type: SSEEventType, message: string, data?: unknown): number;
|
|
282
|
+
/**
|
|
283
|
+
* Write a pre-built payload to this pod's LOCAL clients for `requestId`. Shared
|
|
284
|
+
* by {@link send} (local half) and the relay re-emit path ({@link onRelayMessage}),
|
|
285
|
+
* so a relayed frame reuses the producer's original `ts` rather than a new one.
|
|
286
|
+
* Never relays — callers decide whether to publish.
|
|
287
|
+
*/
|
|
288
|
+
private sendLocal;
|
|
289
|
+
/**
|
|
290
|
+
* Broadcast a message to all connected clients across all requests
|
|
291
|
+
*
|
|
292
|
+
* @param type - Event type
|
|
293
|
+
* @param message - Message string
|
|
294
|
+
* @param data - Optional additional data
|
|
295
|
+
* @returns Total number of clients the message was sent to
|
|
296
|
+
*/
|
|
297
|
+
broadcast(type: SSEEventType, message: string, data?: unknown): number;
|
|
298
|
+
/** Write a pre-built payload to all LOCAL clients across every request. */
|
|
299
|
+
private broadcastLocal;
|
|
300
|
+
/** Fire-and-forget relay publish. No-op (local-only) when no relay is wired. */
|
|
301
|
+
private publishRelay;
|
|
302
|
+
/**
|
|
303
|
+
* Close all clients for a specific request
|
|
304
|
+
*
|
|
305
|
+
* @param requestId - Request ID to close
|
|
306
|
+
* @param finalMessage - Optional final message to send before closing
|
|
307
|
+
*/
|
|
308
|
+
closeRequest(requestId: string, finalMessage?: string): void;
|
|
309
|
+
/**
|
|
310
|
+
* Get statistics about current connections
|
|
311
|
+
*/
|
|
312
|
+
getStats(): SSEManagerStats;
|
|
313
|
+
/**
|
|
314
|
+
* Check if a request has any connected clients
|
|
315
|
+
*/
|
|
316
|
+
hasClients(requestId: string): boolean;
|
|
317
|
+
/**
|
|
318
|
+
* Get the number of clients for a specific request
|
|
319
|
+
*/
|
|
320
|
+
getClientCount(requestId: string): number;
|
|
321
|
+
/**
|
|
322
|
+
* Middleware to initialize a ticket-authenticated SSE connection.
|
|
323
|
+
*
|
|
324
|
+
* The stream is NOT open to the world: the caller must first exchange its
|
|
325
|
+
* JWT for a short-lived single-use ticket (see {@link createTicket}, wired to
|
|
326
|
+
* `POST /logs/ticket`) and pass it as `?ticket=<t>`. The middleware validates
|
|
327
|
+
* + consumes the ticket, binds the connection to the ticket's org, and
|
|
328
|
+
* enforces the per-org connection cap. A missing / invalid / expired /
|
|
329
|
+
* already-used ticket is rejected with 401 before any SSE headers flush.
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```typescript
|
|
333
|
+
* app.get('/logs/:requestId', sseManager.middleware());
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
middleware(): (req: {
|
|
337
|
+
params: {
|
|
338
|
+
requestId: string;
|
|
339
|
+
};
|
|
340
|
+
query?: {
|
|
341
|
+
ticket?: unknown;
|
|
342
|
+
};
|
|
343
|
+
}, res: Response) => Promise<void>;
|
|
344
|
+
/**
|
|
345
|
+
* Start periodic cleanup of stale connections
|
|
346
|
+
*/
|
|
347
|
+
private startCleanupInterval;
|
|
348
|
+
/**
|
|
349
|
+
* Clean up stale connections using single-pass partition.
|
|
350
|
+
* O(R × C) instead of O(R × C²), and avoids mutation-during-iteration.
|
|
351
|
+
*/
|
|
352
|
+
private cleanup;
|
|
353
|
+
/**
|
|
354
|
+
* Shutdown the SSE manager and close all connections
|
|
355
|
+
*/
|
|
356
|
+
shutdown(): void;
|
|
357
|
+
}
|