@hazeljs/realtime 0.2.0-rc.3

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.
Files changed (45) hide show
  1. package/LICENSE +192 -0
  2. package/README.md +243 -0
  3. package/dist/index.d.ts +22 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +28 -0
  6. package/dist/providers/openai/index.d.ts +5 -0
  7. package/dist/providers/openai/index.d.ts.map +1 -0
  8. package/dist/providers/openai/index.js +7 -0
  9. package/dist/providers/openai/openai-realtime.client.d.ts +74 -0
  10. package/dist/providers/openai/openai-realtime.client.d.ts.map +1 -0
  11. package/dist/providers/openai/openai-realtime.client.js +211 -0
  12. package/dist/providers/openai/openai-realtime.client.test.d.ts +2 -0
  13. package/dist/providers/openai/openai-realtime.client.test.d.ts.map +1 -0
  14. package/dist/providers/openai/openai-realtime.client.test.js +262 -0
  15. package/dist/providers/openai/openai-realtime.session.d.ts +63 -0
  16. package/dist/providers/openai/openai-realtime.session.d.ts.map +1 -0
  17. package/dist/providers/openai/openai-realtime.session.js +95 -0
  18. package/dist/providers/openai/openai-realtime.session.test.d.ts +2 -0
  19. package/dist/providers/openai/openai-realtime.session.test.d.ts.map +1 -0
  20. package/dist/providers/openai/openai-realtime.session.test.js +149 -0
  21. package/dist/realtime-bootstrap.service.d.ts +16 -0
  22. package/dist/realtime-bootstrap.service.d.ts.map +1 -0
  23. package/dist/realtime-bootstrap.service.js +23 -0
  24. package/dist/realtime.gateway.d.ts +45 -0
  25. package/dist/realtime.gateway.d.ts.map +1 -0
  26. package/dist/realtime.gateway.js +94 -0
  27. package/dist/realtime.gateway.test.d.ts +2 -0
  28. package/dist/realtime.gateway.test.d.ts.map +1 -0
  29. package/dist/realtime.gateway.test.js +230 -0
  30. package/dist/realtime.module.d.ts +40 -0
  31. package/dist/realtime.module.d.ts.map +1 -0
  32. package/dist/realtime.module.js +87 -0
  33. package/dist/realtime.service.d.ts +41 -0
  34. package/dist/realtime.service.d.ts.map +1 -0
  35. package/dist/realtime.service.js +81 -0
  36. package/dist/realtime.service.test.d.ts +2 -0
  37. package/dist/realtime.service.test.d.ts.map +1 -0
  38. package/dist/realtime.service.test.js +129 -0
  39. package/dist/realtime.types.d.ts +81 -0
  40. package/dist/realtime.types.d.ts.map +1 -0
  41. package/dist/realtime.types.js +5 -0
  42. package/dist/realtime.types.test.d.ts +2 -0
  43. package/dist/realtime.types.test.d.ts.map +1 -0
  44. package/dist/realtime.types.test.js +61 -0
  45. package/package.json +58 -0
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const openai_realtime_session_1 = require("./openai-realtime.session");
4
+ const openai_realtime_client_1 = require("./openai-realtime.client");
5
+ const mockClientConnect = jest.fn().mockResolvedValue(undefined);
6
+ const mockClientSend = jest.fn();
7
+ const mockClientDisconnect = jest.fn();
8
+ jest.mock('./openai-realtime.client', () => ({
9
+ OpenAIRealtimeClient: jest.fn().mockImplementation(() => ({
10
+ connect: mockClientConnect,
11
+ send: jest.fn(),
12
+ appendAudio: jest.fn(),
13
+ addConversationItem: jest.fn(),
14
+ createResponse: jest.fn(),
15
+ onAny: jest.fn((handler) => {
16
+ mockOnAnyHandler = handler;
17
+ return () => { };
18
+ }),
19
+ disconnect: mockClientDisconnect,
20
+ connected: true,
21
+ })),
22
+ }));
23
+ let mockOnAnyHandler = null;
24
+ describe('OpenAIRealtimeSession', () => {
25
+ const mockHazelClient = {
26
+ id: 'session-1',
27
+ send: mockClientSend,
28
+ };
29
+ beforeEach(() => {
30
+ jest.clearAllMocks();
31
+ mockOnAnyHandler = null;
32
+ });
33
+ describe('connect', () => {
34
+ it('should connect the underlying client', async () => {
35
+ const session = new openai_realtime_session_1.OpenAIRealtimeSession(mockHazelClient, {
36
+ apiKey: 'test-key',
37
+ });
38
+ await session.connect();
39
+ expect(mockClientConnect).toHaveBeenCalled();
40
+ });
41
+ it('should forward server events to hazel client via onAny', async () => {
42
+ const session = new openai_realtime_session_1.OpenAIRealtimeSession(mockHazelClient, {
43
+ apiKey: 'test-key',
44
+ });
45
+ await session.connect();
46
+ expect(mockOnAnyHandler).toBeDefined();
47
+ mockOnAnyHandler({ type: 'session.created', session: { id: 's1' } });
48
+ expect(mockClientSend).toHaveBeenCalledWith('realtime', {
49
+ type: 'session.created',
50
+ session: { id: 's1' },
51
+ });
52
+ });
53
+ });
54
+ describe('handleClientMessage', () => {
55
+ it('should forward valid OpenAI events to client', async () => {
56
+ const session = new openai_realtime_session_1.OpenAIRealtimeSession(mockHazelClient, {
57
+ apiKey: 'test-key',
58
+ });
59
+ await session.connect();
60
+ session.handleClientMessage({
61
+ type: 'input_audio_buffer.append',
62
+ audio: 'base64data',
63
+ });
64
+ const clientInstance = openai_realtime_client_1.OpenAIRealtimeClient.mock.results[0]?.value;
65
+ expect(clientInstance.send).toHaveBeenCalledWith({
66
+ type: 'input_audio_buffer.append',
67
+ audio: 'base64data',
68
+ });
69
+ });
70
+ it('should ignore non-object payload', async () => {
71
+ const session = new openai_realtime_session_1.OpenAIRealtimeSession(mockHazelClient, {
72
+ apiKey: 'test-key',
73
+ });
74
+ await session.connect();
75
+ session.handleClientMessage(null);
76
+ session.handleClientMessage('string');
77
+ session.handleClientMessage(123);
78
+ const clientInstance = openai_realtime_client_1.OpenAIRealtimeClient.mock.results[0]?.value;
79
+ expect(clientInstance.send).not.toHaveBeenCalled();
80
+ });
81
+ it('should ignore payload without type', async () => {
82
+ const session = new openai_realtime_session_1.OpenAIRealtimeSession(mockHazelClient, {
83
+ apiKey: 'test-key',
84
+ });
85
+ await session.connect();
86
+ session.handleClientMessage({ data: 'no type' });
87
+ const clientInstance = openai_realtime_client_1.OpenAIRealtimeClient.mock.results[0]?.value;
88
+ expect(clientInstance.send).not.toHaveBeenCalled();
89
+ });
90
+ });
91
+ describe('appendAudio', () => {
92
+ it('should call client appendAudio', async () => {
93
+ const session = new openai_realtime_session_1.OpenAIRealtimeSession(mockHazelClient, {
94
+ apiKey: 'test-key',
95
+ });
96
+ await session.connect();
97
+ session.appendAudio('base64chunk');
98
+ const clientInstance = openai_realtime_client_1.OpenAIRealtimeClient.mock.results[0]?.value;
99
+ expect(clientInstance.appendAudio).toHaveBeenCalledWith('base64chunk');
100
+ });
101
+ });
102
+ describe('sendText', () => {
103
+ it('should add conversation item and create response', async () => {
104
+ const session = new openai_realtime_session_1.OpenAIRealtimeSession(mockHazelClient, {
105
+ apiKey: 'test-key',
106
+ });
107
+ await session.connect();
108
+ session.sendText('Hello world');
109
+ const clientInstance = openai_realtime_client_1.OpenAIRealtimeClient.mock.results[0]?.value;
110
+ expect(clientInstance.addConversationItem).toHaveBeenCalledWith('Hello world');
111
+ expect(clientInstance.createResponse).toHaveBeenCalledWith({
112
+ outputModalities: ['audio', 'text'],
113
+ });
114
+ });
115
+ });
116
+ describe('getStats', () => {
117
+ it('should return session stats', async () => {
118
+ const session = new openai_realtime_session_1.OpenAIRealtimeSession(mockHazelClient, {
119
+ apiKey: 'test-key',
120
+ });
121
+ await session.connect();
122
+ const stats = session.getStats();
123
+ expect(stats).toMatchObject({
124
+ sessionId: 'session-1',
125
+ provider: 'openai',
126
+ });
127
+ expect(stats.connectedAt).toBeDefined();
128
+ });
129
+ });
130
+ describe('disconnect', () => {
131
+ it('should disconnect the client', async () => {
132
+ const session = new openai_realtime_session_1.OpenAIRealtimeSession(mockHazelClient, {
133
+ apiKey: 'test-key',
134
+ });
135
+ await session.connect();
136
+ session.disconnect();
137
+ expect(mockClientDisconnect).toHaveBeenCalled();
138
+ });
139
+ });
140
+ describe('isConnected', () => {
141
+ it('should return client connected state', async () => {
142
+ const session = new openai_realtime_session_1.OpenAIRealtimeSession(mockHazelClient, {
143
+ apiKey: 'test-key',
144
+ });
145
+ await session.connect();
146
+ expect(session.isConnected).toBe(true);
147
+ });
148
+ });
149
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * RealtimeBootstrapService - attaches RealtimeGateway to HTTP server on app bootstrap
3
+ */
4
+ import type { HazelApp } from '@hazeljs/core';
5
+ import type { RealtimeGateway } from './realtime.gateway';
6
+ /**
7
+ * Implements OnApplicationBootstrap to auto-attach RealtimeGateway when the server is ready.
8
+ * Registered by RealtimeModule so users don't need to manually call gateway.attachToServer().
9
+ */
10
+ export declare class RealtimeBootstrapService {
11
+ private readonly gateway;
12
+ private readonly app;
13
+ constructor(gateway: RealtimeGateway, app: HazelApp);
14
+ onApplicationBootstrap(): void;
15
+ }
16
+ //# sourceMappingURL=realtime-bootstrap.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime-bootstrap.service.d.ts","sourceRoot":"","sources":["../src/realtime-bootstrap.service.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D;;;GAGG;AACH,qBAAa,wBAAwB;IAEjC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBADH,OAAO,EAAE,eAAe,EACxB,GAAG,EAAE,QAAQ;IAGhC,sBAAsB,IAAI,IAAI;CAM/B"}
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ /**
3
+ * RealtimeBootstrapService - attaches RealtimeGateway to HTTP server on app bootstrap
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RealtimeBootstrapService = void 0;
7
+ /**
8
+ * Implements OnApplicationBootstrap to auto-attach RealtimeGateway when the server is ready.
9
+ * Registered by RealtimeModule so users don't need to manually call gateway.attachToServer().
10
+ */
11
+ class RealtimeBootstrapService {
12
+ constructor(gateway, app) {
13
+ this.gateway = gateway;
14
+ this.app = app;
15
+ }
16
+ onApplicationBootstrap() {
17
+ const server = this.app.getServer();
18
+ if (server && this.gateway) {
19
+ this.gateway.attachToServer(server);
20
+ }
21
+ }
22
+ }
23
+ exports.RealtimeBootstrapService = RealtimeBootstrapService;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * RealtimeGateway - WebSocket server for real-time voice AI
3
+ * Uses @Realtime decorator, extends WebSocketGateway, proxies to OpenAI Realtime API
4
+ */
5
+ import 'reflect-metadata';
6
+ import { Server as HttpServer } from 'http';
7
+ import type { WebSocketServer } from 'ws';
8
+ import { WebSocketGateway } from '@hazeljs/websocket';
9
+ import type { WebSocketClient, WebSocketMessage } from '@hazeljs/websocket';
10
+ import { RealtimeService } from './realtime.service';
11
+ export interface RealtimeGatewayOptions {
12
+ /** WebSocket path (default: /realtime) */
13
+ path?: string;
14
+ /** Max payload size in bytes (default: 1MB) */
15
+ maxPayload?: number;
16
+ }
17
+ /**
18
+ * Gateway for real-time voice AI — uses @Realtime decorator for path/config
19
+ */
20
+ export declare class RealtimeGateway extends WebSocketGateway {
21
+ private readonly realtimeService;
22
+ private readonly realtimeOptions;
23
+ private sessionPromises;
24
+ constructor(realtimeService: RealtimeService, options?: RealtimeGatewayOptions);
25
+ /**
26
+ * Attach Realtime WebSocket server to existing HTTP server
27
+ */
28
+ attachToServer(server: HttpServer, options?: {
29
+ path?: string;
30
+ maxPayload?: number;
31
+ }): WebSocketServer;
32
+ /**
33
+ * Override: create OpenAI session on connection (async, stored for handleMessage)
34
+ */
35
+ protected handleConnection(client: WebSocketClient): void;
36
+ /**
37
+ * Override: forward messages to OpenAI session (await session if still connecting)
38
+ */
39
+ protected handleMessage(clientId: string, message: WebSocketMessage): void;
40
+ /**
41
+ * Override: cleanup session on disconnect
42
+ */
43
+ protected handleDisconnection(clientId: string): void;
44
+ }
45
+ //# sourceMappingURL=realtime.gateway.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime.gateway.d.ts","sourceRoot":"","sources":["../src/realtime.gateway.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,MAAM,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAiC,MAAM,oBAAoB,CAAC;AACrF,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE5E,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,qBACa,eAAgB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyB;IACzD,OAAO,CAAC,eAAe,CAAqD;gBAEhE,eAAe,EAAE,eAAe,EAAE,OAAO,GAAE,sBAA2B;IAUlF;;OAEG;IACM,cAAc,CACrB,MAAM,EAAE,UAAU,EAClB,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAC/C,eAAe;IAQlB;;OAEG;cACgB,gBAAgB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI;IAelE;;OAEG;cACgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAcnF;;OAEG;cACgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;CAM/D"}
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ /**
3
+ * RealtimeGateway - WebSocket server for real-time voice AI
4
+ * Uses @Realtime decorator, extends WebSocketGateway, proxies to OpenAI Realtime API
5
+ */
6
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
7
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
8
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
9
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
10
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
11
+ };
12
+ var __metadata = (this && this.__metadata) || function (k, v) {
13
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
14
+ };
15
+ var __importDefault = (this && this.__importDefault) || function (mod) {
16
+ return (mod && mod.__esModule) ? mod : { "default": mod };
17
+ };
18
+ var RealtimeGateway_1;
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.RealtimeGateway = void 0;
21
+ require("reflect-metadata");
22
+ const websocket_1 = require("@hazeljs/websocket");
23
+ const core_1 = __importDefault(require("@hazeljs/core"));
24
+ const realtime_service_1 = require("./realtime.service");
25
+ /**
26
+ * Gateway for real-time voice AI — uses @Realtime decorator for path/config
27
+ */
28
+ let RealtimeGateway = RealtimeGateway_1 = class RealtimeGateway extends websocket_1.WebSocketGateway {
29
+ constructor(realtimeService, options = {}) {
30
+ super();
31
+ this.sessionPromises = new Map();
32
+ this.realtimeService = realtimeService;
33
+ const metadata = (0, websocket_1.getRealtimeMetadata)(RealtimeGateway_1);
34
+ this.realtimeOptions = {
35
+ path: options.path ?? metadata?.path ?? '/realtime',
36
+ maxPayload: options.maxPayload ?? metadata?.maxPayload ?? 1048576,
37
+ };
38
+ }
39
+ /**
40
+ * Attach Realtime WebSocket server to existing HTTP server
41
+ */
42
+ attachToServer(server, options) {
43
+ return super.attachToServer(server, {
44
+ path: options?.path ?? this.realtimeOptions.path,
45
+ maxPayload: options?.maxPayload ?? this.realtimeOptions.maxPayload,
46
+ perMessageDeflate: false,
47
+ });
48
+ }
49
+ /**
50
+ * Override: create OpenAI session on connection (async, stored for handleMessage)
51
+ */
52
+ handleConnection(client) {
53
+ const clientId = client.id;
54
+ const sessionPromise = this.realtimeService.createOpenAISession(client);
55
+ this.sessionPromises.set(clientId, sessionPromise);
56
+ sessionPromise
57
+ .then(() => core_1.default.info(`Realtime session started: ${clientId}`))
58
+ .catch((err) => {
59
+ core_1.default.error(`Realtime session failed for ${clientId}:`, err);
60
+ client.disconnect();
61
+ });
62
+ super.handleConnection(client);
63
+ }
64
+ /**
65
+ * Override: forward messages to OpenAI session (await session if still connecting)
66
+ */
67
+ handleMessage(clientId, message) {
68
+ const sessionPromise = this.sessionPromises.get(clientId);
69
+ if (!sessionPromise)
70
+ return;
71
+ sessionPromise
72
+ .then((session) => {
73
+ // Realtime client sends raw OpenAI events (e.g. { type, audio })
74
+ session.handleClientMessage(message);
75
+ })
76
+ .catch(() => {
77
+ // Session failed, ignore message
78
+ });
79
+ }
80
+ /**
81
+ * Override: cleanup session on disconnect
82
+ */
83
+ handleDisconnection(clientId) {
84
+ this.sessionPromises.delete(clientId);
85
+ this.realtimeService.removeSession(clientId);
86
+ core_1.default.info(`Realtime session ended: ${clientId}`);
87
+ super.handleDisconnection(clientId);
88
+ }
89
+ };
90
+ exports.RealtimeGateway = RealtimeGateway;
91
+ exports.RealtimeGateway = RealtimeGateway = RealtimeGateway_1 = __decorate([
92
+ (0, websocket_1.Realtime)({ path: '/realtime', maxPayload: 1048576 }),
93
+ __metadata("design:paramtypes", [realtime_service_1.RealtimeService, Object])
94
+ ], RealtimeGateway);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=realtime.gateway.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime.gateway.test.d.ts","sourceRoot":"","sources":["../src/realtime.gateway.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const http_1 = require("http");
4
+ const realtime_service_1 = require("./realtime.service");
5
+ jest.mock('./providers/openai', () => {
6
+ const session = {
7
+ connect: jest.fn().mockResolvedValue(undefined),
8
+ disconnect: jest.fn(),
9
+ handleClientMessage: jest.fn(),
10
+ getStats: jest.fn().mockReturnValue({ sessionId: 'test', provider: 'openai' }),
11
+ isConnected: true,
12
+ };
13
+ global.__gatewayMockSession = session;
14
+ return {
15
+ OpenAIRealtimeSession: jest.fn(() => session),
16
+ };
17
+ });
18
+ const mockSession = global.__gatewayMockSession;
19
+ jest.mock('@hazeljs/core', () => ({
20
+ __esModule: true,
21
+ default: {
22
+ info: jest.fn(),
23
+ debug: jest.fn(),
24
+ error: jest.fn(),
25
+ warn: jest.fn(),
26
+ },
27
+ Service: () => () => { },
28
+ }));
29
+ jest.mock('@hazeljs/websocket', () => {
30
+ const mockWss = { close: jest.fn((cb) => cb?.()) };
31
+ class MockWebSocketGateway {
32
+ constructor() {
33
+ this.attachToServer = jest.fn().mockReturnValue(mockWss);
34
+ this.close = jest.fn().mockResolvedValue(undefined);
35
+ }
36
+ handleConnection() { }
37
+ handleMessage() { }
38
+ handleDisconnection() { }
39
+ }
40
+ const Realtime = () => () => { };
41
+ const getRealtimeMetadata = jest.fn().mockReturnValue({
42
+ path: '/realtime',
43
+ maxPayload: 1048576,
44
+ });
45
+ return {
46
+ __esModule: true,
47
+ WebSocketGateway: MockWebSocketGateway,
48
+ Realtime,
49
+ getRealtimeMetadata,
50
+ };
51
+ });
52
+ const realtime_gateway_1 = require("./realtime.gateway");
53
+ describe('RealtimeGateway', () => {
54
+ let realtimeService;
55
+ let gateway;
56
+ beforeEach(() => {
57
+ jest.clearAllMocks();
58
+ mockSession.connect.mockResolvedValue(undefined);
59
+ process.env.OPENAI_API_KEY = 'test-key';
60
+ realtimeService = new realtime_service_1.RealtimeService({ openaiApiKey: 'test-key' });
61
+ gateway = new realtime_gateway_1.RealtimeGateway(realtimeService);
62
+ });
63
+ afterEach(() => {
64
+ delete process.env.OPENAI_API_KEY;
65
+ });
66
+ describe('constructor', () => {
67
+ it('should create gateway with default options', () => {
68
+ expect(gateway).toBeDefined();
69
+ });
70
+ it('should accept custom options', () => {
71
+ const customGateway = new realtime_gateway_1.RealtimeGateway(realtimeService, {
72
+ path: '/voice',
73
+ maxPayload: 2048,
74
+ });
75
+ expect(customGateway).toBeDefined();
76
+ });
77
+ });
78
+ describe('attachToServer', () => {
79
+ it('should attach to HTTP server', () => {
80
+ const server = (0, http_1.createServer)(() => { });
81
+ const result = gateway.attachToServer(server);
82
+ expect(result).toBeDefined();
83
+ server.close();
84
+ });
85
+ it('should use gateway options when attachToServer called without options', () => {
86
+ const server = (0, http_1.createServer)(() => { });
87
+ const customGateway = new realtime_gateway_1.RealtimeGateway(realtimeService, { path: '/voice' });
88
+ const result = customGateway.attachToServer(server);
89
+ expect(result).toBeDefined();
90
+ server.close();
91
+ });
92
+ it('should use custom path when provided', () => {
93
+ const server = (0, http_1.createServer)(() => { });
94
+ const customGateway = new realtime_gateway_1.RealtimeGateway(realtimeService, {
95
+ path: '/voice',
96
+ });
97
+ const result = customGateway.attachToServer(server, { path: '/custom' });
98
+ expect(result).toBeDefined();
99
+ server.close();
100
+ });
101
+ });
102
+ describe('handleConnection error', () => {
103
+ it('should disconnect client when session creation fails', async () => {
104
+ mockSession.connect.mockRejectedValueOnce(new Error('Connection failed'));
105
+ const mockClient = {
106
+ id: 'client-fail',
107
+ socket: { on: jest.fn(), send: jest.fn(), close: jest.fn() },
108
+ metadata: new Map(),
109
+ rooms: new Set(),
110
+ send: jest.fn(),
111
+ disconnect: jest.fn(),
112
+ join: jest.fn(),
113
+ leave: jest.fn(),
114
+ inRoom: jest.fn().mockReturnValue(false),
115
+ };
116
+ gateway.handleConnection(mockClient);
117
+ await new Promise((r) => setTimeout(r, 50));
118
+ expect(mockClient.disconnect).toHaveBeenCalled();
119
+ mockSession.connect.mockResolvedValue(undefined);
120
+ });
121
+ });
122
+ describe('handleConnection', () => {
123
+ it('should create OpenAI session on connection', async () => {
124
+ const mockClient = {
125
+ id: 'client-123',
126
+ socket: { on: jest.fn(), send: jest.fn(), close: jest.fn() },
127
+ metadata: new Map(),
128
+ rooms: new Set(),
129
+ send: jest.fn(),
130
+ disconnect: jest.fn(),
131
+ join: jest.fn(),
132
+ leave: jest.fn(),
133
+ inRoom: jest.fn().mockReturnValue(false),
134
+ };
135
+ gateway.handleConnection(mockClient);
136
+ await new Promise((r) => setTimeout(r, 50));
137
+ expect(realtimeService.getSession('client-123')).toBeDefined();
138
+ });
139
+ });
140
+ describe('handleMessage', () => {
141
+ it('should forward message to session when session exists', async () => {
142
+ const mockClient = {
143
+ id: 'client-msg',
144
+ send: jest.fn(),
145
+ };
146
+ await realtimeService.createOpenAISession(mockClient);
147
+ const server = (0, http_1.createServer)(() => { });
148
+ gateway.attachToServer(server);
149
+ const mockSocket = {
150
+ on: jest.fn((ev, fn) => {
151
+ if (ev === 'message')
152
+ _mockMessageHandler = fn;
153
+ return mockSocket;
154
+ }),
155
+ send: jest.fn(),
156
+ close: jest.fn(),
157
+ };
158
+ let _mockMessageHandler = () => { };
159
+ const mockClientForConn = {
160
+ id: 'client-msg',
161
+ socket: mockSocket,
162
+ metadata: new Map(),
163
+ rooms: new Set(),
164
+ send: jest.fn(),
165
+ disconnect: jest.fn(),
166
+ join: jest.fn(),
167
+ leave: jest.fn(),
168
+ inRoom: jest.fn().mockReturnValue(false),
169
+ };
170
+ gateway.handleConnection(mockClientForConn);
171
+ await new Promise((r) => setTimeout(r, 50));
172
+ const session = realtimeService.getSession('client-msg');
173
+ expect(session).toBeDefined();
174
+ gateway.handleMessage('client-msg', { type: 'test.event', data: 'payload' });
175
+ await new Promise((r) => setTimeout(r, 50));
176
+ expect(mockSession.handleClientMessage).toHaveBeenCalledWith({
177
+ type: 'test.event',
178
+ data: 'payload',
179
+ });
180
+ server.close();
181
+ });
182
+ it('should do nothing when session does not exist', () => {
183
+ gateway.handleMessage('non-existent', { type: 'test' });
184
+ expect(mockSession.handleClientMessage).not.toHaveBeenCalled();
185
+ });
186
+ it('should ignore message when session creation failed', async () => {
187
+ mockSession.connect.mockRejectedValueOnce(new Error('fail'));
188
+ const mockClient = {
189
+ id: 'client-msg-fail',
190
+ socket: { on: jest.fn(), send: jest.fn(), close: jest.fn() },
191
+ metadata: new Map(),
192
+ rooms: new Set(),
193
+ send: jest.fn(),
194
+ disconnect: jest.fn(),
195
+ join: jest.fn(),
196
+ leave: jest.fn(),
197
+ inRoom: jest.fn().mockReturnValue(false),
198
+ };
199
+ gateway.handleConnection(mockClient);
200
+ gateway.handleMessage('client-msg-fail', { type: 'test.event' });
201
+ await new Promise((r) => setTimeout(r, 50));
202
+ expect(mockSession.handleClientMessage).not.toHaveBeenCalled();
203
+ mockSession.connect.mockResolvedValue(undefined);
204
+ });
205
+ });
206
+ describe('handleDisconnection', () => {
207
+ it('should remove session on disconnect', async () => {
208
+ const mockClient = {
209
+ id: 'client-disc',
210
+ send: jest.fn(),
211
+ };
212
+ await realtimeService.createOpenAISession(mockClient);
213
+ expect(realtimeService.getSession('client-disc')).toBeDefined();
214
+ gateway.handleDisconnection('client-disc');
215
+ expect(realtimeService.getSession('client-disc')).toBeUndefined();
216
+ expect(mockSession.disconnect).toHaveBeenCalled();
217
+ });
218
+ });
219
+ describe('close', () => {
220
+ it('should close the gateway', async () => {
221
+ const server = (0, http_1.createServer)(() => { });
222
+ gateway.attachToServer(server);
223
+ await gateway.close();
224
+ expect(true).toBe(true);
225
+ });
226
+ it('should resolve when not attached', async () => {
227
+ await expect(gateway.close()).resolves.toBeUndefined();
228
+ });
229
+ });
230
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * RealtimeModule - Real-time voice AI for HazelJS
3
+ */
4
+ import { RealtimeService } from './realtime.service';
5
+ import { RealtimeGateway } from './realtime.gateway';
6
+ import { RealtimeBootstrapService } from './realtime-bootstrap.service';
7
+ import type { RealtimeModuleOptions } from './realtime.types';
8
+ export declare class RealtimeModule {
9
+ /**
10
+ * Configure Realtime module with OpenAI Realtime API
11
+ */
12
+ static forRoot(options?: RealtimeModuleOptions): {
13
+ module: typeof RealtimeModule;
14
+ providers: Array<{
15
+ provide: typeof RealtimeService | typeof RealtimeGateway | typeof RealtimeBootstrapService;
16
+ useClass?: typeof RealtimeBootstrapService;
17
+ useValue?: RealtimeService | RealtimeGateway;
18
+ }>;
19
+ exports: (typeof RealtimeService | typeof RealtimeGateway)[];
20
+ global: boolean;
21
+ };
22
+ /**
23
+ * Configure Realtime module asynchronously (e.g. from config service)
24
+ */
25
+ static forRootAsync(options: {
26
+ useFactory: (...args: unknown[]) => Promise<RealtimeModuleOptions> | RealtimeModuleOptions;
27
+ inject?: unknown[];
28
+ }): {
29
+ module: typeof RealtimeModule;
30
+ providers: Array<{
31
+ provide: string | typeof RealtimeService | typeof RealtimeGateway | typeof RealtimeBootstrapService;
32
+ useFactory?: unknown;
33
+ useClass?: typeof RealtimeBootstrapService;
34
+ inject?: unknown[];
35
+ }>;
36
+ exports: (typeof RealtimeService | typeof RealtimeGateway)[];
37
+ global: boolean;
38
+ };
39
+ }
40
+ //# sourceMappingURL=realtime.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime.module.d.ts","sourceRoot":"","sources":["../src/realtime.module.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAE9D,qBAIa,cAAc;IACzB;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,qBAA0B,GAAG;QACnD,MAAM,EAAE,OAAO,cAAc,CAAC;QAC9B,SAAS,EAAE,KAAK,CAAC;YACf,OAAO,EAAE,OAAO,eAAe,GAAG,OAAO,eAAe,GAAG,OAAO,wBAAwB,CAAC;YAC3F,QAAQ,CAAC,EAAE,OAAO,wBAAwB,CAAC;YAC3C,QAAQ,CAAC,EAAE,eAAe,GAAG,eAAe,CAAC;SAC9C,CAAC,CAAC;QACH,OAAO,EAAE,CAAC,OAAO,eAAe,GAAG,OAAO,eAAe,CAAC,EAAE,CAAC;QAC7D,MAAM,EAAE,OAAO,CAAC;KACjB;IA0BD;;OAEG;IACH,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE;QAC3B,UAAU,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,qBAAqB,CAAC,GAAG,qBAAqB,CAAC;QAC3F,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;KACpB,GAAG;QACF,MAAM,EAAE,OAAO,cAAc,CAAC;QAC9B,SAAS,EAAE,KAAK,CAAC;YACf,OAAO,EACH,MAAM,GACN,OAAO,eAAe,GACtB,OAAO,eAAe,GACtB,OAAO,wBAAwB,CAAC;YACpC,UAAU,CAAC,EAAE,OAAO,CAAC;YACrB,QAAQ,CAAC,EAAE,OAAO,wBAAwB,CAAC;YAC3C,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;SACpB,CAAC,CAAC;QACH,OAAO,EAAE,CAAC,OAAO,eAAe,GAAG,OAAO,eAAe,CAAC,EAAE,CAAC;QAC7D,MAAM,EAAE,OAAO,CAAC;KACjB;CAkCF"}