@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volley/recognition-client-sdk",
3
- "version": "0.1.211",
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": [
@@ -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
- * .url('ws://localhost:3101/ws/v1/recognize')
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: Check if we already have a connection promise (handles simultaneous calls)
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
- // RETRY HINT: Wrap this method with exponential backoff (e.g., 1s, 2s, 4s) on FAILED state
301
- // Ensure audioBuffer persists between retries - same audioUtteranceId = same audio session
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
- this.log('debug', 'Creating new connection to WebSocket', { url: this.config.url });
304
- this.state = ClientState.CONNECTING;
305
-
306
- const connectionStartTime = Date.now();
319
+ return this.connectionPromise;
320
+ }
307
321
 
308
- // Store the promise IMMEDIATELY so simultaneous calls will get the same promise
309
- this.connectionPromise = new Promise((resolve, reject) => {
310
- const timeout = setTimeout(() => {
311
- this.log('warn', 'Connection timeout', { timeout: 10000 });
312
- this.state = ClientState.FAILED;
313
- reject(new Error('Timeout'));
314
- }, 10000);
315
-
316
- const originalOnConnected = this.onConnected.bind(this);
317
- this.onConnected = (): void => {
318
- clearTimeout(timeout);
319
- const connectionTime = Date.now() - connectionStartTime;
320
- this.log('debug', 'Connection established successfully', {
321
- connectionTimeMs: connectionTime,
322
- url: this.config.url
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
- const originalOnError = this.onError.bind(this);
330
- this.onError = (error): void => {
331
- clearTimeout(timeout);
332
- this.log('warn', 'Connection error', error);
333
- this.state = ClientState.FAILED;
334
- originalOnError(error);
335
- reject(error);
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
- super.connect();
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
- return this.connectionPromise;
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 - defaults to production)
61
+ * WebSocket endpoint URL (optional)
62
+ * Either `url` or `stage` must be provided.
63
+ * If both are provided, `url` takes precedence.
61
64
  *
62
- * For different stages, use the helper function:
65
+ * Example with explicit URL:
63
66
  * ```typescript
64
- * import { getRecognitionServiceBase } from '@recog/client-sdk-ts';
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
  }