@tanstack/ai-grok 0.8.4 → 0.8.6

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.
@@ -7,7 +7,6 @@ import type {
7
7
  RealtimeMessage,
8
8
  RealtimeMode,
9
9
  RealtimeSessionConfig,
10
- RealtimeStatus,
11
10
  RealtimeToken,
12
11
  } from '@tanstack/ai'
13
12
  import type { InternalLogger } from '@tanstack/ai/adapter-internals'
@@ -129,7 +128,11 @@ async function createWebRTCConnection(
129
128
 
130
129
  let audioElement: HTMLAudioElement | null = null
131
130
 
132
- let dataChannel: RTCDataChannel | null = null
131
+ // Captured into a const so closures see a non-nullable reference (teardown
132
+ // re-points the outer `dataChannel` to null, but in-flight closures still
133
+ // need to close their own channel).
134
+ const channel = pc.createDataChannel('oai-events')
135
+ let dataChannel: RTCDataChannel | null = channel
133
136
 
134
137
  let currentMode: RealtimeMode = 'idle'
135
138
  let currentMessageId: string | null = null
@@ -171,8 +174,6 @@ async function createWebRTCConnection(
171
174
  }
172
175
  }
173
176
 
174
- dataChannel = pc.createDataChannel('oai-events')
175
-
176
177
  let dataChannelOpened = false
177
178
  let rejectDataChannelReady: ((reason: unknown) => void) | null = null
178
179
  let dataChannelReadyTimeout: ReturnType<typeof setTimeout> | null = null
@@ -198,7 +199,7 @@ async function createWebRTCConnection(
198
199
  }
199
200
  }, 15000)
200
201
 
201
- dataChannel!.onopen = () => {
202
+ channel.onopen = () => {
202
203
  dataChannelOpened = true
203
204
  if (dataChannelReadyTimeout !== null) {
204
205
  clearTimeout(dataChannelReadyTimeout)
@@ -208,12 +209,12 @@ async function createWebRTCConnection(
208
209
  // don't attempt a redundant reject on an already-settled promise.
209
210
  rejectDataChannelReady = null
210
211
  flushPendingEvents()
211
- emit('status_change', { status: 'connected' as RealtimeStatus })
212
+ emit('status_change', { status: 'connected' })
212
213
  resolve()
213
214
  }
214
215
  })
215
216
 
216
- dataChannel.onmessage = (event) => {
217
+ channel.onmessage = (event) => {
217
218
  try {
218
219
  const message = JSON.parse(event.data)
219
220
  const messageRecord: Record<string, unknown> =
@@ -235,7 +236,7 @@ async function createWebRTCConnection(
235
236
  }
236
237
  }
237
238
 
238
- dataChannel.onerror = (error) => {
239
+ channel.onerror = (error) => {
239
240
  // Closing the peer connection cascades into `onerror`/`onclose` on the
240
241
  // data channel. Once teardown has started, re-surfacing those as
241
242
  // `emit('error')` is noise that confuses consumers (they just called
@@ -250,6 +251,7 @@ async function createWebRTCConnection(
250
251
  // doesn't end up as "[object Event]".
251
252
  // `onerror` always fires with an Event (often an RTCErrorEvent), so we
252
253
  // can read it via the untyped helpers without first proving object-ness.
254
+ // eslint-disable-next-line no-restricted-syntax -- RTCErrorEvent is a typed DOM class that does not structurally overlap Record<string, unknown>; we duck-type it via readObject/readString
253
255
  const errorRecord = error as unknown as Record<string, unknown>
254
256
  const rtcError = readObject(errorRecord, 'error')
255
257
  const msg =
@@ -261,7 +263,7 @@ async function createWebRTCConnection(
261
263
  emit('error', { error: dcErr })
262
264
  }
263
265
 
264
- dataChannel.onclose = () => {
266
+ channel.onclose = () => {
265
267
  // Same rationale as `onerror` above: `pc.close()` during teardown
266
268
  // cascades to the data channel's `onclose`. If we've already started
267
269
  // teardown, there's nothing to do here.
@@ -294,10 +296,7 @@ async function createWebRTCConnection(
294
296
  // guard listeners would see two `idle` events per disconnect.
295
297
  if (!isTornDown) {
296
298
  emit('status_change', {
297
- status:
298
- state === 'failed'
299
- ? ('error' as RealtimeStatus)
300
- : ('idle' as RealtimeStatus),
299
+ status: state === 'failed' ? 'error' : 'idle',
301
300
  })
302
301
  }
303
302
  if (!dataChannelOpened) {
@@ -730,7 +729,9 @@ async function createWebRTCConnection(
730
729
  currentMode = 'listening'
731
730
  emit('mode_change', { mode: 'listening' })
732
731
  }
733
- emit('interrupted', { messageId: currentMessageId ?? undefined })
732
+ emit('interrupted', {
733
+ ...(currentMessageId !== null && { messageId: currentMessageId }),
734
+ })
734
735
  break
735
736
 
736
737
  case 'error': {
@@ -759,10 +760,12 @@ async function createWebRTCConnection(
759
760
  break
760
761
  }
761
762
 
763
+ case undefined:
762
764
  default:
763
765
  // The xAI realtime protocol is a moving target; log unhandled event
764
766
  // types at provider level so they're visible during debugging without
765
- // emitting a user-visible error.
767
+ // emitting a user-visible error. `undefined` shares the bucket because
768
+ // a malformed event without a `type` field is just as unhandleable.
766
769
  logger.provider('grok.realtime unhandled server event', {
767
770
  type: event.type,
768
771
  })
@@ -928,7 +931,7 @@ async function createWebRTCConnection(
928
931
  `provider=grok direction=out type=${readString(event, 'type') ?? '<unknown>'}`,
929
932
  { frame: event },
930
933
  )
931
- dataChannel!.send(JSON.stringify(event))
934
+ channel.send(JSON.stringify(event))
932
935
  }
933
936
  pendingEvents.length = 0
934
937
  } catch (error) {
@@ -954,7 +957,7 @@ async function createWebRTCConnection(
954
957
  // every cleanup site stays in sync (input analyser, output analyser,
955
958
  // output source, audio element, etc.).
956
959
  await teardownConnection()
957
- emit('status_change', { status: 'idle' as RealtimeStatus })
960
+ emit('status_change', { status: 'idle' })
958
961
  },
959
962
 
960
963
  async startAudioCapture() {
@@ -1117,17 +1120,21 @@ async function createWebRTCConnection(
1117
1120
  sendEvent({ type: 'response.cancel' })
1118
1121
  currentMode = 'listening'
1119
1122
  emit('mode_change', { mode: 'listening' })
1120
- emit('interrupted', { messageId: currentMessageId ?? undefined })
1123
+ emit('interrupted', {
1124
+ ...(currentMessageId !== null && { messageId: currentMessageId }),
1125
+ })
1121
1126
  },
1122
1127
 
1123
1128
  on<TEvent extends RealtimeEvent>(
1124
1129
  event: TEvent,
1125
1130
  handler: RealtimeEventHandler<TEvent>,
1126
1131
  ): () => void {
1127
- if (!eventHandlers.has(event)) {
1128
- eventHandlers.set(event, new Set())
1132
+ let handlers = eventHandlers.get(event)
1133
+ if (!handlers) {
1134
+ handlers = new Set()
1135
+ eventHandlers.set(event, handlers)
1129
1136
  }
1130
- eventHandlers.get(event)!.add(handler)
1137
+ handlers.add(handler)
1131
1138
 
1132
1139
  return () => {
1133
1140
  eventHandlers.get(event)?.delete(handler)
@@ -194,6 +194,7 @@ export function arrayBufferToBase64(buffer: ArrayBuffer): string {
194
194
  const end = Math.min(i + chunkSize, bytes.length)
195
195
  binary += String.fromCharCode.apply(
196
196
  null,
197
+ // eslint-disable-next-line no-restricted-syntax -- TS lib types String.fromCharCode.apply as Array<number> but runtime accepts any ArrayLike
197
198
  bytes.subarray(i, end) as unknown as Array<number>,
198
199
  )
199
200
  }