@volley/recognition-client-sdk 0.1.211 → 0.1.254
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 +83 -7
- package/dist/{browser-C4ZssGoU.d.ts → browser-BZs4BL_w.d.ts} +84 -6
- package/dist/index.d.ts +630 -16
- package/dist/index.js +276 -44
- package/dist/index.js.map +1 -1
- package/dist/recog-client-sdk.browser.d.ts +1 -1
- package/dist/recog-client-sdk.browser.js +195 -42
- package/dist/recog-client-sdk.browser.js.map +1 -1
- package/package.json +2 -2
- package/src/config-builder.ts +21 -3
- package/src/errors.ts +84 -0
- package/src/index.ts +34 -1
- package/src/recognition-client.spec.ts +39 -0
- package/src/recognition-client.ts +153 -40
- package/src/recognition-client.types.ts +58 -6
- package/src/simplified-vgf-recognition-client.ts +9 -0
- package/src/utils/url-builder.spec.ts +53 -6
- package/src/utils/url-builder.ts +19 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volley/recognition-client-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.254",
|
|
4
4
|
"description": "Recognition Service TypeScript/Node.js Client SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"ts-jest": "^29.4.5",
|
|
54
54
|
"tsup": "^8.5.0",
|
|
55
55
|
"typescript": "^5.1.6",
|
|
56
|
+
"@recog/shared-types": "1.0.0",
|
|
56
57
|
"@recog/shared-config": "1.0.0",
|
|
57
58
|
"@recog/websocket": "1.0.0",
|
|
58
|
-
"@recog/shared-types": "1.0.0",
|
|
59
59
|
"@recog/shared-utils": "1.0.0"
|
|
60
60
|
},
|
|
61
61
|
"keywords": [
|
package/src/config-builder.ts
CHANGED
|
@@ -13,7 +13,8 @@ import type {
|
|
|
13
13
|
GameContextV1,
|
|
14
14
|
TranscriptionResultV1,
|
|
15
15
|
MetadataResultV1,
|
|
16
|
-
ErrorResultV1
|
|
16
|
+
ErrorResultV1,
|
|
17
|
+
Stage
|
|
17
18
|
} from '@recog/shared-types';
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -23,8 +24,10 @@ import type {
|
|
|
23
24
|
*
|
|
24
25
|
* Example:
|
|
25
26
|
* ```typescript
|
|
27
|
+
* import { STAGES } from '@recog/shared-types';
|
|
28
|
+
*
|
|
26
29
|
* const config = new ConfigBuilder()
|
|
27
|
-
* .
|
|
30
|
+
* .stage(STAGES.STAGING) // Recommended: automatic environment selection
|
|
28
31
|
* .asrRequestConfig({
|
|
29
32
|
* provider: RecognitionProvider.DEEPGRAM,
|
|
30
33
|
* model: 'nova-2-general'
|
|
@@ -37,13 +40,28 @@ export class ConfigBuilder {
|
|
|
37
40
|
private config: Partial<RealTimeTwoWayWebSocketRecognitionClientConfig> = {};
|
|
38
41
|
|
|
39
42
|
/**
|
|
40
|
-
* Set the WebSocket URL
|
|
43
|
+
* Set the WebSocket URL (advanced usage)
|
|
44
|
+
* For standard environments, use stage() instead
|
|
41
45
|
*/
|
|
42
46
|
url(url: string): this {
|
|
43
47
|
this.config.url = url;
|
|
44
48
|
return this;
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Set the stage for automatic environment selection (recommended)
|
|
53
|
+
* @param stage - STAGES.LOCAL | STAGES.DEV | STAGES.STAGING | STAGES.PRODUCTION
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* import { STAGES } from '@recog/shared-types';
|
|
57
|
+
* builder.stage(STAGES.STAGING)
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
stage(stage: Stage | string): this {
|
|
61
|
+
this.config.stage = stage;
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
/**
|
|
48
66
|
* Set ASR request configuration
|
|
49
67
|
*/
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK Error Classes
|
|
3
|
+
*
|
|
4
|
+
* Typed error classes that extend native Error with recognition-specific metadata
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ErrorTypeV1 } from '@recog/shared-types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Base class for all recognition SDK errors
|
|
11
|
+
*/
|
|
12
|
+
export class RecognitionError extends Error {
|
|
13
|
+
public readonly errorType: ErrorTypeV1;
|
|
14
|
+
public readonly timestamp: number;
|
|
15
|
+
|
|
16
|
+
constructor(errorType: ErrorTypeV1, message: string) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'RecognitionError';
|
|
19
|
+
this.errorType = errorType;
|
|
20
|
+
this.timestamp = Date.now();
|
|
21
|
+
|
|
22
|
+
// Maintains proper stack trace for where error was thrown (only available on V8)
|
|
23
|
+
if (Error.captureStackTrace) {
|
|
24
|
+
Error.captureStackTrace(this, this.constructor);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Connection error - thrown when WebSocket connection fails after all retry attempts
|
|
31
|
+
*/
|
|
32
|
+
export class ConnectionError extends RecognitionError {
|
|
33
|
+
public readonly attempts: number;
|
|
34
|
+
public readonly url: string;
|
|
35
|
+
public readonly underlyingError?: Error;
|
|
36
|
+
|
|
37
|
+
constructor(message: string, attempts: number, url: string, underlyingError?: Error) {
|
|
38
|
+
super(ErrorTypeV1.CONNECTION_ERROR, message);
|
|
39
|
+
this.name = 'ConnectionError';
|
|
40
|
+
this.attempts = attempts;
|
|
41
|
+
this.url = url;
|
|
42
|
+
if (underlyingError !== undefined) {
|
|
43
|
+
this.underlyingError = underlyingError;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Timeout error - thrown when operations exceed timeout limits
|
|
50
|
+
*/
|
|
51
|
+
export class TimeoutError extends RecognitionError {
|
|
52
|
+
public readonly timeoutMs: number;
|
|
53
|
+
public readonly operation: string;
|
|
54
|
+
|
|
55
|
+
constructor(message: string, timeoutMs: number, operation: string) {
|
|
56
|
+
super(ErrorTypeV1.TIMEOUT_ERROR, message);
|
|
57
|
+
this.name = 'TimeoutError';
|
|
58
|
+
this.timeoutMs = timeoutMs;
|
|
59
|
+
this.operation = operation;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validation error - thrown when invalid configuration or input is provided
|
|
65
|
+
*/
|
|
66
|
+
export class ValidationError extends RecognitionError {
|
|
67
|
+
public readonly field?: string;
|
|
68
|
+
public readonly expected?: string;
|
|
69
|
+
public readonly received?: string;
|
|
70
|
+
|
|
71
|
+
constructor(message: string, field?: string, expected?: string, received?: string) {
|
|
72
|
+
super(ErrorTypeV1.VALIDATION_ERROR, message);
|
|
73
|
+
this.name = 'ValidationError';
|
|
74
|
+
if (field !== undefined) {
|
|
75
|
+
this.field = field;
|
|
76
|
+
}
|
|
77
|
+
if (expected !== undefined) {
|
|
78
|
+
this.expected = expected;
|
|
79
|
+
}
|
|
80
|
+
if (received !== undefined) {
|
|
81
|
+
this.received = received;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,35 @@ export { ConfigBuilder } from './config-builder.js';
|
|
|
21
21
|
// Export factory functions
|
|
22
22
|
export { createClient, createClientWithBuilder } from './factory.js';
|
|
23
23
|
|
|
24
|
+
// Export error classes
|
|
25
|
+
export {
|
|
26
|
+
RecognitionError,
|
|
27
|
+
ConnectionError,
|
|
28
|
+
TimeoutError,
|
|
29
|
+
ValidationError
|
|
30
|
+
} from './errors.js';
|
|
31
|
+
|
|
32
|
+
// Export error types from shared-types
|
|
33
|
+
export { ErrorTypeV1 } from '@recog/shared-types';
|
|
34
|
+
|
|
35
|
+
// Export error exception types for advanced error handling
|
|
36
|
+
export type {
|
|
37
|
+
RecognitionException,
|
|
38
|
+
ConnectionException,
|
|
39
|
+
TimeoutException,
|
|
40
|
+
ValidationException,
|
|
41
|
+
AuthenticationException,
|
|
42
|
+
ProviderException,
|
|
43
|
+
QuotaExceededException,
|
|
44
|
+
UnknownException
|
|
45
|
+
} from '@recog/shared-types';
|
|
46
|
+
|
|
47
|
+
// Export error helper functions
|
|
48
|
+
export {
|
|
49
|
+
isExceptionImmediatelyAvailable,
|
|
50
|
+
getUserFriendlyMessage
|
|
51
|
+
} from '@recog/shared-types';
|
|
52
|
+
|
|
24
53
|
// Export VGF state management (new simplified interface)
|
|
25
54
|
export {
|
|
26
55
|
SimplifiedVGFRecognitionClient,
|
|
@@ -67,7 +96,11 @@ export {
|
|
|
67
96
|
GeminiModel,
|
|
68
97
|
OpenAIModel,
|
|
69
98
|
Language,
|
|
70
|
-
SampleRate
|
|
99
|
+
SampleRate,
|
|
100
|
+
|
|
101
|
+
// Stage/Environment types
|
|
102
|
+
STAGES,
|
|
103
|
+
type Stage
|
|
71
104
|
} from '@recog/shared-types';
|
|
72
105
|
|
|
73
106
|
// Re-export shared config helpers so consumers don't depend on internal package
|
|
@@ -60,6 +60,45 @@ describe('RealTimeTwoWayWebSocketRecognitionClient', () => {
|
|
|
60
60
|
expect(client.getAudioUtteranceId()).toBe(originalId);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it('should expose the WebSocket URL via getUrl()', () => {
|
|
64
|
+
const url = client.getUrl();
|
|
65
|
+
expect(url).toBeDefined();
|
|
66
|
+
expect(typeof url).toBe('string');
|
|
67
|
+
expect(url).toContain('audioUtteranceId=');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should build URL from stage parameter', () => {
|
|
71
|
+
const stagingClient = new RealTimeTwoWayWebSocketRecognitionClient({
|
|
72
|
+
stage: 'staging',
|
|
73
|
+
asrRequestConfig: {
|
|
74
|
+
provider: 'deepgram',
|
|
75
|
+
language: 'en',
|
|
76
|
+
sampleRate: 16000,
|
|
77
|
+
encoding: 'linear16'
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
const url = stagingClient.getUrl();
|
|
81
|
+
expect(url).toBeDefined();
|
|
82
|
+
// URL should be built from stage (exact URL depends on mocked getRecognitionServiceBase)
|
|
83
|
+
expect(url).toContain('/ws/v1/recognize');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should prioritize url over stage when both provided', () => {
|
|
87
|
+
const explicitUrlClient = new RealTimeTwoWayWebSocketRecognitionClient({
|
|
88
|
+
url: 'ws://custom.example.com/ws/v1/recognize',
|
|
89
|
+
stage: 'staging',
|
|
90
|
+
asrRequestConfig: {
|
|
91
|
+
provider: 'deepgram',
|
|
92
|
+
language: 'en',
|
|
93
|
+
sampleRate: 16000,
|
|
94
|
+
encoding: 'linear16'
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
const url = explicitUrlClient.getUrl();
|
|
98
|
+
expect(url).toContain('ws://custom.example.com/ws/v1/recognize');
|
|
99
|
+
expect(url).not.toContain('staging');
|
|
100
|
+
});
|
|
101
|
+
|
|
63
102
|
it('should initialize stats correctly', () => {
|
|
64
103
|
const stats = client.getStats();
|
|
65
104
|
expect(stats.audioBytesSent).toBe(0);
|
|
@@ -61,6 +61,7 @@ import type {
|
|
|
61
61
|
import { buildWebSocketUrl } from './utils/url-builder.js';
|
|
62
62
|
import { AudioRingBuffer } from './utils/audio-ring-buffer.js';
|
|
63
63
|
import { MessageHandler } from './utils/message-handler.js';
|
|
64
|
+
import { ConnectionError } from './errors.js';
|
|
64
65
|
|
|
65
66
|
// ============================================================================
|
|
66
67
|
// UTILITIES
|
|
@@ -132,6 +133,10 @@ interface InternalConfig {
|
|
|
132
133
|
lowWaterMark: number;
|
|
133
134
|
maxBufferDurationSec: number;
|
|
134
135
|
chunksPerSecond: number;
|
|
136
|
+
connectionRetry: {
|
|
137
|
+
maxAttempts: number;
|
|
138
|
+
delayMs: number;
|
|
139
|
+
};
|
|
135
140
|
logger?: (level: 'debug' | 'info' | 'warn' | 'error', message: string, data?: any) => void;
|
|
136
141
|
}
|
|
137
142
|
|
|
@@ -171,9 +176,11 @@ export class RealTimeTwoWayWebSocketRecognitionClient
|
|
|
171
176
|
const audioUtteranceId = config.audioUtteranceId || uuidv4();
|
|
172
177
|
|
|
173
178
|
// Build WebSocket URL with query parameters
|
|
179
|
+
// Precedence: url > stage > default production
|
|
174
180
|
const url = buildWebSocketUrl({
|
|
175
181
|
audioUtteranceId,
|
|
176
182
|
...(config.url && { url: config.url }),
|
|
183
|
+
...(config.stage && { stage: config.stage }),
|
|
177
184
|
...(config.callbackUrls && { callbackUrls: config.callbackUrls }),
|
|
178
185
|
...(config.userId && { userId: config.userId }),
|
|
179
186
|
...(config.gameSessionId && { gameSessionId: config.gameSessionId }),
|
|
@@ -191,6 +198,11 @@ export class RealTimeTwoWayWebSocketRecognitionClient
|
|
|
191
198
|
lowWM: config.lowWaterMark ?? 128_000
|
|
192
199
|
});
|
|
193
200
|
|
|
201
|
+
// Process retry config with defaults and validation
|
|
202
|
+
const retryConfig = config.connectionRetry || {};
|
|
203
|
+
const maxAttempts = Math.max(1, Math.min(5, retryConfig.maxAttempts ?? 4)); // Default: 4 attempts (3 retries), clamp 1-5
|
|
204
|
+
const delayMs = retryConfig.delayMs ?? 200; // Fast retry for short audio sessions
|
|
205
|
+
|
|
194
206
|
// Process config with defaults
|
|
195
207
|
this.config = {
|
|
196
208
|
url,
|
|
@@ -208,6 +220,10 @@ export class RealTimeTwoWayWebSocketRecognitionClient
|
|
|
208
220
|
lowWaterMark: config.lowWaterMark ?? 128_000,
|
|
209
221
|
maxBufferDurationSec: config.maxBufferDurationSec ?? 60,
|
|
210
222
|
chunksPerSecond: config.chunksPerSecond ?? 100,
|
|
223
|
+
connectionRetry: {
|
|
224
|
+
maxAttempts,
|
|
225
|
+
delayMs
|
|
226
|
+
},
|
|
211
227
|
...(config.logger && { logger: config.logger })
|
|
212
228
|
};
|
|
213
229
|
|
|
@@ -275,11 +291,10 @@ export class RealTimeTwoWayWebSocketRecognitionClient
|
|
|
275
291
|
// ==========================================================================
|
|
276
292
|
|
|
277
293
|
override async connect(): Promise<void> {
|
|
278
|
-
// FIRST:
|
|
294
|
+
// FIRST: Prevent concurrent connection attempts - return existing promise if connecting
|
|
279
295
|
if (this.connectionPromise) {
|
|
280
|
-
this.log('debug', 'Returning existing connection promise', {
|
|
281
|
-
state: this.state
|
|
282
|
-
hasPromise: true
|
|
296
|
+
this.log('debug', 'Returning existing connection promise (already connecting)', {
|
|
297
|
+
state: this.state
|
|
283
298
|
});
|
|
284
299
|
return this.connectionPromise;
|
|
285
300
|
}
|
|
@@ -297,48 +312,142 @@ export class RealTimeTwoWayWebSocketRecognitionClient
|
|
|
297
312
|
return Promise.resolve();
|
|
298
313
|
}
|
|
299
314
|
|
|
300
|
-
//
|
|
301
|
-
//
|
|
315
|
+
// THIRD: Create connection promise with retry logic
|
|
316
|
+
// Store the promise IMMEDIATELY to prevent concurrent attempts
|
|
317
|
+
this.connectionPromise = this.connectWithRetry();
|
|
302
318
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const connectionStartTime = Date.now();
|
|
319
|
+
return this.connectionPromise;
|
|
320
|
+
}
|
|
307
321
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Attempt to connect with retry logic
|
|
324
|
+
* Only retries on initial connection establishment, not mid-stream interruptions
|
|
325
|
+
*/
|
|
326
|
+
private async connectWithRetry(): Promise<void> {
|
|
327
|
+
const { maxAttempts, delayMs } = this.config.connectionRetry;
|
|
328
|
+
const connectionTimeout = 10000; // 10 second timeout per attempt
|
|
329
|
+
|
|
330
|
+
// TODO: Consider implementing error-code-based retry strategy
|
|
331
|
+
// - Retry on 503 (Service Unavailable) with longer delays
|
|
332
|
+
// - Don't retry on 401 (Unauthorized) or 400 (Bad Request)
|
|
333
|
+
// - Requires extracting HTTP status from WebSocket connection error
|
|
334
|
+
// For now: Simple retry for all connection failures
|
|
335
|
+
|
|
336
|
+
let lastError: Error | undefined;
|
|
337
|
+
|
|
338
|
+
// Store original handlers once (not per-attempt to avoid nested wrappers)
|
|
339
|
+
const originalOnConnected = this.config.onConnected;
|
|
340
|
+
const originalOnError = this.config.onError;
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
344
|
+
// Use debug for first attempt (usually succeeds), info for retries
|
|
345
|
+
const attemptLogLevel = attempt === 1 ? 'debug' : 'info';
|
|
346
|
+
this.log(attemptLogLevel, `Connection attempt ${attempt}/${maxAttempts}`, {
|
|
347
|
+
url: this.config.url,
|
|
348
|
+
delayMs: attempt > 1 ? delayMs : 0
|
|
323
349
|
});
|
|
324
|
-
this.state = ClientState.CONNECTED;
|
|
325
|
-
originalOnConnected();
|
|
326
|
-
resolve();
|
|
327
|
-
};
|
|
328
350
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
351
|
+
this.state = ClientState.CONNECTING;
|
|
352
|
+
const connectionStartTime = Date.now();
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
// Create promise for this single attempt with timeout
|
|
356
|
+
await new Promise<void>((resolve, reject) => {
|
|
357
|
+
let settled = false; // Guard against late callbacks for this attempt
|
|
358
|
+
|
|
359
|
+
const timeout = setTimeout(() => {
|
|
360
|
+
if (settled) return;
|
|
361
|
+
settled = true;
|
|
362
|
+
this.log('warn', 'Connection timeout', { timeout: connectionTimeout, attempt });
|
|
363
|
+
this.state = ClientState.FAILED;
|
|
364
|
+
reject(new Error(`Connection timeout after ${connectionTimeout}ms`));
|
|
365
|
+
}, connectionTimeout);
|
|
366
|
+
|
|
367
|
+
// One-shot handlers for this attempt
|
|
368
|
+
this.onConnected = (): void => {
|
|
369
|
+
if (settled) return; // Ignore late callback
|
|
370
|
+
settled = true;
|
|
371
|
+
clearTimeout(timeout);
|
|
372
|
+
|
|
373
|
+
const connectionTime = Date.now() - connectionStartTime;
|
|
374
|
+
this.log('debug', 'Connection established successfully', {
|
|
375
|
+
connectionTimeMs: connectionTime,
|
|
376
|
+
url: this.config.url,
|
|
377
|
+
attempt
|
|
378
|
+
});
|
|
379
|
+
this.state = ClientState.CONNECTED;
|
|
380
|
+
|
|
381
|
+
// Call original handler
|
|
382
|
+
originalOnConnected();
|
|
383
|
+
resolve();
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
this.onError = (error): void => {
|
|
387
|
+
if (settled) return; // Ignore late callback
|
|
388
|
+
settled = true;
|
|
389
|
+
clearTimeout(timeout);
|
|
390
|
+
|
|
391
|
+
this.log('warn', 'Connection error', { error, attempt });
|
|
392
|
+
this.state = ClientState.FAILED;
|
|
393
|
+
|
|
394
|
+
// Don't call originalOnError - it expects ErrorResultV1, not WebSocket Event
|
|
395
|
+
// Connection errors are handled by throwing ConnectionError after retry exhaustion
|
|
396
|
+
reject(error);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Start the connection attempt
|
|
400
|
+
super.connect();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Success! Connection established
|
|
404
|
+
const successLogLevel = attempt === 1 ? 'debug' : 'info';
|
|
405
|
+
this.log(successLogLevel, `Connection successful on attempt ${attempt}`, {
|
|
406
|
+
totalAttempts: attempt
|
|
407
|
+
});
|
|
408
|
+
return; // Success - exit retry loop
|
|
409
|
+
|
|
410
|
+
} catch (error) {
|
|
411
|
+
lastError = error as Error;
|
|
412
|
+
|
|
413
|
+
if (attempt < maxAttempts) {
|
|
414
|
+
// Not the last attempt - wait before retry
|
|
415
|
+
// Use info for first 2 retries (attempts 2-3), warn for 3rd retry (attempt 4)
|
|
416
|
+
const logLevel = attempt < 3 ? 'info' : 'warn';
|
|
417
|
+
this.log(logLevel, `Connection attempt ${attempt} failed, retrying after ${delayMs}ms`, {
|
|
418
|
+
error: lastError.message,
|
|
419
|
+
nextAttempt: attempt + 1
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Reset state to allow retry (but DON'T clear connectionPromise - maintains concurrency guard)
|
|
423
|
+
this.state = ClientState.INITIAL;
|
|
424
|
+
|
|
425
|
+
// Wait before next attempt
|
|
426
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
427
|
+
} else {
|
|
428
|
+
// Last attempt failed - all retries exhausted
|
|
429
|
+
this.log('warn', `All ${maxAttempts} connection attempts failed`, {
|
|
430
|
+
error: lastError.message
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
337
435
|
|
|
338
|
-
|
|
339
|
-
|
|
436
|
+
// All retries exhausted - throw typed ConnectionError
|
|
437
|
+
throw new ConnectionError(
|
|
438
|
+
`Failed to establish connection after ${maxAttempts} attempts`,
|
|
439
|
+
maxAttempts,
|
|
440
|
+
this.config.url,
|
|
441
|
+
lastError
|
|
442
|
+
);
|
|
443
|
+
} finally {
|
|
444
|
+
// Restore original handlers
|
|
445
|
+
this.config.onConnected = originalOnConnected;
|
|
446
|
+
this.config.onError = originalOnError;
|
|
340
447
|
|
|
341
|
-
|
|
448
|
+
// Clear connectionPromise only after entire retry sequence completes (success or failure)
|
|
449
|
+
this.connectionPromise = undefined;
|
|
450
|
+
}
|
|
342
451
|
}
|
|
343
452
|
|
|
344
453
|
override sendAudio(audioData: ArrayBuffer | ArrayBufferView | Blob): void {
|
|
@@ -437,6 +546,10 @@ export class RealTimeTwoWayWebSocketRecognitionClient
|
|
|
437
546
|
return this.config.audioUtteranceId;
|
|
438
547
|
}
|
|
439
548
|
|
|
549
|
+
getUrl(): string {
|
|
550
|
+
return this.config.url;
|
|
551
|
+
}
|
|
552
|
+
|
|
440
553
|
getState(): ClientState {
|
|
441
554
|
return this.state;
|
|
442
555
|
}
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
MetadataResultV1,
|
|
12
12
|
ErrorResultV1,
|
|
13
13
|
ASRRequestConfig,
|
|
14
|
-
GameContextV1
|
|
14
|
+
GameContextV1,
|
|
15
|
+
Stage
|
|
15
16
|
} from '@recog/shared-types';
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -57,17 +58,36 @@ export type IRecognitionCallbackUrl = RecognitionCallbackUrl;
|
|
|
57
58
|
|
|
58
59
|
export interface IRecognitionClientConfig {
|
|
59
60
|
/**
|
|
60
|
-
* WebSocket endpoint URL (optional
|
|
61
|
+
* WebSocket endpoint URL (optional)
|
|
62
|
+
* Either `url` or `stage` must be provided.
|
|
63
|
+
* If both are provided, `url` takes precedence.
|
|
61
64
|
*
|
|
62
|
-
*
|
|
65
|
+
* Example with explicit URL:
|
|
63
66
|
* ```typescript
|
|
64
|
-
*
|
|
65
|
-
* const base = getRecognitionServiceBase('staging'); // or 'dev', 'production'
|
|
66
|
-
* const url = `${base.wsBase}/ws/v1/recognize`;
|
|
67
|
+
* { url: 'wss://custom-endpoint.example.com/ws/v1/recognize' }
|
|
67
68
|
* ```
|
|
68
69
|
*/
|
|
69
70
|
url?: string;
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Stage for recognition service (recommended)
|
|
74
|
+
* Either `url` or `stage` must be provided.
|
|
75
|
+
* If both are provided, `url` takes precedence.
|
|
76
|
+
* Defaults to production if neither is provided.
|
|
77
|
+
*
|
|
78
|
+
* Example with STAGES enum (recommended):
|
|
79
|
+
* ```typescript
|
|
80
|
+
* import { STAGES } from '@recog/shared-types';
|
|
81
|
+
* { stage: STAGES.STAGING }
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* String values also accepted:
|
|
85
|
+
* ```typescript
|
|
86
|
+
* { stage: 'staging' } // STAGES.LOCAL | STAGES.DEV | STAGES.STAGING | STAGES.PRODUCTION
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
stage?: Stage | string;
|
|
90
|
+
|
|
71
91
|
/** ASR configuration (provider, model, language, etc.) - optional */
|
|
72
92
|
asrRequestConfig?: ASRRequestConfig;
|
|
73
93
|
|
|
@@ -137,6 +157,31 @@ export interface IRecognitionClientConfig {
|
|
|
137
157
|
/** Expected chunks per second for ring buffer sizing (default: 100) */
|
|
138
158
|
chunksPerSecond?: number;
|
|
139
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Connection retry configuration (optional)
|
|
162
|
+
* Only applies to initial connection establishment, not mid-stream interruptions.
|
|
163
|
+
*
|
|
164
|
+
* Default: { maxAttempts: 4, delayMs: 200 } (try once, retry 3 times = 4 total attempts)
|
|
165
|
+
*
|
|
166
|
+
* Timing: Attempt 1 → FAIL → wait 200ms → Attempt 2 → FAIL → wait 200ms → Attempt 3 → FAIL → wait 200ms → Attempt 4
|
|
167
|
+
*
|
|
168
|
+
* Example:
|
|
169
|
+
* ```typescript
|
|
170
|
+
* {
|
|
171
|
+
* connectionRetry: {
|
|
172
|
+
* maxAttempts: 2, // Try connecting up to 2 times (1 retry)
|
|
173
|
+
* delayMs: 500 // Wait 500ms between attempts
|
|
174
|
+
* }
|
|
175
|
+
* }
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
connectionRetry?: {
|
|
179
|
+
/** Maximum number of connection attempts (default: 4, min: 1, max: 5) */
|
|
180
|
+
maxAttempts?: number;
|
|
181
|
+
/** Delay in milliseconds between retry attempts (default: 200ms) */
|
|
182
|
+
delayMs?: number;
|
|
183
|
+
};
|
|
184
|
+
|
|
140
185
|
/**
|
|
141
186
|
* Optional logger function for debugging
|
|
142
187
|
* If not provided, no logging will occur
|
|
@@ -223,6 +268,13 @@ export interface IRecognitionClient {
|
|
|
223
268
|
* @returns Statistics about audio transmission and buffering
|
|
224
269
|
*/
|
|
225
270
|
getStats(): IRecognitionClientStats;
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get the WebSocket URL being used by this client
|
|
274
|
+
* Available immediately after client construction.
|
|
275
|
+
* @returns WebSocket URL string
|
|
276
|
+
*/
|
|
277
|
+
getUrl(): string;
|
|
226
278
|
}
|
|
227
279
|
|
|
228
280
|
/**
|
|
@@ -106,6 +106,11 @@ export interface ISimplifiedVGFRecognitionClient {
|
|
|
106
106
|
*/
|
|
107
107
|
getAudioUtteranceId(): string;
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Get the WebSocket URL being used
|
|
111
|
+
*/
|
|
112
|
+
getUrl(): string;
|
|
113
|
+
|
|
109
114
|
/**
|
|
110
115
|
* Get the underlying client state (for advanced usage)
|
|
111
116
|
*/
|
|
@@ -254,6 +259,10 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
|
|
|
254
259
|
return this.client.getAudioUtteranceId();
|
|
255
260
|
}
|
|
256
261
|
|
|
262
|
+
getUrl(): string {
|
|
263
|
+
return this.client.getUrl();
|
|
264
|
+
}
|
|
265
|
+
|
|
257
266
|
getState(): ClientState {
|
|
258
267
|
return this.client.getState();
|
|
259
268
|
}
|