opinionated-machine 5.1.0 → 6.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 +966 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/AbstractController.d.ts +3 -3
- package/dist/lib/AbstractController.js.map +1 -1
- package/dist/lib/AbstractModule.d.ts +23 -1
- package/dist/lib/AbstractModule.js +25 -0
- package/dist/lib/AbstractModule.js.map +1 -1
- package/dist/lib/DIContext.d.ts +35 -0
- package/dist/lib/DIContext.js +108 -1
- package/dist/lib/DIContext.js.map +1 -1
- package/dist/lib/resolverFunctions.d.ts +34 -0
- package/dist/lib/resolverFunctions.js +47 -0
- package/dist/lib/resolverFunctions.js.map +1 -1
- package/dist/lib/sse/AbstractSSEController.d.ts +163 -0
- package/dist/lib/sse/AbstractSSEController.js +228 -0
- package/dist/lib/sse/AbstractSSEController.js.map +1 -0
- package/dist/lib/sse/SSEConnectionSpy.d.ts +55 -0
- package/dist/lib/sse/SSEConnectionSpy.js +136 -0
- package/dist/lib/sse/SSEConnectionSpy.js.map +1 -0
- package/dist/lib/sse/index.d.ts +5 -0
- package/dist/lib/sse/index.js +6 -0
- package/dist/lib/sse/index.js.map +1 -0
- package/dist/lib/sse/sseContracts.d.ts +132 -0
- package/dist/lib/sse/sseContracts.js +102 -0
- package/dist/lib/sse/sseContracts.js.map +1 -0
- package/dist/lib/sse/sseParser.d.ts +167 -0
- package/dist/lib/sse/sseParser.js +225 -0
- package/dist/lib/sse/sseParser.js.map +1 -0
- package/dist/lib/sse/sseRouteBuilder.d.ts +47 -0
- package/dist/lib/sse/sseRouteBuilder.js +114 -0
- package/dist/lib/sse/sseRouteBuilder.js.map +1 -0
- package/dist/lib/sse/sseTypes.d.ts +164 -0
- package/dist/lib/sse/sseTypes.js +2 -0
- package/dist/lib/sse/sseTypes.js.map +1 -0
- package/dist/lib/testing/index.d.ts +5 -0
- package/dist/lib/testing/index.js +5 -0
- package/dist/lib/testing/index.js.map +1 -0
- package/dist/lib/testing/sseHttpClient.d.ts +203 -0
- package/dist/lib/testing/sseHttpClient.js +262 -0
- package/dist/lib/testing/sseHttpClient.js.map +1 -0
- package/dist/lib/testing/sseInjectClient.d.ts +173 -0
- package/dist/lib/testing/sseInjectClient.js +234 -0
- package/dist/lib/testing/sseInjectClient.js.map +1 -0
- package/dist/lib/testing/sseInjectHelpers.d.ts +59 -0
- package/dist/lib/testing/sseInjectHelpers.js +117 -0
- package/dist/lib/testing/sseInjectHelpers.js.map +1 -0
- package/dist/lib/testing/sseTestServer.d.ts +93 -0
- package/dist/lib/testing/sseTestServer.js +108 -0
- package/dist/lib/testing/sseTestServer.js.map +1 -0
- package/dist/lib/testing/sseTestTypes.d.ts +106 -0
- package/dist/lib/testing/sseTestTypes.js +2 -0
- package/dist/lib/testing/sseTestTypes.js.map +1 -0
- package/package.json +13 -11
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { SSEConnectionSpy } from "./SSEConnectionSpy.js";
|
|
2
|
+
export { SSEConnectionSpy } from "./SSEConnectionSpy.js";
|
|
3
|
+
/**
|
|
4
|
+
* Abstract base class for SSE controllers.
|
|
5
|
+
*
|
|
6
|
+
* Provides connection management, broadcasting, and lifecycle hooks.
|
|
7
|
+
* Extend this class to create SSE controllers that handle real-time
|
|
8
|
+
* streaming connections.
|
|
9
|
+
*
|
|
10
|
+
* @template APIContracts - Map of route names to SSE route definitions
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* class NotificationsSSEController extends AbstractSSEController<typeof contracts> {
|
|
15
|
+
* public static contracts = {
|
|
16
|
+
* notifications: buildSSERoute({ ... }),
|
|
17
|
+
* } as const
|
|
18
|
+
*
|
|
19
|
+
* public buildSSERoutes() {
|
|
20
|
+
* return {
|
|
21
|
+
* notifications: {
|
|
22
|
+
* contract: NotificationsSSEController.contracts.notifications,
|
|
23
|
+
* handler: this.handleNotifications,
|
|
24
|
+
* },
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export class AbstractSSEController {
|
|
31
|
+
/** Map of connection ID to connection object */
|
|
32
|
+
connections = new Map();
|
|
33
|
+
/** Private storage for connection spy */
|
|
34
|
+
_connectionSpy;
|
|
35
|
+
/**
|
|
36
|
+
* SSE controllers must override this constructor and call super with their
|
|
37
|
+
* dependencies object and the SSE config.
|
|
38
|
+
*
|
|
39
|
+
* @param _dependencies - The dependencies object (cradle proxy in awilix)
|
|
40
|
+
* @param sseConfig - Optional SSE controller configuration
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* class MySSEController extends AbstractSSEController<MyContracts> {
|
|
45
|
+
* private myService: MyService
|
|
46
|
+
*
|
|
47
|
+
* constructor(deps: { myService: MyService }, sseConfig?: SSEControllerConfig) {
|
|
48
|
+
* super(deps, sseConfig)
|
|
49
|
+
* this.myService = deps.myService
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
constructor(_dependencies, sseConfig) {
|
|
55
|
+
if (sseConfig?.enableConnectionSpy) {
|
|
56
|
+
this._connectionSpy = new SSEConnectionSpy();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get the connection spy for testing.
|
|
61
|
+
* Throws an error if spies are not enabled.
|
|
62
|
+
* Enable spies by passing `{ enableConnectionSpy: true }` to the constructor.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* // In test, create controller with spy enabled
|
|
67
|
+
* // Pass dependencies first, then config with enableConnectionSpy
|
|
68
|
+
* const controller = new MySSEController({}, { enableConnectionSpy: true })
|
|
69
|
+
*
|
|
70
|
+
* // Start connection (async)
|
|
71
|
+
* connectSSE(baseUrl, '/api/stream')
|
|
72
|
+
*
|
|
73
|
+
* // Wait for connection - handles race condition
|
|
74
|
+
* const connection = await controller.connectionSpy.waitForConnection()
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* @throws Error if connection spy is not enabled
|
|
78
|
+
*/
|
|
79
|
+
get connectionSpy() {
|
|
80
|
+
if (!this._connectionSpy) {
|
|
81
|
+
throw new Error('Connection spy is not enabled. Pass { enableConnectionSpy: true } to the constructor. ' +
|
|
82
|
+
'This should only be used in test environments.');
|
|
83
|
+
}
|
|
84
|
+
return this._connectionSpy;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Send an event to a specific connection.
|
|
88
|
+
*
|
|
89
|
+
* @param connectionId - The connection to send to
|
|
90
|
+
* @param message - The SSE message to send
|
|
91
|
+
* @returns true if sent successfully, false if connection not found or closed
|
|
92
|
+
*/
|
|
93
|
+
async sendEvent(connectionId, message) {
|
|
94
|
+
const connection = this.connections.get(connectionId);
|
|
95
|
+
if (!connection) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const reply = connection.reply;
|
|
100
|
+
// @fastify/sse handles JSON serialization internally, so pass data as-is
|
|
101
|
+
await reply.sse.send({
|
|
102
|
+
data: message.data,
|
|
103
|
+
event: message.event,
|
|
104
|
+
id: message.id,
|
|
105
|
+
retry: message.retry,
|
|
106
|
+
});
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Send failed - connection is likely closed (client disconnected, network error, etc.)
|
|
111
|
+
// Remove from tracking to prevent further send attempts to a dead connection
|
|
112
|
+
// Use unregisterConnection to ensure hooks and spy are notified
|
|
113
|
+
this.unregisterConnection(connectionId);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Broadcast an event to all connected clients.
|
|
119
|
+
*
|
|
120
|
+
* @param message - The SSE message to broadcast
|
|
121
|
+
* @returns Number of clients the message was sent to
|
|
122
|
+
*/
|
|
123
|
+
async broadcast(message) {
|
|
124
|
+
let sent = 0;
|
|
125
|
+
const connectionIds = Array.from(this.connections.keys());
|
|
126
|
+
for (const id of connectionIds) {
|
|
127
|
+
if (await this.sendEvent(id, message)) {
|
|
128
|
+
sent++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return sent;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Broadcast an event to connections matching a predicate.
|
|
135
|
+
*
|
|
136
|
+
* @param message - The SSE message to broadcast
|
|
137
|
+
* @param predicate - Function to filter connections
|
|
138
|
+
* @returns Number of clients the message was sent to
|
|
139
|
+
*/
|
|
140
|
+
async broadcastIf(message, predicate) {
|
|
141
|
+
let sent = 0;
|
|
142
|
+
for (const [id, connection] of this.connections) {
|
|
143
|
+
if (predicate(connection) && (await this.sendEvent(id, message))) {
|
|
144
|
+
sent++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return sent;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get all active connections.
|
|
151
|
+
*/
|
|
152
|
+
getConnections() {
|
|
153
|
+
return Array.from(this.connections.values());
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get the number of active connections.
|
|
157
|
+
*/
|
|
158
|
+
getConnectionCount() {
|
|
159
|
+
return this.connections.size;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Close a specific connection.
|
|
163
|
+
*
|
|
164
|
+
* This gracefully ends the SSE stream by calling the underlying `reply.sse.close()`.
|
|
165
|
+
* All previously sent data is flushed to the client before the connection terminates.
|
|
166
|
+
* Use this to signal end-of-stream after sending all events (e.g., in request-response
|
|
167
|
+
* style streaming like OpenAI completions).
|
|
168
|
+
*
|
|
169
|
+
* @param connectionId - The connection to close
|
|
170
|
+
* @returns true if connection was found and closed
|
|
171
|
+
*/
|
|
172
|
+
closeConnection(connectionId) {
|
|
173
|
+
const connection = this.connections.get(connectionId);
|
|
174
|
+
if (!connection) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const reply = connection.reply;
|
|
179
|
+
reply.sse.close();
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Connection may already be closed
|
|
183
|
+
}
|
|
184
|
+
// Use unregisterConnection to ensure hooks and spy are notified
|
|
185
|
+
this.unregisterConnection(connectionId);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Close all active connections.
|
|
190
|
+
* Called during graceful shutdown via asyncDispose.
|
|
191
|
+
*/
|
|
192
|
+
closeAllConnections() {
|
|
193
|
+
const connectionIds = Array.from(this.connections.keys());
|
|
194
|
+
for (const id of connectionIds) {
|
|
195
|
+
this.closeConnection(id);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Register a connection (called internally by route builder).
|
|
200
|
+
* Triggers the onConnectionEstablished hook and spy if defined.
|
|
201
|
+
* @internal
|
|
202
|
+
*/
|
|
203
|
+
registerConnection(connection) {
|
|
204
|
+
this.connections.set(connection.id, connection);
|
|
205
|
+
this.onConnectionEstablished?.(connection);
|
|
206
|
+
// Notify spy after hook (so hook can set context before spy sees it)
|
|
207
|
+
this._connectionSpy?.addConnection(connection);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Unregister a connection (called internally by route builder).
|
|
211
|
+
* Triggers the onConnectionClosed hook and spy if defined.
|
|
212
|
+
* This method is idempotent - calling it multiple times for the same
|
|
213
|
+
* connection ID has no effect after the first call.
|
|
214
|
+
* @internal
|
|
215
|
+
*/
|
|
216
|
+
unregisterConnection(connectionId) {
|
|
217
|
+
const connection = this.connections.get(connectionId);
|
|
218
|
+
if (!connection) {
|
|
219
|
+
// Already unregistered or never existed - do nothing (idempotent)
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
this.onConnectionClosed?.(connection);
|
|
223
|
+
// Notify spy of disconnection
|
|
224
|
+
this._connectionSpy?.addDisconnection(connectionId);
|
|
225
|
+
this.connections.delete(connectionId);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=AbstractSSEController.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AbstractSSEController.js","sourceRoot":"","sources":["../../../lib/sse/AbstractSSEController.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { SSEConnection } from './AbstractSSEController.ts';
|
|
2
|
+
export type SSEConnectionEvent = {
|
|
3
|
+
type: 'connect' | 'disconnect';
|
|
4
|
+
connectionId: string;
|
|
5
|
+
connection?: SSEConnection;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Connection spy for testing SSE controllers.
|
|
9
|
+
* Tracks connection and disconnection events separately.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SSEConnectionSpy {
|
|
12
|
+
private events;
|
|
13
|
+
private activeConnections;
|
|
14
|
+
private claimedConnections;
|
|
15
|
+
private connectionWaiters;
|
|
16
|
+
private disconnectionWaiters;
|
|
17
|
+
/** @internal Called when a connection is established */
|
|
18
|
+
addConnection(connection: SSEConnection): void;
|
|
19
|
+
/** @internal Called when a connection is closed */
|
|
20
|
+
addDisconnection(connectionId: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Wait for a connection to be established.
|
|
23
|
+
*
|
|
24
|
+
* @param options.timeout - Timeout in milliseconds (default: 5000)
|
|
25
|
+
* @param options.predicate - Optional predicate to match a specific connection.
|
|
26
|
+
* When provided, waits for an unclaimed connection that matches the predicate.
|
|
27
|
+
* Connections are "claimed" when returned by waitForConnection, allowing
|
|
28
|
+
* multiple sequential waits for the same URL path.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* // Wait for any connection
|
|
33
|
+
* const conn = await spy.waitForConnection()
|
|
34
|
+
*
|
|
35
|
+
* // Wait for a connection with specific URL
|
|
36
|
+
* const conn = await spy.waitForConnection({
|
|
37
|
+
* predicate: (c) => c.request.url.includes('/api/notifications'),
|
|
38
|
+
* })
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
waitForConnection(options?: {
|
|
42
|
+
timeout?: number;
|
|
43
|
+
predicate?: (connection: SSEConnection) => boolean;
|
|
44
|
+
}): Promise<SSEConnection>;
|
|
45
|
+
/** Wait for a specific connection to disconnect */
|
|
46
|
+
waitForDisconnection(connectionId: string, options?: {
|
|
47
|
+
timeout?: number;
|
|
48
|
+
}): Promise<void>;
|
|
49
|
+
/** Check if a connection is currently active */
|
|
50
|
+
isConnected(connectionId: string): boolean;
|
|
51
|
+
/** Get all connection events in order, optionally filtered by connectionId */
|
|
52
|
+
getEvents(connectionId?: string): SSEConnectionEvent[];
|
|
53
|
+
/** Clear all events and cancel pending waiters */
|
|
54
|
+
clear(): void;
|
|
55
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection spy for testing SSE controllers.
|
|
3
|
+
* Tracks connection and disconnection events separately.
|
|
4
|
+
*/
|
|
5
|
+
export class SSEConnectionSpy {
|
|
6
|
+
events = [];
|
|
7
|
+
activeConnections = new Set();
|
|
8
|
+
claimedConnections = new Set();
|
|
9
|
+
connectionWaiters = [];
|
|
10
|
+
disconnectionWaiters = [];
|
|
11
|
+
/** @internal Called when a connection is established */
|
|
12
|
+
addConnection(connection) {
|
|
13
|
+
this.events.push({ type: 'connect', connectionId: connection.id, connection });
|
|
14
|
+
this.activeConnections.add(connection.id);
|
|
15
|
+
// Find and resolve first matching connection waiter
|
|
16
|
+
const waiterIndex = this.connectionWaiters.findIndex((w) => !w.predicate || w.predicate(connection));
|
|
17
|
+
if (waiterIndex !== -1) {
|
|
18
|
+
// biome-ignore lint/style/noNonNullAssertion: we just received this index
|
|
19
|
+
const waiter = this.connectionWaiters[waiterIndex];
|
|
20
|
+
this.connectionWaiters.splice(waiterIndex, 1);
|
|
21
|
+
clearTimeout(waiter.timeoutId);
|
|
22
|
+
this.claimedConnections.add(connection.id);
|
|
23
|
+
waiter.resolve(connection);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** @internal Called when a connection is closed */
|
|
27
|
+
addDisconnection(connectionId) {
|
|
28
|
+
this.events.push({ type: 'disconnect', connectionId });
|
|
29
|
+
this.activeConnections.delete(connectionId);
|
|
30
|
+
// Resolve all pending disconnection waiters for this connection
|
|
31
|
+
const matchingWaiters = this.disconnectionWaiters.filter((w) => w.connectionId === connectionId);
|
|
32
|
+
for (const waiter of matchingWaiters) {
|
|
33
|
+
clearTimeout(waiter.timeoutId);
|
|
34
|
+
waiter.resolve();
|
|
35
|
+
}
|
|
36
|
+
this.disconnectionWaiters = this.disconnectionWaiters.filter((w) => w.connectionId !== connectionId);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Wait for a connection to be established.
|
|
40
|
+
*
|
|
41
|
+
* @param options.timeout - Timeout in milliseconds (default: 5000)
|
|
42
|
+
* @param options.predicate - Optional predicate to match a specific connection.
|
|
43
|
+
* When provided, waits for an unclaimed connection that matches the predicate.
|
|
44
|
+
* Connections are "claimed" when returned by waitForConnection, allowing
|
|
45
|
+
* multiple sequential waits for the same URL path.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* // Wait for any connection
|
|
50
|
+
* const conn = await spy.waitForConnection()
|
|
51
|
+
*
|
|
52
|
+
* // Wait for a connection with specific URL
|
|
53
|
+
* const conn = await spy.waitForConnection({
|
|
54
|
+
* predicate: (c) => c.request.url.includes('/api/notifications'),
|
|
55
|
+
* })
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
waitForConnection(options) {
|
|
59
|
+
const timeout = options?.timeout ?? 5000;
|
|
60
|
+
const predicate = options?.predicate;
|
|
61
|
+
// Check if a matching unclaimed connection already exists (must still be active)
|
|
62
|
+
const connectEvent = this.events.find((e) => e.type === 'connect' &&
|
|
63
|
+
e.connection &&
|
|
64
|
+
!this.claimedConnections.has(e.connection.id) &&
|
|
65
|
+
this.activeConnections.has(e.connection.id) &&
|
|
66
|
+
(!predicate || predicate(e.connection)));
|
|
67
|
+
if (connectEvent?.connection) {
|
|
68
|
+
this.claimedConnections.add(connectEvent.connection.id);
|
|
69
|
+
return Promise.resolve(connectEvent.connection);
|
|
70
|
+
}
|
|
71
|
+
// No matching connection yet, create a waiter
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const timeoutId = setTimeout(() => {
|
|
74
|
+
const index = this.connectionWaiters.findIndex((w) => w.resolve === resolve);
|
|
75
|
+
if (index !== -1) {
|
|
76
|
+
this.connectionWaiters.splice(index, 1);
|
|
77
|
+
}
|
|
78
|
+
reject(new Error(`Timeout waiting for connection after ${timeout}ms`));
|
|
79
|
+
}, timeout);
|
|
80
|
+
this.connectionWaiters.push({ resolve, reject, timeoutId, predicate });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Wait for a specific connection to disconnect */
|
|
84
|
+
waitForDisconnection(connectionId, options) {
|
|
85
|
+
const timeout = options?.timeout ?? 5000;
|
|
86
|
+
// Check if already disconnected
|
|
87
|
+
const hasDisconnected = this.events.some((e) => e.type === 'disconnect' && e.connectionId === connectionId);
|
|
88
|
+
if (hasDisconnected) {
|
|
89
|
+
return Promise.resolve();
|
|
90
|
+
}
|
|
91
|
+
// Not disconnected yet, create a waiter
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const waiter = {
|
|
94
|
+
connectionId,
|
|
95
|
+
resolve,
|
|
96
|
+
reject,
|
|
97
|
+
timeoutId: setTimeout(() => {
|
|
98
|
+
const index = this.disconnectionWaiters.indexOf(waiter);
|
|
99
|
+
if (index !== -1) {
|
|
100
|
+
this.disconnectionWaiters.splice(index, 1);
|
|
101
|
+
}
|
|
102
|
+
reject(new Error(`Timeout waiting for disconnection after ${timeout}ms`));
|
|
103
|
+
}, timeout),
|
|
104
|
+
};
|
|
105
|
+
this.disconnectionWaiters.push(waiter);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/** Check if a connection is currently active */
|
|
109
|
+
isConnected(connectionId) {
|
|
110
|
+
return this.activeConnections.has(connectionId);
|
|
111
|
+
}
|
|
112
|
+
/** Get all connection events in order, optionally filtered by connectionId */
|
|
113
|
+
getEvents(connectionId) {
|
|
114
|
+
if (connectionId === undefined) {
|
|
115
|
+
return [...this.events];
|
|
116
|
+
}
|
|
117
|
+
return this.events.filter((e) => e.connectionId === connectionId);
|
|
118
|
+
}
|
|
119
|
+
/** Clear all events and cancel pending waiters */
|
|
120
|
+
clear() {
|
|
121
|
+
this.events = [];
|
|
122
|
+
this.activeConnections.clear();
|
|
123
|
+
this.claimedConnections.clear();
|
|
124
|
+
for (const waiter of this.connectionWaiters) {
|
|
125
|
+
clearTimeout(waiter.timeoutId);
|
|
126
|
+
waiter.reject(new Error('ConnectionSpy was cleared'));
|
|
127
|
+
}
|
|
128
|
+
for (const waiter of this.disconnectionWaiters) {
|
|
129
|
+
clearTimeout(waiter.timeoutId);
|
|
130
|
+
waiter.reject(new Error('ConnectionSpy was cleared'));
|
|
131
|
+
}
|
|
132
|
+
this.connectionWaiters = [];
|
|
133
|
+
this.disconnectionWaiters = [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=SSEConnectionSpy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SSEConnectionSpy.js","sourceRoot":"","sources":["../../../lib/sse/SSEConnectionSpy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { AbstractSSEController, type BuildSSERoutesReturnType, type InferSSERequest, type SSEConnection, type SSEConnectionEvent, type SSEControllerConfig, type SSEHandlerConfig, type SSELogger, type SSEMessage, type SSEPreHandler, type SSERouteHandler, type SSERouteOptions, } from './AbstractSSEController.js';
|
|
2
|
+
export { SSEConnectionSpy } from './SSEConnectionSpy.js';
|
|
3
|
+
export { type AnySSERouteDefinition, buildPayloadSSERoute, buildSSEHandler, buildSSERoute, type PayloadSSERouteConfig, type SSEMethod, type SSERouteConfig, type SSERouteDefinition, } from './sseContracts.js';
|
|
4
|
+
export { type ParsedSSEEvent, type ParseSSEBufferResult, parseSSEBuffer, parseSSEEvents, } from './sseParser.js';
|
|
5
|
+
export { buildFastifySSERoute, type RegisterSSERoutesOptions } from './sseRouteBuilder.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { AbstractSSEController, } from './AbstractSSEController.js';
|
|
2
|
+
export { SSEConnectionSpy } from './SSEConnectionSpy.js';
|
|
3
|
+
export { buildPayloadSSERoute, buildSSEHandler, buildSSERoute, } from './sseContracts.js';
|
|
4
|
+
export { parseSSEBuffer, parseSSEEvents, } from './sseParser.js';
|
|
5
|
+
export { buildFastifySSERoute } from './sseRouteBuilder.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/sse/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,GAYtB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAEL,oBAAoB,EACpB,eAAe,EACf,aAAa,GAKd,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAGL,cAAc,EACd,cAAc,GACf,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,oBAAoB,EAAiC,MAAM,sBAAsB,CAAA"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { SSERouteHandler } from './sseTypes.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Supported HTTP methods for SSE routes.
|
|
5
|
+
* While traditional SSE uses GET, modern APIs (e.g., OpenAI) use POST
|
|
6
|
+
* to send request parameters in the body while streaming responses.
|
|
7
|
+
*/
|
|
8
|
+
export type SSEMethod = 'GET' | 'POST' | 'PUT' | 'PATCH';
|
|
9
|
+
/**
|
|
10
|
+
* Definition for an SSE route with type-safe contracts.
|
|
11
|
+
*
|
|
12
|
+
* @template Method - HTTP method (GET, POST, PUT, PATCH)
|
|
13
|
+
* @template Path - URL path pattern
|
|
14
|
+
* @template Params - Path parameters schema
|
|
15
|
+
* @template Query - Query string parameters schema
|
|
16
|
+
* @template RequestHeaders - Request headers schema
|
|
17
|
+
* @template Body - Request body schema (for POST/PUT/PATCH)
|
|
18
|
+
* @template Events - Map of event name to event data schema
|
|
19
|
+
*/
|
|
20
|
+
export type SSERouteDefinition<Method extends SSEMethod = SSEMethod, Path extends string = string, Params extends z.ZodTypeAny = z.ZodTypeAny, Query extends z.ZodTypeAny = z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny = z.ZodTypeAny, Body extends z.ZodTypeAny | undefined = undefined, Events extends Record<string, z.ZodTypeAny> = Record<string, z.ZodTypeAny>> = {
|
|
21
|
+
method: Method;
|
|
22
|
+
path: Path;
|
|
23
|
+
params: Params;
|
|
24
|
+
query: Query;
|
|
25
|
+
requestHeaders: RequestHeaders;
|
|
26
|
+
body: Body;
|
|
27
|
+
events: Events;
|
|
28
|
+
isSSE: true;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Type representing any SSE route definition (for use in generic constraints)
|
|
32
|
+
*/
|
|
33
|
+
export type AnySSERouteDefinition = SSERouteDefinition<SSEMethod, string, z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny | undefined, Record<string, z.ZodTypeAny>>;
|
|
34
|
+
/**
|
|
35
|
+
* Configuration for building a GET SSE route
|
|
36
|
+
*/
|
|
37
|
+
export type SSERouteConfig<Path extends string, Params extends z.ZodTypeAny, Query extends z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny, Events extends Record<string, z.ZodTypeAny>> = {
|
|
38
|
+
path: Path;
|
|
39
|
+
params: Params;
|
|
40
|
+
query: Query;
|
|
41
|
+
requestHeaders: RequestHeaders;
|
|
42
|
+
events: Events;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Configuration for building a POST/PUT/PATCH SSE route with request body
|
|
46
|
+
*/
|
|
47
|
+
export type PayloadSSERouteConfig<Path extends string, Params extends z.ZodTypeAny, Query extends z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny, Body extends z.ZodTypeAny, Events extends Record<string, z.ZodTypeAny>> = {
|
|
48
|
+
method?: 'POST' | 'PUT' | 'PATCH';
|
|
49
|
+
path: Path;
|
|
50
|
+
params: Params;
|
|
51
|
+
query: Query;
|
|
52
|
+
requestHeaders: RequestHeaders;
|
|
53
|
+
body: Body;
|
|
54
|
+
events: Events;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Build a GET SSE route definition (traditional SSE).
|
|
58
|
+
*
|
|
59
|
+
* Use this for long-lived connections where the client subscribes
|
|
60
|
+
* to receive events over time (e.g., notifications, real-time updates).
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const notificationsStream = buildSSERoute({
|
|
65
|
+
* path: '/api/notifications/stream',
|
|
66
|
+
* params: z.object({}),
|
|
67
|
+
* query: z.object({ userId: z.string().uuid() }),
|
|
68
|
+
* requestHeaders: z.object({ authorization: z.string() }),
|
|
69
|
+
* events: {
|
|
70
|
+
* notification: z.object({ id: z.string(), message: z.string() }),
|
|
71
|
+
* },
|
|
72
|
+
* })
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildSSERoute<Path extends string, Params extends z.ZodTypeAny, Query extends z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny, Events extends Record<string, z.ZodTypeAny>>(config: SSERouteConfig<Path, Params, Query, RequestHeaders, Events>): SSERouteDefinition<'GET', Path, Params, Query, RequestHeaders, undefined, Events>;
|
|
76
|
+
/**
|
|
77
|
+
* Build a POST/PUT/PATCH SSE route definition (OpenAI-style streaming API).
|
|
78
|
+
*
|
|
79
|
+
* Use this for request-response streaming where the client sends a request
|
|
80
|
+
* body and receives a stream of events in response (e.g., chat completions).
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const chatCompletionStream = buildPayloadSSERoute({
|
|
85
|
+
* method: 'POST',
|
|
86
|
+
* path: '/api/ai/chat/completions',
|
|
87
|
+
* params: z.object({}),
|
|
88
|
+
* query: z.object({}),
|
|
89
|
+
* requestHeaders: z.object({ authorization: z.string() }),
|
|
90
|
+
* body: z.object({
|
|
91
|
+
* model: z.string(),
|
|
92
|
+
* messages: z.array(z.object({ role: z.string(), content: z.string() })),
|
|
93
|
+
* stream: z.literal(true),
|
|
94
|
+
* }),
|
|
95
|
+
* events: {
|
|
96
|
+
* chunk: z.object({ content: z.string() }),
|
|
97
|
+
* done: z.object({ usage: z.object({ tokens: z.number() }) }),
|
|
98
|
+
* },
|
|
99
|
+
* })
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare function buildPayloadSSERoute<Path extends string, Params extends z.ZodTypeAny, Query extends z.ZodTypeAny, RequestHeaders extends z.ZodTypeAny, Body extends z.ZodTypeAny, Events extends Record<string, z.ZodTypeAny>>(config: PayloadSSERouteConfig<Path, Params, Query, RequestHeaders, Body, Events>): SSERouteDefinition<'POST' | 'PUT' | 'PATCH', Path, Params, Query, RequestHeaders, Body, Events>;
|
|
103
|
+
/**
|
|
104
|
+
* Type-inference helper for SSE handlers.
|
|
105
|
+
*
|
|
106
|
+
* Similar to `buildFastifyPayloadRoute`, this function provides automatic
|
|
107
|
+
* type inference for the request and connection parameters based on the contract.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* class MyController extends AbstractSSEController<{ stream: typeof streamContract }> {
|
|
112
|
+
* private handleStream = buildSSEHandler(
|
|
113
|
+
* streamContract,
|
|
114
|
+
* async (request, connection) => {
|
|
115
|
+
* // request.body is typed from contract
|
|
116
|
+
* // request.query is typed from contract
|
|
117
|
+
* const { message } = request.body
|
|
118
|
+
* },
|
|
119
|
+
* )
|
|
120
|
+
*
|
|
121
|
+
* buildSSERoutes() {
|
|
122
|
+
* return {
|
|
123
|
+
* stream: {
|
|
124
|
+
* contract: streamContract,
|
|
125
|
+
* handler: this.handleStream,
|
|
126
|
+
* },
|
|
127
|
+
* }
|
|
128
|
+
* }
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export declare function buildSSEHandler<Contract extends AnySSERouteDefinition>(_contract: Contract, handler: SSERouteHandler<z.infer<Contract['params']>, z.infer<Contract['query']>, z.infer<Contract['requestHeaders']>, Contract['body'] extends z.ZodTypeAny ? z.infer<Contract['body']> : undefined>): typeof handler;
|