@tanstack/ai-client 0.7.1 → 0.7.4

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 CHANGED
@@ -87,18 +87,18 @@ Available adapters: `openaiText`, `openaiEmbed`, `openaiSummarize`, `anthropicTe
87
87
  <td>
88
88
  <a href="https://www.coderabbit.ai/?via=tanstack&dub_id=aCcEEdAOqqutX6OS" >
89
89
  <picture>
90
- <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/coderabbit-dark-CMcuvjEy.svg" height="40" />
91
- <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/coderabbit-light-DVMJ2jHi.svg" height="40" />
92
- <img src="https://tanstack.com/assets/coderabbit-light-DVMJ2jHi.svg" height="40" alt="CodeRabbit" />
90
+ <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/coderabbit-dark-D643Zkrv.svg" />
91
+ <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/coderabbit-light-CIzGLYU_.svg" />
92
+ <img src="https://tanstack.com/assets/coderabbit-light-CIzGLYU_.svg" height="40" alt="CodeRabbit" />
93
93
  </picture>
94
94
  </a>
95
95
  </td>
96
96
  <td>
97
97
  <a href="https://www.cloudflare.com?utm_source=tanstack">
98
98
  <picture>
99
- <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/cloudflare-white-DQDB7UaL.svg" height="60" />
100
- <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/cloudflare-black-CPufaW0B.svg" height="60" />
101
- <img src="https://tanstack.com/assets/cloudflare-black-CPufaW0B.svg" height="60" alt="Cloudflare" />
99
+ <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/cloudflare-white-Co-Tyjbl.svg" />
100
+ <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/cloudflare-black-6Ojsn8yh.svg" />
101
+ <img src="https://tanstack.com/assets/cloudflare-white-Co-Tyjbl.svg" height="60" alt="Cloudflare" />
102
102
  </picture>
103
103
  </a>
104
104
  </td>
@@ -1,6 +1,6 @@
1
1
  import { AnyClientTool, ModelMessage, StreamChunk } from '@tanstack/ai';
2
2
  import { ConnectionAdapter } from './connection-adapters.js';
3
- import { ChatClientOptions, ChatClientState, MultimodalContent, UIMessage } from './types.js';
3
+ import { ChatClientOptions, ChatClientState, ConnectionStatus, MultimodalContent, UIMessage } from './types.js';
4
4
  export declare class ChatClient {
5
5
  private processor;
6
6
  private connection;
@@ -8,8 +8,10 @@ export declare class ChatClient {
8
8
  private body;
9
9
  private pendingMessageBody;
10
10
  private isLoading;
11
+ private isSubscribed;
11
12
  private error;
12
13
  private status;
14
+ private connectionStatus;
13
15
  private abortController;
14
16
  private events;
15
17
  private clientToolsRef;
@@ -18,17 +20,44 @@ export declare class ChatClient {
18
20
  private postStreamActions;
19
21
  private pendingToolExecutions;
20
22
  private continuationPending;
23
+ private subscriptionAbortController;
24
+ private processingResolve;
25
+ private errorReportedGeneration;
26
+ private streamGeneration;
21
27
  private continuationSkipped;
28
+ private sessionGenerating;
29
+ private activeRunIds;
22
30
  private callbacksRef;
23
31
  constructor(options: ChatClientOptions);
24
32
  private generateUniqueId;
25
33
  private setIsLoading;
26
34
  private setStatus;
35
+ private setIsSubscribed;
36
+ private setConnectionStatus;
37
+ private setSessionGenerating;
38
+ private resetSessionGenerating;
27
39
  private setError;
40
+ private abortSubscriptionLoop;
41
+ private resolveProcessing;
42
+ private cancelInFlightStream;
43
+ private reportStreamError;
28
44
  /**
29
- * Process a stream through the StreamProcessor
45
+ * Start the background subscription loop.
30
46
  */
31
- private processStream;
47
+ private startSubscription;
48
+ /**
49
+ * Consume chunks from the connection subscription.
50
+ */
51
+ private consumeSubscription;
52
+ /**
53
+ * Ensure subscription loop is running, starting it if needed.
54
+ */
55
+ private ensureSubscription;
56
+ /**
57
+ * Create a promise that resolves when onStreamEnd fires.
58
+ * Used by streamResponse to await processing completion.
59
+ */
60
+ private waitForProcessing;
32
61
  /**
33
62
  * Send a message and stream the response.
34
63
  * Supports both simple string content and multimodal content (images, audio, video, documents).
@@ -83,6 +112,18 @@ export declare class ChatClient {
83
112
  * Returns true if the stream completed successfully, false on abort or error.
84
113
  */
85
114
  private streamResponse;
115
+ /**
116
+ * Start the client subscription loop.
117
+ * This controls the connection lifecycle independently from request lifecycle.
118
+ */
119
+ subscribe(options?: {
120
+ restart?: boolean;
121
+ }): void;
122
+ /**
123
+ * Unsubscribe and fully tear down live behavior.
124
+ * This aborts an in-flight request and the subscription loop.
125
+ */
126
+ unsubscribe(): void;
86
127
  /**
87
128
  * Reload the last assistant message
88
129
  */
@@ -140,6 +181,21 @@ export declare class ChatClient {
140
181
  * Get current status
141
182
  */
142
183
  getStatus(): ChatClientState;
184
+ /**
185
+ * Get whether the subscription loop is active
186
+ */
187
+ getIsSubscribed(): boolean;
188
+ /**
189
+ * Get current connection lifecycle status
190
+ */
191
+ getConnectionStatus(): ConnectionStatus;
192
+ /**
193
+ * Whether the shared session is actively generating.
194
+ * Derived from stream run events (RUN_STARTED / RUN_FINISHED / RUN_ERROR).
195
+ * Unlike `isLoading` (request-local), this reflects shared generation
196
+ * activity visible to all subscribers (e.g. across tabs/devices).
197
+ */
198
+ getSessionGenerating(): boolean;
143
199
  /**
144
200
  * Get current error
145
201
  */
@@ -159,6 +215,9 @@ export declare class ChatClient {
159
215
  onChunk?: (chunk: StreamChunk) => void;
160
216
  onFinish?: (message: UIMessage) => void;
161
217
  onError?: (error: Error) => void;
218
+ onSubscriptionChange?: (isSubscribed: boolean) => void;
219
+ onConnectionStatusChange?: (status: ConnectionStatus) => void;
220
+ onSessionGeneratingChange?: (isGenerating: boolean) => void;
162
221
  onCustomEvent?: (eventType: string, data: unknown, context: {
163
222
  toolCallId?: string;
164
223
  }) => void;
@@ -1,22 +1,31 @@
1
1
  import { StreamProcessor, normalizeToUIMessage, generateMessageId } from "@tanstack/ai";
2
2
  import { DefaultChatClientEventEmitter } from "./events.js";
3
+ import { normalizeConnectionAdapter } from "./connection-adapters.js";
3
4
  class ChatClient {
4
5
  constructor(options) {
5
6
  this.body = {};
6
7
  this.pendingMessageBody = void 0;
7
8
  this.isLoading = false;
9
+ this.isSubscribed = false;
8
10
  this.error = void 0;
9
11
  this.status = "ready";
12
+ this.connectionStatus = "disconnected";
10
13
  this.abortController = null;
11
14
  this.currentStreamId = null;
12
15
  this.currentMessageId = null;
13
16
  this.postStreamActions = [];
14
17
  this.pendingToolExecutions = /* @__PURE__ */ new Map();
15
18
  this.continuationPending = false;
19
+ this.subscriptionAbortController = null;
20
+ this.processingResolve = null;
21
+ this.errorReportedGeneration = null;
22
+ this.streamGeneration = 0;
16
23
  this.continuationSkipped = false;
24
+ this.sessionGenerating = false;
25
+ this.activeRunIds = /* @__PURE__ */ new Set();
17
26
  this.uniqueId = options.id || this.generateUniqueId("chat");
18
27
  this.body = options.body || {};
19
- this.connection = options.connection;
28
+ this.connection = normalizeConnectionAdapter(options.connection);
20
29
  this.events = new DefaultChatClientEventEmitter(this.uniqueId);
21
30
  this.clientToolsRef = { current: /* @__PURE__ */ new Map() };
22
31
  if (options.tools) {
@@ -42,6 +51,12 @@ class ChatClient {
42
51
  }),
43
52
  onStatusChange: options.onStatusChange || (() => {
44
53
  }),
54
+ onSubscriptionChange: options.onSubscriptionChange || (() => {
55
+ }),
56
+ onConnectionStatusChange: options.onConnectionStatusChange || (() => {
57
+ }),
58
+ onSessionGeneratingChange: options.onSessionGeneratingChange || (() => {
59
+ }),
45
60
  onCustomEvent: options.onCustomEvent || (() => {
46
61
  })
47
62
  }
@@ -55,15 +70,29 @@ class ChatClient {
55
70
  },
56
71
  onStreamStart: () => {
57
72
  this.setStatus("streaming");
73
+ const assistantMessageId = this.processor.getCurrentAssistantMessageId();
74
+ if (!assistantMessageId) {
75
+ return;
76
+ }
77
+ const messages = this.processor.getMessages();
78
+ const assistantMessage = messages.find(
79
+ (m) => m.id === assistantMessageId
80
+ );
81
+ if (assistantMessage) {
82
+ this.currentMessageId = assistantMessage.id;
83
+ this.events.messageAppended(
84
+ assistantMessage,
85
+ this.currentStreamId || void 0
86
+ );
87
+ }
58
88
  },
59
89
  onStreamEnd: (message) => {
60
90
  this.callbacksRef.current.onFinish(message);
61
91
  this.setStatus("ready");
92
+ this.resolveProcessing();
62
93
  },
63
94
  onError: (error) => {
64
- this.setError(error);
65
- this.setStatus("error");
66
- this.callbacksRef.current.onError(error);
95
+ this.reportStreamError(error);
67
96
  },
68
97
  onTextUpdate: (messageId, content) => {
69
98
  if (this.currentStreamId) {
@@ -156,47 +185,135 @@ class ChatClient {
156
185
  this.status = status;
157
186
  this.callbacksRef.current.onStatusChange(status);
158
187
  }
188
+ setIsSubscribed(isSubscribed) {
189
+ this.isSubscribed = isSubscribed;
190
+ this.callbacksRef.current.onSubscriptionChange(isSubscribed);
191
+ }
192
+ setConnectionStatus(status) {
193
+ this.connectionStatus = status;
194
+ this.callbacksRef.current.onConnectionStatusChange(status);
195
+ }
196
+ setSessionGenerating(isGenerating) {
197
+ if (this.sessionGenerating === isGenerating) return;
198
+ this.sessionGenerating = isGenerating;
199
+ this.callbacksRef.current.onSessionGeneratingChange(isGenerating);
200
+ }
201
+ resetSessionGenerating() {
202
+ this.activeRunIds.clear();
203
+ this.setSessionGenerating(false);
204
+ }
159
205
  setError(error) {
160
206
  this.error = error;
161
207
  this.callbacksRef.current.onErrorChange(error);
162
208
  this.events.errorChanged(error?.message || null);
163
209
  }
210
+ abortSubscriptionLoop() {
211
+ this.subscriptionAbortController?.abort();
212
+ this.subscriptionAbortController = null;
213
+ }
214
+ resolveProcessing() {
215
+ this.processingResolve?.();
216
+ this.processingResolve = null;
217
+ }
218
+ cancelInFlightStream(options) {
219
+ this.abortController?.abort();
220
+ this.abortController = null;
221
+ if (options?.abortSubscription) {
222
+ this.abortSubscriptionLoop();
223
+ }
224
+ this.resolveProcessing();
225
+ this.setIsLoading(false);
226
+ if (options?.setReadyStatus) {
227
+ this.setStatus("ready");
228
+ }
229
+ }
230
+ reportStreamError(error) {
231
+ const alreadyReported = this.errorReportedGeneration === this.streamGeneration;
232
+ this.setError(error);
233
+ if (this.isLoading || this.status === "submitted" || this.status === "streaming") {
234
+ this.setStatus("error");
235
+ }
236
+ if (!alreadyReported) {
237
+ this.errorReportedGeneration = this.streamGeneration;
238
+ this.callbacksRef.current.onError(error);
239
+ }
240
+ }
241
+ /**
242
+ * Start the background subscription loop.
243
+ */
244
+ startSubscription() {
245
+ this.subscriptionAbortController = new AbortController();
246
+ const signal = this.subscriptionAbortController.signal;
247
+ this.consumeSubscription(signal).catch((err) => {
248
+ if (err instanceof Error && err.name !== "AbortError") {
249
+ this.setConnectionStatus("error");
250
+ this.resetSessionGenerating();
251
+ this.setIsSubscribed(false);
252
+ this.reportStreamError(err);
253
+ }
254
+ this.resolveProcessing();
255
+ }).finally(() => {
256
+ if (this.subscriptionAbortController?.signal !== signal) {
257
+ return;
258
+ }
259
+ this.subscriptionAbortController = null;
260
+ if (!signal.aborted && this.isSubscribed) {
261
+ this.setIsSubscribed(false);
262
+ if (this.connectionStatus !== "error") {
263
+ this.setConnectionStatus("disconnected");
264
+ }
265
+ }
266
+ });
267
+ }
164
268
  /**
165
- * Process a stream through the StreamProcessor
269
+ * Consume chunks from the connection subscription.
166
270
  */
167
- async processStream(source) {
168
- this.currentStreamId = this.generateUniqueId("stream");
169
- this.processor.prepareAssistantMessage();
170
- for await (const chunk of source) {
271
+ async consumeSubscription(signal) {
272
+ const stream = this.connection.subscribe(signal);
273
+ for await (const chunk of stream) {
274
+ if (signal.aborted) break;
275
+ if (this.connectionStatus === "connecting") {
276
+ this.setConnectionStatus("connected");
277
+ }
171
278
  this.callbacksRef.current.onChunk(chunk);
172
279
  this.processor.processChunk(chunk);
173
- if (!this.currentMessageId) {
174
- const newMessageId = this.processor.getCurrentAssistantMessageId() ?? null;
175
- if (newMessageId) {
176
- this.currentMessageId = newMessageId;
177
- const assistantMessage = this.processor.getMessages().find((m) => m.id === newMessageId);
178
- if (assistantMessage) {
179
- this.events.messageAppended(
180
- assistantMessage,
181
- this.currentStreamId || void 0
182
- );
183
- }
280
+ if (chunk.type === "RUN_STARTED") {
281
+ this.activeRunIds.add(chunk.runId);
282
+ this.setSessionGenerating(true);
283
+ }
284
+ if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") {
285
+ if (chunk.runId) {
286
+ this.activeRunIds.delete(chunk.runId);
287
+ } else if (chunk.type === "RUN_ERROR") {
288
+ this.activeRunIds.clear();
184
289
  }
290
+ this.setSessionGenerating(this.activeRunIds.size > 0);
291
+ this.resolveProcessing();
185
292
  }
186
293
  await new Promise((resolve) => setTimeout(resolve, 0));
187
294
  }
188
- if (this.pendingToolExecutions.size > 0) {
189
- await Promise.all(this.pendingToolExecutions.values());
295
+ }
296
+ /**
297
+ * Ensure subscription loop is running, starting it if needed.
298
+ */
299
+ ensureSubscription() {
300
+ if (!this.isSubscribed) {
301
+ this.subscribe();
302
+ return;
190
303
  }
191
- this.processor.finalizeStream();
192
- const messageId = this.processor.getCurrentAssistantMessageId();
193
- this.currentStreamId = null;
194
- this.currentMessageId = null;
195
- if (messageId) {
196
- const messages = this.processor.getMessages();
197
- return messages.find((m) => m.id === messageId) || null;
304
+ if (!this.subscriptionAbortController || this.subscriptionAbortController.signal.aborted) {
305
+ this.subscribe({ restart: true });
198
306
  }
199
- return null;
307
+ }
308
+ /**
309
+ * Create a promise that resolves when onStreamEnd fires.
310
+ * Used by streamResponse to await processing completion.
311
+ */
312
+ waitForProcessing() {
313
+ this.resolveProcessing();
314
+ return new Promise((resolve) => {
315
+ this.processingResolve = resolve;
316
+ });
200
317
  }
201
318
  /**
202
319
  * Send a message and stream the response.
@@ -289,9 +406,11 @@ class ChatClient {
289
406
  if (this.isLoading) {
290
407
  return false;
291
408
  }
409
+ const generation = ++this.streamGeneration;
292
410
  this.setIsLoading(true);
293
411
  this.setStatus("submitted");
294
412
  this.setError(void 0);
413
+ this.errorReportedGeneration = null;
295
414
  this.abortController = new AbortController();
296
415
  this.pendingToolExecutions.clear();
297
416
  let streamCompletedSuccessfully = false;
@@ -304,41 +423,89 @@ class ChatClient {
304
423
  conversationId: this.uniqueId
305
424
  };
306
425
  this.pendingMessageBody = void 0;
307
- const stream = this.connection.connect(
426
+ this.currentStreamId = this.generateUniqueId("stream");
427
+ this.currentMessageId = null;
428
+ this.processor.prepareAssistantMessage();
429
+ this.ensureSubscription();
430
+ const processingComplete = this.waitForProcessing();
431
+ await this.connection.send(
308
432
  messages,
309
433
  mergedBody,
310
434
  this.abortController.signal
311
435
  );
312
- await this.processStream(stream);
436
+ await processingComplete;
437
+ if (generation !== this.streamGeneration) {
438
+ return false;
439
+ }
440
+ if (this.status === "error") {
441
+ return false;
442
+ }
443
+ if (this.pendingToolExecutions.size > 0) {
444
+ await Promise.all(this.pendingToolExecutions.values());
445
+ }
446
+ this.processor.finalizeStream();
313
447
  streamCompletedSuccessfully = true;
314
448
  } catch (err) {
315
449
  if (err instanceof Error) {
316
450
  if (err.name === "AbortError") {
317
451
  return false;
318
452
  }
319
- this.setError(err);
320
- this.setStatus("error");
321
- this.callbacksRef.current.onError(err);
453
+ if (generation === this.streamGeneration) {
454
+ this.reportStreamError(err);
455
+ }
322
456
  }
323
457
  } finally {
324
- this.abortController = null;
325
- this.setIsLoading(false);
326
- this.pendingMessageBody = void 0;
327
- await this.drainPostStreamActions();
328
- if (streamCompletedSuccessfully) {
329
- const messages = this.processor.getMessages();
330
- const lastPart = messages.at(-1)?.parts.at(-1);
331
- if (lastPart?.type === "tool-result" && this.shouldAutoSend()) {
332
- try {
333
- await this.checkForContinuation();
334
- } catch (error) {
335
- console.error("Failed to continue flow after tool result:", error);
458
+ if (generation === this.streamGeneration) {
459
+ this.currentStreamId = null;
460
+ this.currentMessageId = null;
461
+ this.abortController = null;
462
+ this.setIsLoading(false);
463
+ this.pendingMessageBody = void 0;
464
+ await this.drainPostStreamActions();
465
+ if (streamCompletedSuccessfully) {
466
+ const messages = this.processor.getMessages();
467
+ const lastPart = messages.at(-1)?.parts.at(-1);
468
+ if (lastPart?.type === "tool-result" && this.shouldAutoSend()) {
469
+ try {
470
+ await this.checkForContinuation();
471
+ } catch (error) {
472
+ console.error("Failed to continue flow after tool result:", error);
473
+ }
336
474
  }
337
475
  }
338
476
  }
339
477
  }
340
478
  return streamCompletedSuccessfully;
341
479
  }
480
+ /**
481
+ * Start the client subscription loop.
482
+ * This controls the connection lifecycle independently from request lifecycle.
483
+ */
484
+ subscribe(options) {
485
+ const restart = options?.restart === true;
486
+ if (this.isSubscribed && !restart) {
487
+ return;
488
+ }
489
+ if (this.isSubscribed && restart) {
490
+ this.abortSubscriptionLoop();
491
+ }
492
+ this.setIsSubscribed(true);
493
+ this.setConnectionStatus("connecting");
494
+ this.startSubscription();
495
+ }
496
+ /**
497
+ * Unsubscribe and fully tear down live behavior.
498
+ * This aborts an in-flight request and the subscription loop.
499
+ */
500
+ unsubscribe() {
501
+ this.cancelInFlightStream({
502
+ setReadyStatus: true,
503
+ abortSubscription: true
504
+ });
505
+ this.resetSessionGenerating();
506
+ this.setIsSubscribed(false);
507
+ this.setConnectionStatus("disconnected");
508
+ }
342
509
  /**
343
510
  * Reload the last assistant message
344
511
  */
@@ -349,6 +516,9 @@ class ChatClient {
349
516
  (m) => m.role === "user"
350
517
  );
351
518
  if (lastUserMessageIndex === -1) return;
519
+ if (this.isLoading) {
520
+ this.cancelInFlightStream();
521
+ }
352
522
  this.events.reloaded(lastUserMessageIndex);
353
523
  this.processor.removeMessagesAfter(lastUserMessageIndex);
354
524
  await this.streamResponse();
@@ -357,12 +527,7 @@ class ChatClient {
357
527
  * Stop the current stream
358
528
  */
359
529
  stop() {
360
- if (this.abortController) {
361
- this.abortController.abort();
362
- this.abortController = null;
363
- }
364
- this.setIsLoading(false);
365
- this.setStatus("ready");
530
+ this.cancelInFlightStream({ setReadyStatus: true });
366
531
  this.events.stopped();
367
532
  }
368
533
  /**
@@ -485,6 +650,27 @@ class ChatClient {
485
650
  getStatus() {
486
651
  return this.status;
487
652
  }
653
+ /**
654
+ * Get whether the subscription loop is active
655
+ */
656
+ getIsSubscribed() {
657
+ return this.isSubscribed;
658
+ }
659
+ /**
660
+ * Get current connection lifecycle status
661
+ */
662
+ getConnectionStatus() {
663
+ return this.connectionStatus;
664
+ }
665
+ /**
666
+ * Whether the shared session is actively generating.
667
+ * Derived from stream run events (RUN_STARTED / RUN_FINISHED / RUN_ERROR).
668
+ * Unlike `isLoading` (request-local), this reflects shared generation
669
+ * activity visible to all subscribers (e.g. across tabs/devices).
670
+ */
671
+ getSessionGenerating() {
672
+ return this.sessionGenerating;
673
+ }
488
674
  /**
489
675
  * Get current error
490
676
  */
@@ -502,7 +688,22 @@ class ChatClient {
502
688
  */
503
689
  updateOptions(options) {
504
690
  if (options.connection !== void 0) {
505
- this.connection = options.connection;
691
+ const wasSubscribed = this.isSubscribed;
692
+ if (this.isLoading) {
693
+ this.cancelInFlightStream({
694
+ setReadyStatus: true,
695
+ abortSubscription: true
696
+ });
697
+ } else if (wasSubscribed) {
698
+ this.abortSubscriptionLoop();
699
+ }
700
+ this.resetSessionGenerating();
701
+ this.setIsSubscribed(false);
702
+ this.setConnectionStatus("disconnected");
703
+ this.connection = normalizeConnectionAdapter(options.connection);
704
+ if (wasSubscribed) {
705
+ this.subscribe();
706
+ }
506
707
  }
507
708
  if (options.body !== void 0) {
508
709
  this.body = options.body;
@@ -525,6 +726,15 @@ class ChatClient {
525
726
  if (options.onError !== void 0) {
526
727
  this.callbacksRef.current.onError = options.onError;
527
728
  }
729
+ if (options.onSubscriptionChange !== void 0) {
730
+ this.callbacksRef.current.onSubscriptionChange = options.onSubscriptionChange;
731
+ }
732
+ if (options.onConnectionStatusChange !== void 0) {
733
+ this.callbacksRef.current.onConnectionStatusChange = options.onConnectionStatusChange;
734
+ }
735
+ if (options.onSessionGeneratingChange !== void 0) {
736
+ this.callbacksRef.current.onSessionGeneratingChange = options.onSessionGeneratingChange;
737
+ }
528
738
  if (options.onCustomEvent !== void 0) {
529
739
  this.callbacksRef.current.onCustomEvent = options.onCustomEvent;
530
740
  }