opinionated-machine 6.2.0 → 6.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +341 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/DIContext.d.ts +31 -1
- package/dist/lib/DIContext.js +80 -3
- package/dist/lib/DIContext.js.map +1 -1
- package/dist/lib/dualmode/AbstractDualModeController.d.ts +97 -0
- package/dist/lib/dualmode/AbstractDualModeController.js +79 -0
- package/dist/lib/dualmode/AbstractDualModeController.js.map +1 -0
- package/dist/lib/dualmode/dualModeContracts.d.ts +134 -0
- package/dist/lib/dualmode/dualModeContracts.js +77 -0
- package/dist/lib/dualmode/dualModeContracts.js.map +1 -0
- package/dist/lib/dualmode/dualModeTypes.d.ts +23 -0
- package/dist/lib/dualmode/dualModeTypes.js +2 -0
- package/dist/lib/dualmode/dualModeTypes.js.map +1 -0
- package/dist/lib/dualmode/fastifyDualModeRouteBuilder.d.ts +30 -0
- package/dist/lib/dualmode/fastifyDualModeRouteBuilder.js +137 -0
- package/dist/lib/dualmode/fastifyDualModeRouteBuilder.js.map +1 -0
- package/dist/lib/dualmode/fastifyDualModeTypes.d.ts +153 -0
- package/dist/lib/dualmode/fastifyDualModeTypes.js +25 -0
- package/dist/lib/dualmode/fastifyDualModeTypes.js.map +1 -0
- package/dist/lib/dualmode/index.d.ts +5 -0
- package/dist/lib/dualmode/index.js +6 -0
- package/dist/lib/dualmode/index.js.map +1 -0
- package/dist/lib/resolverFunctions.d.ts +24 -0
- package/dist/lib/resolverFunctions.js +31 -0
- package/dist/lib/resolverFunctions.js.map +1 -1
- package/dist/lib/sse/AbstractSSEController.d.ts +8 -6
- package/dist/lib/sse/AbstractSSEController.js +1 -1
- package/dist/lib/sse/AbstractSSEController.js.map +1 -1
- package/dist/lib/sse/{sseRouteBuilder.d.ts → fastifySSERouteBuilder.d.ts} +7 -4
- package/dist/lib/sse/fastifySSERouteBuilder.js +53 -0
- package/dist/lib/sse/fastifySSERouteBuilder.js.map +1 -0
- package/dist/lib/sse/fastifySSERouteUtils.d.ts +83 -0
- package/dist/lib/sse/fastifySSERouteUtils.js +163 -0
- package/dist/lib/sse/fastifySSERouteUtils.js.map +1 -0
- package/dist/lib/sse/fastifySSETypes.d.ts +200 -0
- package/dist/lib/sse/fastifySSETypes.js +47 -0
- package/dist/lib/sse/fastifySSETypes.js.map +1 -0
- package/dist/lib/sse/index.d.ts +5 -4
- package/dist/lib/sse/index.js +4 -2
- package/dist/lib/sse/index.js.map +1 -1
- package/dist/lib/sse/sseContracts.d.ts +50 -60
- package/dist/lib/sse/sseContracts.js +8 -54
- package/dist/lib/sse/sseContracts.js.map +1 -1
- package/dist/lib/sse/sseTypes.d.ts +4 -157
- package/dist/lib/testing/index.d.ts +1 -1
- package/dist/lib/testing/index.js +1 -1
- package/dist/lib/testing/index.js.map +1 -1
- package/dist/lib/testing/sseHttpClient.d.ts +1 -1
- package/dist/lib/testing/sseHttpClient.js.map +1 -1
- package/dist/lib/testing/sseInjectHelpers.d.ts +3 -10
- package/dist/lib/testing/sseInjectHelpers.js +16 -15
- package/dist/lib/testing/sseInjectHelpers.js.map +1 -1
- package/dist/lib/testing/sseTestTypes.d.ts +3 -3
- package/package.json +1 -1
- package/dist/lib/sse/sseRouteBuilder.js +0 -177
- package/dist/lib/sse/sseRouteBuilder.js.map +0 -1
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ Very opinionated DI framework for fastify, built on top of awilix
|
|
|
17
17
|
- [`asRepositoryClass`](#asrepositoryclasstype-opts)
|
|
18
18
|
- [`asControllerClass`](#ascontrollerclasstype-opts)
|
|
19
19
|
- [`asSSEControllerClass`](#asssecontrollerclasstype-sseoptions-opts)
|
|
20
|
+
- [`asDualModeControllerClass`](#asdualmodecontrollerclasstype-sseoptions-opts)
|
|
20
21
|
- [Message Queue Resolvers](#message-queue-resolvers)
|
|
21
22
|
- [`asMessageQueueHandlerClass`](#asmessagequeuehandlerclasstype-mqoptions-opts)
|
|
22
23
|
- [Background Job Resolvers](#background-job-resolvers)
|
|
@@ -53,6 +54,13 @@ Very opinionated DI framework for fastify, built on top of awilix
|
|
|
53
54
|
- [SSEHttpClient](#ssehttpclient)
|
|
54
55
|
- [SSEInjectClient](#sseinjectclient)
|
|
55
56
|
- [Contract-Aware Inject Helpers](#contract-aware-inject-helpers)
|
|
57
|
+
- [Dual-Mode Controllers (SSE + JSON)](#dual-mode-controllers-sse--json)
|
|
58
|
+
- [Overview](#overview)
|
|
59
|
+
- [Defining Dual-Mode Contracts](#defining-dual-mode-contracts)
|
|
60
|
+
- [Implementing Dual-Mode Controllers](#implementing-dual-mode-controllers)
|
|
61
|
+
- [Registering Dual-Mode Controllers](#registering-dual-mode-controllers)
|
|
62
|
+
- [Accept Header Routing](#accept-header-routing)
|
|
63
|
+
- [Testing Dual-Mode Controllers](#testing-dual-mode-controllers)
|
|
56
64
|
|
|
57
65
|
## Basic usage
|
|
58
66
|
|
|
@@ -297,6 +305,19 @@ resolveControllers(diOptions: DependencyInjectionOptions) {
|
|
|
297
305
|
}
|
|
298
306
|
```
|
|
299
307
|
|
|
308
|
+
#### `asDualModeControllerClass(Type, sseOptions?, opts?)`
|
|
309
|
+
For dual-mode controller classes that handle both SSE and JSON responses on the same route. Marks the dependency as **private** with `isDualModeController: true` for auto-detection. Inherits all SSE controller features including connection management and graceful shutdown. When `sseOptions.diOptions.isTestMode` is true, enables the connection spy for testing SSE mode.
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
// In resolveControllers()
|
|
313
|
+
resolveControllers(diOptions: DependencyInjectionOptions) {
|
|
314
|
+
return {
|
|
315
|
+
userController: asControllerClass(UserController),
|
|
316
|
+
chatController: asDualModeControllerClass(ChatDualModeController, { diOptions }),
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
300
321
|
### Message Queue Resolvers
|
|
301
322
|
|
|
302
323
|
#### `asMessageQueueHandlerClass(Type, mqOptions, opts?)`
|
|
@@ -377,15 +398,26 @@ await app.register(FastifySSEPlugin)
|
|
|
377
398
|
|
|
378
399
|
### Defining SSE Contracts
|
|
379
400
|
|
|
380
|
-
Use `
|
|
401
|
+
Use `buildSSEContract` for GET-based SSE streams or `buildPayloadSSEContract` for POST/PUT/PATCH streams. Paths are defined using `pathResolver`, a type-safe function that receives typed params and returns the URL path:
|
|
381
402
|
|
|
382
403
|
```ts
|
|
383
404
|
import { z } from 'zod'
|
|
384
|
-
import {
|
|
405
|
+
import { buildSSEContract, buildPayloadSSEContract } from 'opinionated-machine'
|
|
385
406
|
|
|
386
|
-
// GET-based SSE stream
|
|
387
|
-
export const
|
|
388
|
-
|
|
407
|
+
// GET-based SSE stream with path params
|
|
408
|
+
export const channelStreamContract = buildSSEContract({
|
|
409
|
+
pathResolver: (params) => `/api/channels/${params.channelId}/stream`,
|
|
410
|
+
params: z.object({ channelId: z.string() }),
|
|
411
|
+
query: z.object({}),
|
|
412
|
+
requestHeaders: z.object({}),
|
|
413
|
+
events: {
|
|
414
|
+
message: z.object({ content: z.string() }),
|
|
415
|
+
},
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
// GET-based SSE stream without path params
|
|
419
|
+
export const notificationsContract = buildSSEContract({
|
|
420
|
+
pathResolver: () => '/api/notifications/stream',
|
|
389
421
|
params: z.object({}),
|
|
390
422
|
query: z.object({ userId: z.string().optional() }),
|
|
391
423
|
requestHeaders: z.object({}),
|
|
@@ -398,9 +430,9 @@ export const notificationsContract = buildSSERoute({
|
|
|
398
430
|
})
|
|
399
431
|
|
|
400
432
|
// POST-based SSE stream (e.g., AI chat completions)
|
|
401
|
-
export const chatCompletionContract =
|
|
433
|
+
export const chatCompletionContract = buildPayloadSSEContract({
|
|
402
434
|
method: 'POST',
|
|
403
|
-
|
|
435
|
+
pathResolver: () => '/api/chat/completions',
|
|
404
436
|
params: z.object({}),
|
|
405
437
|
query: z.object({}),
|
|
406
438
|
requestHeaders: z.object({}),
|
|
@@ -1260,3 +1292,305 @@ const result = await closed
|
|
|
1260
1292
|
const events = parseSSEEvents(result.body)
|
|
1261
1293
|
```
|
|
1262
1294
|
|
|
1295
|
+
## Dual-Mode Controllers (SSE + JSON)
|
|
1296
|
+
|
|
1297
|
+
Dual-mode controllers handle both SSE streaming and JSON responses on the same route path, automatically branching based on the `Accept` header. This is ideal for APIs that support both real-time streaming and traditional request-response patterns.
|
|
1298
|
+
|
|
1299
|
+
### Overview
|
|
1300
|
+
|
|
1301
|
+
| Accept Header | Response Mode |
|
|
1302
|
+
| ------------- | ------------- |
|
|
1303
|
+
| `text/event-stream` | SSE streaming |
|
|
1304
|
+
| `application/json` | JSON response |
|
|
1305
|
+
| `*/*` or missing | JSON (default, configurable) |
|
|
1306
|
+
|
|
1307
|
+
Dual-mode controllers extend `AbstractDualModeController` which inherits from `AbstractSSEController`, providing access to all SSE features (connection management, broadcasting, lifecycle hooks) while adding JSON response support.
|
|
1308
|
+
|
|
1309
|
+
### Defining Dual-Mode Contracts
|
|
1310
|
+
|
|
1311
|
+
Use `buildDualModeContract` for GET routes or `buildPayloadDualModeContract` for POST/PUT/PATCH routes. The key difference from SSE contracts is the addition of `jsonResponse` schema:
|
|
1312
|
+
|
|
1313
|
+
```ts
|
|
1314
|
+
import { z } from 'zod'
|
|
1315
|
+
import { buildDualModeContract, buildPayloadDualModeContract } from 'opinionated-machine'
|
|
1316
|
+
|
|
1317
|
+
// GET dual-mode route (polling or streaming job status)
|
|
1318
|
+
export const jobStatusContract = buildDualModeContract({
|
|
1319
|
+
pathResolver: (params) => `/api/jobs/${params.jobId}/status`,
|
|
1320
|
+
params: z.object({ jobId: z.string().uuid() }),
|
|
1321
|
+
query: z.object({ verbose: z.string().optional() }),
|
|
1322
|
+
requestHeaders: z.object({}),
|
|
1323
|
+
jsonResponse: z.object({
|
|
1324
|
+
status: z.enum(['pending', 'running', 'completed', 'failed']),
|
|
1325
|
+
progress: z.number(),
|
|
1326
|
+
result: z.string().optional(),
|
|
1327
|
+
}),
|
|
1328
|
+
events: {
|
|
1329
|
+
progress: z.object({ percent: z.number(), message: z.string().optional() }),
|
|
1330
|
+
done: z.object({ result: z.string() }),
|
|
1331
|
+
},
|
|
1332
|
+
})
|
|
1333
|
+
|
|
1334
|
+
// POST dual-mode route (OpenAI-style chat completion)
|
|
1335
|
+
export const chatCompletionContract = buildPayloadDualModeContract({
|
|
1336
|
+
method: 'POST',
|
|
1337
|
+
pathResolver: (params) => `/api/chats/${params.chatId}/completions`,
|
|
1338
|
+
params: z.object({ chatId: z.string().uuid() }),
|
|
1339
|
+
query: z.object({}),
|
|
1340
|
+
requestHeaders: z.object({ authorization: z.string() }),
|
|
1341
|
+
body: z.object({ message: z.string() }),
|
|
1342
|
+
jsonResponse: z.object({
|
|
1343
|
+
reply: z.string(),
|
|
1344
|
+
usage: z.object({ tokens: z.number() }),
|
|
1345
|
+
}),
|
|
1346
|
+
events: {
|
|
1347
|
+
chunk: z.object({ delta: z.string() }),
|
|
1348
|
+
done: z.object({ usage: z.object({ total: z.number() }) }),
|
|
1349
|
+
},
|
|
1350
|
+
})
|
|
1351
|
+
```
|
|
1352
|
+
|
|
1353
|
+
**Note**: Dual-mode contracts use `pathResolver` instead of static `path` for type-safe path construction. The `pathResolver` function receives typed params and returns the URL path.
|
|
1354
|
+
|
|
1355
|
+
### Implementing Dual-Mode Controllers
|
|
1356
|
+
|
|
1357
|
+
Dual-mode controllers use `buildDualModeHandler` to define both JSON and SSE handlers:
|
|
1358
|
+
|
|
1359
|
+
```ts
|
|
1360
|
+
import {
|
|
1361
|
+
AbstractDualModeController,
|
|
1362
|
+
buildDualModeHandler,
|
|
1363
|
+
type BuildFastifyDualModeRoutesReturnType,
|
|
1364
|
+
type DualModeControllerConfig,
|
|
1365
|
+
} from 'opinionated-machine'
|
|
1366
|
+
|
|
1367
|
+
type Contracts = {
|
|
1368
|
+
chatCompletion: typeof chatCompletionContract
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
type Dependencies = {
|
|
1372
|
+
aiService: AIService
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
export class ChatDualModeController extends AbstractDualModeController<Contracts> {
|
|
1376
|
+
public static contracts = {
|
|
1377
|
+
chatCompletion: chatCompletionContract,
|
|
1378
|
+
} as const
|
|
1379
|
+
|
|
1380
|
+
private readonly aiService: AIService
|
|
1381
|
+
|
|
1382
|
+
constructor(deps: Dependencies, config?: DualModeControllerConfig) {
|
|
1383
|
+
super(deps, config)
|
|
1384
|
+
this.aiService = deps.aiService
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
public buildDualModeRoutes(): BuildFastifyDualModeRoutesReturnType<Contracts> {
|
|
1388
|
+
return {
|
|
1389
|
+
chatCompletion: {
|
|
1390
|
+
contract: ChatDualModeController.contracts.chatCompletion,
|
|
1391
|
+
handlers: buildDualModeHandler(chatCompletionContract, {
|
|
1392
|
+
// JSON mode - return complete response
|
|
1393
|
+
json: async (ctx) => {
|
|
1394
|
+
const result = await this.aiService.complete(ctx.request.body.message)
|
|
1395
|
+
return {
|
|
1396
|
+
reply: result.text,
|
|
1397
|
+
usage: { tokens: result.tokenCount },
|
|
1398
|
+
}
|
|
1399
|
+
},
|
|
1400
|
+
// SSE mode - stream response chunks
|
|
1401
|
+
sse: async (ctx) => {
|
|
1402
|
+
let totalTokens = 0
|
|
1403
|
+
for await (const chunk of this.aiService.stream(ctx.request.body.message)) {
|
|
1404
|
+
await ctx.connection.send('chunk', { delta: chunk.text })
|
|
1405
|
+
totalTokens += chunk.tokenCount ?? 0
|
|
1406
|
+
}
|
|
1407
|
+
await ctx.connection.send('done', { usage: { total: totalTokens } })
|
|
1408
|
+
this.closeConnection(ctx.connection.id)
|
|
1409
|
+
},
|
|
1410
|
+
}),
|
|
1411
|
+
options: {
|
|
1412
|
+
// Optional: set SSE as default mode (instead of JSON)
|
|
1413
|
+
defaultMode: 'sse',
|
|
1414
|
+
// Optional: route-level authentication
|
|
1415
|
+
preHandler: (request, reply) => {
|
|
1416
|
+
if (!request.headers.authorization) {
|
|
1417
|
+
return Promise.resolve(reply.code(401).send({ error: 'Unauthorized' }))
|
|
1418
|
+
}
|
|
1419
|
+
},
|
|
1420
|
+
// Optional: SSE lifecycle hooks
|
|
1421
|
+
onConnect: (conn) => console.log('Client connected:', conn.id),
|
|
1422
|
+
onDisconnect: (conn) => console.log('Client disconnected:', conn.id),
|
|
1423
|
+
},
|
|
1424
|
+
},
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
```
|
|
1429
|
+
|
|
1430
|
+
**Handler Context:**
|
|
1431
|
+
|
|
1432
|
+
| Mode | Context Properties |
|
|
1433
|
+
| ---- | ------------------ |
|
|
1434
|
+
| `json` | `ctx.mode`, `ctx.request`, `ctx.reply` |
|
|
1435
|
+
| `sse` | `ctx.mode`, `ctx.connection`, `ctx.request` |
|
|
1436
|
+
|
|
1437
|
+
The `json` handler must return a value matching `jsonResponse` schema. The `sse` handler uses `ctx.connection.send()` for type-safe event streaming.
|
|
1438
|
+
|
|
1439
|
+
### Registering Dual-Mode Controllers
|
|
1440
|
+
|
|
1441
|
+
Use `asDualModeControllerClass` in your module:
|
|
1442
|
+
|
|
1443
|
+
```ts
|
|
1444
|
+
import {
|
|
1445
|
+
AbstractModule,
|
|
1446
|
+
asControllerClass,
|
|
1447
|
+
asDualModeControllerClass,
|
|
1448
|
+
asServiceClass,
|
|
1449
|
+
} from 'opinionated-machine'
|
|
1450
|
+
|
|
1451
|
+
export class ChatModule extends AbstractModule<Dependencies> {
|
|
1452
|
+
resolveDependencies() {
|
|
1453
|
+
return {
|
|
1454
|
+
aiService: asServiceClass(AIService),
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
resolveControllers(diOptions: DependencyInjectionOptions) {
|
|
1459
|
+
return {
|
|
1460
|
+
// REST controller
|
|
1461
|
+
usersController: asControllerClass(UsersController),
|
|
1462
|
+
// Dual-mode controller (auto-detected via isDualModeController flag)
|
|
1463
|
+
chatController: asDualModeControllerClass(ChatDualModeController, { diOptions }),
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
```
|
|
1468
|
+
|
|
1469
|
+
Register dual-mode routes after the `@fastify/sse` plugin:
|
|
1470
|
+
|
|
1471
|
+
```ts
|
|
1472
|
+
const app = fastify()
|
|
1473
|
+
app.setValidatorCompiler(validatorCompiler)
|
|
1474
|
+
app.setSerializerCompiler(serializerCompiler)
|
|
1475
|
+
|
|
1476
|
+
// Register @fastify/sse plugin
|
|
1477
|
+
await app.register(FastifySSEPlugin)
|
|
1478
|
+
|
|
1479
|
+
// Register routes
|
|
1480
|
+
context.registerRoutes(app) // REST routes
|
|
1481
|
+
context.registerSSERoutes(app) // SSE-only routes
|
|
1482
|
+
context.registerDualModeRoutes(app) // Dual-mode routes
|
|
1483
|
+
|
|
1484
|
+
// Check if controllers exist before registration (optional)
|
|
1485
|
+
if (context.hasDualModeControllers()) {
|
|
1486
|
+
context.registerDualModeRoutes(app)
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
await app.ready()
|
|
1490
|
+
```
|
|
1491
|
+
|
|
1492
|
+
### Accept Header Routing
|
|
1493
|
+
|
|
1494
|
+
The `Accept` header determines response mode:
|
|
1495
|
+
|
|
1496
|
+
```bash
|
|
1497
|
+
# JSON mode (complete response)
|
|
1498
|
+
curl -X POST http://localhost:3000/api/chats/123/completions \
|
|
1499
|
+
-H "Content-Type: application/json" \
|
|
1500
|
+
-H "Accept: application/json" \
|
|
1501
|
+
-d '{"message": "Hello world"}'
|
|
1502
|
+
|
|
1503
|
+
# SSE mode (streaming response)
|
|
1504
|
+
curl -X POST http://localhost:3000/api/chats/123/completions \
|
|
1505
|
+
-H "Content-Type: application/json" \
|
|
1506
|
+
-H "Accept: text/event-stream" \
|
|
1507
|
+
-d '{"message": "Hello world"}'
|
|
1508
|
+
```
|
|
1509
|
+
|
|
1510
|
+
**Quality values** are supported for content negotiation:
|
|
1511
|
+
|
|
1512
|
+
```bash
|
|
1513
|
+
# Prefer JSON (higher quality value)
|
|
1514
|
+
curl -H "Accept: text/event-stream;q=0.5, application/json;q=1.0" ...
|
|
1515
|
+
|
|
1516
|
+
# Prefer SSE (higher quality value)
|
|
1517
|
+
curl -H "Accept: application/json;q=0.5, text/event-stream;q=1.0" ...
|
|
1518
|
+
```
|
|
1519
|
+
|
|
1520
|
+
### Testing Dual-Mode Controllers
|
|
1521
|
+
|
|
1522
|
+
Test both JSON and SSE modes:
|
|
1523
|
+
|
|
1524
|
+
```ts
|
|
1525
|
+
import { createContainer } from 'awilix'
|
|
1526
|
+
import { DIContext, SSETestServer, SSEInjectClient } from 'opinionated-machine'
|
|
1527
|
+
|
|
1528
|
+
describe('ChatDualModeController', () => {
|
|
1529
|
+
let server: SSETestServer
|
|
1530
|
+
let injectClient: SSEInjectClient
|
|
1531
|
+
|
|
1532
|
+
beforeEach(async () => {
|
|
1533
|
+
const container = createContainer({ injectionMode: 'PROXY' })
|
|
1534
|
+
const context = new DIContext(container, { isTestMode: true }, {})
|
|
1535
|
+
context.registerDependencies({ modules: [new ChatModule()] }, undefined)
|
|
1536
|
+
|
|
1537
|
+
server = await SSETestServer.create(
|
|
1538
|
+
(app) => {
|
|
1539
|
+
context.registerDualModeRoutes(app)
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
configureApp: (app) => {
|
|
1543
|
+
app.setValidatorCompiler(validatorCompiler)
|
|
1544
|
+
app.setSerializerCompiler(serializerCompiler)
|
|
1545
|
+
},
|
|
1546
|
+
setup: () => ({ context }),
|
|
1547
|
+
},
|
|
1548
|
+
)
|
|
1549
|
+
|
|
1550
|
+
injectClient = new SSEInjectClient(server.app)
|
|
1551
|
+
})
|
|
1552
|
+
|
|
1553
|
+
afterEach(async () => {
|
|
1554
|
+
await server.resources.context.destroy()
|
|
1555
|
+
await server.close()
|
|
1556
|
+
})
|
|
1557
|
+
|
|
1558
|
+
it('returns JSON for Accept: application/json', async () => {
|
|
1559
|
+
const response = await server.app.inject({
|
|
1560
|
+
method: 'POST',
|
|
1561
|
+
url: '/api/chats/550e8400-e29b-41d4-a716-446655440000/completions',
|
|
1562
|
+
headers: {
|
|
1563
|
+
'content-type': 'application/json',
|
|
1564
|
+
accept: 'application/json',
|
|
1565
|
+
authorization: 'Bearer token',
|
|
1566
|
+
},
|
|
1567
|
+
payload: { message: 'Hello' },
|
|
1568
|
+
})
|
|
1569
|
+
|
|
1570
|
+
expect(response.statusCode).toBe(200)
|
|
1571
|
+
expect(response.headers['content-type']).toContain('application/json')
|
|
1572
|
+
|
|
1573
|
+
const body = JSON.parse(response.body)
|
|
1574
|
+
expect(body).toHaveProperty('reply')
|
|
1575
|
+
expect(body).toHaveProperty('usage')
|
|
1576
|
+
})
|
|
1577
|
+
|
|
1578
|
+
it('streams SSE for Accept: text/event-stream', async () => {
|
|
1579
|
+
const conn = await injectClient.connectWithBody(
|
|
1580
|
+
'/api/chats/550e8400-e29b-41d4-a716-446655440000/completions',
|
|
1581
|
+
{ message: 'Hello' },
|
|
1582
|
+
{ headers: { authorization: 'Bearer token' } },
|
|
1583
|
+
)
|
|
1584
|
+
|
|
1585
|
+
expect(conn.getStatusCode()).toBe(200)
|
|
1586
|
+
expect(conn.getHeaders()['content-type']).toContain('text/event-stream')
|
|
1587
|
+
|
|
1588
|
+
const events = conn.getReceivedEvents()
|
|
1589
|
+
const chunks = events.filter((e) => e.event === 'chunk')
|
|
1590
|
+
const doneEvents = events.filter((e) => e.event === 'done')
|
|
1591
|
+
|
|
1592
|
+
expect(chunks.length).toBeGreaterThan(0)
|
|
1593
|
+
expect(doneEvents).toHaveLength(1)
|
|
1594
|
+
})
|
|
1595
|
+
})
|
|
1596
|
+
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { AbstractTestContextFactory, type CreateTestContextParams, } from './lib
|
|
|
4
4
|
export type { NestedPartial } from './lib/configUtils.js';
|
|
5
5
|
export { type DependencyInjectionOptions, DIContext, type RegisterDependenciesParams, } from './lib/DIContext.js';
|
|
6
6
|
export { ENABLE_ALL, isAnyMessageQueueConsumerEnabled, isEnqueuedJobWorkersEnabled, isJobQueueEnabled, isMessageQueueConsumerEnabled, isPeriodicJobEnabled, resolveJobQueuesEnabled, } from './lib/diConfigUtils.js';
|
|
7
|
+
export * from './lib/dualmode/index.js';
|
|
7
8
|
export * from './lib/resolverFunctions.js';
|
|
8
9
|
export * from './lib/sse/index.js';
|
|
9
10
|
export * from './lib/testing/index.js';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,8 @@ export { AbstractModule, } from './lib/AbstractModule.js';
|
|
|
3
3
|
export { AbstractTestContextFactory, } from './lib/AbstractTestContextFactory.js';
|
|
4
4
|
export { DIContext, } from './lib/DIContext.js';
|
|
5
5
|
export { ENABLE_ALL, isAnyMessageQueueConsumerEnabled, isEnqueuedJobWorkersEnabled, isJobQueueEnabled, isMessageQueueConsumerEnabled, isPeriodicJobEnabled, resolveJobQueuesEnabled, } from './lib/diConfigUtils.js';
|
|
6
|
+
// Dual-mode (SSE + JSON)
|
|
7
|
+
export * from './lib/dualmode/index.js';
|
|
6
8
|
export * from './lib/resolverFunctions.js';
|
|
7
9
|
// SSE
|
|
8
10
|
export * from './lib/sse/index.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAA8B,MAAM,6BAA6B,CAAA;AAC5F,OAAO,EACL,cAAc,GAGf,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,0BAA0B,GAE3B,MAAM,qCAAqC,CAAA;AAE5C,OAAO,EAEL,SAAS,GAEV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,UAAU,EACV,gCAAgC,EAChC,2BAA2B,EAC3B,iBAAiB,EACjB,6BAA6B,EAC7B,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,wBAAwB,CAAA;AAC/B,cAAc,4BAA4B,CAAA;AAC1C,MAAM;AACN,cAAc,oBAAoB,CAAA;AAClC,wBAAwB;AACxB,cAAc,wBAAwB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAA8B,MAAM,6BAA6B,CAAA;AAC5F,OAAO,EACL,cAAc,GAGf,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,0BAA0B,GAE3B,MAAM,qCAAqC,CAAA;AAE5C,OAAO,EAEL,SAAS,GAEV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,UAAU,EACV,gCAAgC,EAChC,2BAA2B,EAC3B,iBAAiB,EACjB,6BAA6B,EAC7B,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,wBAAwB,CAAA;AAC/B,yBAAyB;AACzB,cAAc,yBAAyB,CAAA;AACvC,cAAc,4BAA4B,CAAA;AAC1C,MAAM;AACN,cAAc,oBAAoB,CAAA;AAClC,wBAAwB;AACxB,cAAc,wBAAwB,CAAA"}
|
package/dist/lib/DIContext.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ import type { FastifyInstance } from 'fastify';
|
|
|
4
4
|
import type { AbstractModule } from './AbstractModule.js';
|
|
5
5
|
import { type NestedPartial } from './configUtils.js';
|
|
6
6
|
import type { ENABLE_ALL } from './diConfigUtils.js';
|
|
7
|
-
import {
|
|
7
|
+
import type { RegisterDualModeRoutesOptions } from './dualmode/fastifyDualModeTypes.js';
|
|
8
|
+
import { type RegisterSSERoutesOptions } from './sse/fastifySSERouteBuilder.js';
|
|
8
9
|
export type RegisterDependenciesParams<Dependencies, Config, ExternalDependencies> = {
|
|
9
10
|
modules: readonly AbstractModule<unknown, ExternalDependencies>[];
|
|
10
11
|
secondaryModules?: readonly AbstractModule<unknown, ExternalDependencies>[];
|
|
@@ -30,6 +31,7 @@ export declare class DIContext<Dependencies extends object, Config extends objec
|
|
|
30
31
|
readonly diContainer: AwilixContainer<Dependencies>;
|
|
31
32
|
private readonly controllerResolvers;
|
|
32
33
|
private readonly sseControllerNames;
|
|
34
|
+
private readonly dualModeControllerNames;
|
|
33
35
|
private readonly appConfig;
|
|
34
36
|
constructor(diContainer: AwilixContainer, options: DependencyInjectionOptions, appConfig: Config, awilixManager?: AwilixManager);
|
|
35
37
|
private registerModule;
|
|
@@ -40,6 +42,11 @@ export declare class DIContext<Dependencies extends object, Config extends objec
|
|
|
40
42
|
* Use this to conditionally call registerSSERoutes().
|
|
41
43
|
*/
|
|
42
44
|
hasSSEControllers(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Check if any dual-mode controllers are registered.
|
|
47
|
+
* Use this to conditionally call registerDualModeRoutes().
|
|
48
|
+
*/
|
|
49
|
+
hasDualModeControllers(): boolean;
|
|
43
50
|
/**
|
|
44
51
|
* Register SSE routes with the Fastify app.
|
|
45
52
|
*
|
|
@@ -59,6 +66,29 @@ export declare class DIContext<Dependencies extends object, Config extends objec
|
|
|
59
66
|
* ```
|
|
60
67
|
*/
|
|
61
68
|
registerSSERoutes(app: FastifyInstance<any, any, any, any>, options?: RegisterSSERoutesOptions): void;
|
|
69
|
+
/**
|
|
70
|
+
* Register dual-mode routes with the Fastify app.
|
|
71
|
+
*
|
|
72
|
+
* Dual-mode routes handle both SSE streaming and JSON responses on the
|
|
73
|
+
* same path, automatically branching based on the `Accept` header.
|
|
74
|
+
*
|
|
75
|
+
* Must be called separately from registerRoutes() and registerSSERoutes().
|
|
76
|
+
* Requires @fastify/sse plugin to be registered on the app.
|
|
77
|
+
*
|
|
78
|
+
* @param app - Fastify instance with @fastify/sse registered
|
|
79
|
+
* @param options - Optional configuration for dual-mode routes
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* // Register @fastify/sse plugin first
|
|
84
|
+
* await app.register(fastifySSE, { heartbeatInterval: 30000 })
|
|
85
|
+
*
|
|
86
|
+
* // Then register dual-mode routes
|
|
87
|
+
* context.registerDualModeRoutes(app)
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
registerDualModeRoutes(app: FastifyInstance<any, any, any, any>, options?: RegisterDualModeRoutesOptions): void;
|
|
91
|
+
private applyDualModeRouteOptions;
|
|
62
92
|
private applySSERouteOptions;
|
|
63
93
|
private applyPreHandlers;
|
|
64
94
|
private applyRateLimit;
|
package/dist/lib/DIContext.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AwilixManager } from 'awilix-manager';
|
|
2
2
|
import { merge } from 'ts-deepmerge';
|
|
3
3
|
import { mergeConfigAndDependencyOverrides } from './configUtils.js';
|
|
4
|
-
import {
|
|
4
|
+
import { buildFastifyDualModeRoute } from './dualmode/fastifyDualModeRouteBuilder.js';
|
|
5
|
+
import { buildFastifySSERoute, } from './sse/fastifySSERouteBuilder.js';
|
|
5
6
|
export class DIContext {
|
|
6
7
|
options;
|
|
7
8
|
awilixManager;
|
|
@@ -10,6 +11,8 @@ export class DIContext {
|
|
|
10
11
|
controllerResolvers;
|
|
11
12
|
// SSE controller dependency names (resolved from container to preserve singletons)
|
|
12
13
|
sseControllerNames;
|
|
14
|
+
// Dual-mode controller dependency names (resolved from container to preserve singletons)
|
|
15
|
+
dualModeControllerNames;
|
|
13
16
|
appConfig;
|
|
14
17
|
constructor(diContainer, options, appConfig, awilixManager) {
|
|
15
18
|
this.options = options;
|
|
@@ -26,6 +29,7 @@ export class DIContext {
|
|
|
26
29
|
});
|
|
27
30
|
this.controllerResolvers = [];
|
|
28
31
|
this.sseControllerNames = [];
|
|
32
|
+
this.dualModeControllerNames = [];
|
|
29
33
|
}
|
|
30
34
|
registerModule(module, targetDiConfig, externalDependencies, resolveControllers, isPrimaryModule) {
|
|
31
35
|
const resolvedDIConfig = module.resolveDependencies(this.options, externalDependencies);
|
|
@@ -39,8 +43,15 @@ export class DIContext {
|
|
|
39
43
|
if (isPrimaryModule && resolveControllers) {
|
|
40
44
|
const controllers = module.resolveControllers(this.options);
|
|
41
45
|
for (const [name, resolver] of Object.entries(controllers)) {
|
|
42
|
-
// @ts-expect-error
|
|
43
|
-
if (resolver.
|
|
46
|
+
// @ts-expect-error isDualModeController is a custom property on the resolver
|
|
47
|
+
if (resolver.isDualModeController) {
|
|
48
|
+
// Dual-mode controller: register in DI container and track name for route registration
|
|
49
|
+
this.dualModeControllerNames.push(name);
|
|
50
|
+
// @ts-expect-error we can't really ensure type-safety here
|
|
51
|
+
targetDiConfig[name] = resolver;
|
|
52
|
+
// @ts-expect-error isSSEController is a custom property on the resolver
|
|
53
|
+
}
|
|
54
|
+
else if (resolver.isSSEController) {
|
|
44
55
|
// SSE controller: register in DI container and track name for route registration
|
|
45
56
|
this.sseControllerNames.push(name);
|
|
46
57
|
// @ts-expect-error we can't really ensure type-safety here
|
|
@@ -99,6 +110,13 @@ export class DIContext {
|
|
|
99
110
|
hasSSEControllers() {
|
|
100
111
|
return this.sseControllerNames.length > 0;
|
|
101
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Check if any dual-mode controllers are registered.
|
|
115
|
+
* Use this to conditionally call registerDualModeRoutes().
|
|
116
|
+
*/
|
|
117
|
+
hasDualModeControllers() {
|
|
118
|
+
return this.dualModeControllerNames.length > 0;
|
|
119
|
+
}
|
|
102
120
|
/**
|
|
103
121
|
* Register SSE routes with the Fastify app.
|
|
104
122
|
*
|
|
@@ -134,6 +152,65 @@ export class DIContext {
|
|
|
134
152
|
}
|
|
135
153
|
}
|
|
136
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Register dual-mode routes with the Fastify app.
|
|
157
|
+
*
|
|
158
|
+
* Dual-mode routes handle both SSE streaming and JSON responses on the
|
|
159
|
+
* same path, automatically branching based on the `Accept` header.
|
|
160
|
+
*
|
|
161
|
+
* Must be called separately from registerRoutes() and registerSSERoutes().
|
|
162
|
+
* Requires @fastify/sse plugin to be registered on the app.
|
|
163
|
+
*
|
|
164
|
+
* @param app - Fastify instance with @fastify/sse registered
|
|
165
|
+
* @param options - Optional configuration for dual-mode routes
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```typescript
|
|
169
|
+
* // Register @fastify/sse plugin first
|
|
170
|
+
* await app.register(fastifySSE, { heartbeatInterval: 30000 })
|
|
171
|
+
*
|
|
172
|
+
* // Then register dual-mode routes
|
|
173
|
+
* context.registerDualModeRoutes(app)
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
registerDualModeRoutes(
|
|
177
|
+
// biome-ignore lint/suspicious/noExplicitAny: Fastify instance types are complex
|
|
178
|
+
app, options) {
|
|
179
|
+
if (!this.hasDualModeControllers()) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
for (const controllerName of this.dualModeControllerNames) {
|
|
183
|
+
// Resolve from container to use the singleton instance
|
|
184
|
+
const dualModeController = this.diContainer.resolve(controllerName);
|
|
185
|
+
const dualModeRoutes = dualModeController.buildDualModeRoutes();
|
|
186
|
+
for (const routeConfig of Object.values(dualModeRoutes)) {
|
|
187
|
+
const route = buildFastifyDualModeRoute(dualModeController, routeConfig);
|
|
188
|
+
this.applyDualModeRouteOptions(route, options);
|
|
189
|
+
app.route(route);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
applyDualModeRouteOptions(route, options) {
|
|
194
|
+
if (options?.preHandler) {
|
|
195
|
+
this.applyPreHandlers(route, options.preHandler);
|
|
196
|
+
}
|
|
197
|
+
if (options?.rateLimit) {
|
|
198
|
+
this.applyRateLimit(route, options.rateLimit);
|
|
199
|
+
}
|
|
200
|
+
// Apply SSE-specific options (heartbeatInterval, serializer) for SSE mode
|
|
201
|
+
if (options?.heartbeatInterval !== undefined || options?.serializer !== undefined) {
|
|
202
|
+
// biome-ignore lint/suspicious/noExplicitAny: config types vary by plugins
|
|
203
|
+
const routeWithConfig = route;
|
|
204
|
+
routeWithConfig.config = merge(routeWithConfig.config || {}, {
|
|
205
|
+
sse: {
|
|
206
|
+
...(options.heartbeatInterval !== undefined && {
|
|
207
|
+
heartbeatInterval: options.heartbeatInterval,
|
|
208
|
+
}),
|
|
209
|
+
...(options.serializer !== undefined && { serializer: options.serializer }),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
137
214
|
applySSERouteOptions(route, options) {
|
|
138
215
|
if (options?.preHandler) {
|
|
139
216
|
this.applyPreHandlers(route, options.preHandler);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DIContext.js","sourceRoot":"","sources":["../../lib/DIContext.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAE9C,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AAGpC,OAAO,EAAE,iCAAiC,EAAsB,MAAM,kBAAkB,CAAA;AAIxF,OAAO,EAAE,oBAAoB,
|
|
1
|
+
{"version":3,"file":"DIContext.js","sourceRoot":"","sources":["../../lib/DIContext.ts"],"names":[],"mappings":"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"}
|