Skip to content

Commit 63655aa

Browse files
authored
fixes #188: make SSE connection deferred to first useTock call (#189)
* fixes #188: make SSE connection deferred to first useTock call * update jsdoc * fix bad hook execution order * clean up useEffect * rename UseTock internal interface * add missing cleanup of SSE retry
1 parent 15d19f6 commit 63655aa

File tree

2 files changed

+59
-33
lines changed

2 files changed

+59
-33
lines changed

src/network/TockEventSource.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export class TockEventSource {
4242
private initialized: boolean;
4343
private eventSource: EventSource | null;
4444
private retryDelay: number;
45+
private retryTimeoutId: number;
4546
onResponse: (botResponse: BotConnectorResponse) => void;
4647
onStateChange: (state: number) => void;
4748

@@ -93,7 +94,7 @@ export class TockEventSource {
9394
MAX_RETRY_DELAY,
9495
retryDelay + RETRY_DELAY_INCREMENT,
9596
);
96-
setTimeout(async () => {
97+
this.retryTimeoutId = window.setTimeout(async () => {
9798
switch (await getSseStatus(url)) {
9899
case SseStatus.UNSUPPORTED:
99100
reject();
@@ -110,6 +111,7 @@ export class TockEventSource {
110111
}
111112

112113
close() {
114+
window.clearTimeout(this.retryTimeoutId);
113115
this.eventSource?.close();
114116
this.eventSource = null;
115117
this.initialized = false;

src/useTock.ts

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,20 @@ export interface UseTock {
7676
sseInitializing: boolean;
7777
}
7878

79+
/**
80+
* Internal extensions for {@link UseTock}
81+
*/
82+
interface UseTockInternal extends UseTock {
83+
/**
84+
* Hook that initializes the SSE connection
85+
*
86+
* If SSE is disabled by the settings, the hook simply resolves the sseInitPromise.
87+
*
88+
* This method is idempotent.
89+
*/
90+
useSseInit: () => void;
91+
}
92+
7993
function mapButton(button: BotConnectorButton): Button {
8094
if (button.type === 'postback') {
8195
return new PostBackButton(button.title, button.payload, button.imageUrl);
@@ -126,7 +140,7 @@ export const useTock0: (
126140
extraHeadersProvider?: () => Promise<Record<string, string>>,
127141
disableSse?: boolean,
128142
localStorageHistory?: TockLocalStorage,
129-
) => UseTock = (
143+
) => UseTockInternal = (
130144
tockEndPoint: string,
131145
{ locale, localStorage: localStorageSettings, network: networkSettings },
132146
extraHeadersProvider?: () => Promise<Record<string, string>>,
@@ -594,24 +608,26 @@ export const useTock0: (
594608
[],
595609
);
596610

597-
useEffect(() => {
598-
sseSource.current.onStateChange = onSseStateChange;
599-
sseSource.current.onResponse = handleSseBotResponse;
600-
}, [handleSseBotResponse, onSseStateChange]);
611+
const useSseInit = () => {
612+
useEffect(() => {
613+
sseSource.current.onStateChange = onSseStateChange;
614+
sseSource.current.onResponse = handleSseBotResponse;
615+
}, [handleSseBotResponse, onSseStateChange]);
601616

602-
useEffect(() => {
603-
if (disableSse || !tockEndPoint.length) {
604-
afterInit.current();
605-
} else {
606-
// Trigger afterInit regardless of whether the SSE call succeeded or failed
607-
// (it is valid for the backend to refuse SSE connections, but we still attempt to connect by default)
608-
sseSource.current
609-
.open(tockEndPoint, userId)
610-
.catch((e) => console.error(e))
611-
.finally(afterInit.current);
612-
}
613-
return () => sseSource.current.close();
614-
}, [disableSse, tockEndPoint]);
617+
useEffect(() => {
618+
if (disableSse || !tockEndPoint.length) {
619+
afterInit.current();
620+
} else {
621+
// Trigger afterInit regardless of whether the SSE call succeeded or failed
622+
// (it is valid for the backend to refuse SSE connections, but we still attempt to connect by default)
623+
sseSource.current
624+
.open(tockEndPoint, userId)
625+
.catch((e) => console.error(e))
626+
.finally(afterInit.current);
627+
}
628+
return () => sseSource.current.close();
629+
}, [disableSse, tockEndPoint]);
630+
};
615631

616632
const addHistory: (
617633
messageHistory: Array<Message>,
@@ -698,36 +714,44 @@ export const useTock0: (
698714
loadHistory,
699715
sseInitPromise: afterInitPromise.current,
700716
sseInitializing,
717+
useSseInit,
701718
};
702719
};
703720

704-
export const UseTockContext = createContext<UseTock | undefined>(undefined);
721+
export const UseTockContext = createContext<UseTockInternal | undefined>(
722+
undefined,
723+
);
705724

706725
export default (
707726
tockEndPoint?: string,
708727
extraHeadersProvider?: () => Promise<Record<string, string>>,
709728
disableSse?: boolean,
710729
localStorageHistory?: TockLocalStorage,
711-
) => {
730+
): UseTock => {
712731
const contextTock = useContext(UseTockContext);
713732
const settings = useTockSettings();
714-
if (contextTock != null) {
715-
return contextTock;
716-
}
733+
717734
if (settings.endpoint == null && tockEndPoint == null) {
718735
throw new Error('TOCK endpoint must be provided in TockContext');
719736
} else if (settings.endpoint == null) {
720737
console.warn(
721738
'Passing TOCK endpoint as argument to TockChat or useTock is deprecated; please set it in TockContext instead.',
722739
);
723740
}
724-
return contextTock
725-
? contextTock
726-
: useTock0(
727-
(tockEndPoint ?? settings.endpoint)!,
728-
settings,
729-
extraHeadersProvider,
730-
disableSse,
731-
localStorageHistory,
732-
);
741+
742+
// the following conditional does not follow the rules of hooks,
743+
// but in practice the endpoint setting should not appear or disappear in a live app
744+
const ret =
745+
contextTock ??
746+
useTock0(
747+
(tockEndPoint ?? settings.endpoint)!,
748+
settings,
749+
extraHeadersProvider,
750+
disableSse,
751+
localStorageHistory,
752+
);
753+
754+
ret.useSseInit();
755+
756+
return ret;
733757
};

0 commit comments

Comments
 (0)