Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions shesha-reactjs/src/providers/signalR/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import * as signalR from '@microsoft/signalr';

import React, { PropsWithChildren, useContext, useEffect, useReducer, useRef } from 'react';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
import { getFlagSetters } from '../utils/flagsSetters';
import {
ISignalRConnection,
SIGNAL_R_CONTEXT_INITIAL_STATE,
SignalRActionsContext,
SignalRStateContext,
} from './contexts';
import { signalRReducer } from './reducer';
import { setConnectionAction } from './actions';
import { useSheshaApplication } from '../sheshaApplication';

const DEFAULT_RECONNECT_INTERVALS = [0, 2000, 5000, 10000];

export interface ISignalRProvider {
hubUrl: string;
baseUrl?: string;
onConnected?: (connection: ISignalRConnection) => void;
onDisconnected?: () => void;
enableReconnect?: boolean;
reconnectIntervals?: number[]; // default: [0, 2000, 5000, 10000]
}

function SignalRProvider({

Check failure on line 26 in shesha-reactjs/src/providers/signalR/index.tsx

View workflow job for this annotation

GitHub Actions / build-attempt

Missing return type on function

Check failure on line 26 in shesha-reactjs/src/providers/signalR/index.tsx

View workflow job for this annotation

GitHub Actions / build-attempt

Missing return type on function
children,
baseUrl,
hubUrl,
onConnected,
onDisconnected,
enableReconnect,
reconnectIntervals,
}: PropsWithChildren<ISignalRProvider>) {
const [state, dispatch] = useReducer(signalRReducer, { ...SIGNAL_R_CONTEXT_INITIAL_STATE });

Check failure on line 35 in shesha-reactjs/src/providers/signalR/index.tsx

View workflow job for this annotation

GitHub Actions / build-attempt

Unsafe array destructuring of a tuple element with an error typed value
const { backendUrl } = useSheshaApplication();

const setConnection = (connection?: ISignalRConnection) => {

Check failure on line 38 in shesha-reactjs/src/providers/signalR/index.tsx

View workflow job for this annotation

GitHub Actions / build-attempt

Missing return type on function
dispatch(setConnectionAction(connection));

Check failure on line 39 in shesha-reactjs/src/providers/signalR/index.tsx

View workflow job for this annotation

GitHub Actions / build-attempt

Unsafe call of a type that could not be resolved
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// Keep the latest callbacks in refs so the SignalR event handlers always invoke the
// current callbacks without having to list them as effect deps (which would tear down
// and rebuild the connection whenever the parent passes new callback identities).
const onConnectedRef = useRef(onConnected);
const onDisconnectedRef = useRef(onDisconnected);

useEffect(() => {
onConnectedRef.current = onConnected;
onDisconnectedRef.current = onDisconnected;
});

// Depend on the reconnect interval *values*, not the array's identity, so passing a new
// array literal with the same values doesn't needlessly recreate the connection.
const reconnectIntervalsKey = (reconnectIntervals ?? DEFAULT_RECONNECT_INTERVALS).join(',');

useEffect(() => {
// Guards against a start() that resolves after this effect has been cleaned up,
// which would otherwise push an already-stopped connection back into state.
let isActive = true;

let builder = new signalR.HubConnectionBuilder().withUrl(`${baseUrl ?? backendUrl}${hubUrl}`);

if (enableReconnect) {
builder = builder.withAutomaticReconnect(reconnectIntervals ?? DEFAULT_RECONNECT_INTERVALS);
}

const connection: ISignalRConnection = builder.build();

if (enableReconnect) {
connection.onreconnecting((error) => {

Check failure on line 71 in shesha-reactjs/src/providers/signalR/index.tsx

View workflow job for this annotation

GitHub Actions / build-attempt

Unsafe member access .onreconnecting on a type that cannot be resolved

Check failure on line 71 in shesha-reactjs/src/providers/signalR/index.tsx

View workflow job for this annotation

GitHub Actions / build-attempt

Unsafe call of a type that could not be resolved
console.warn('SignalR reconnecting...', error);
});

connection.onreconnected(() => {

Check failure on line 75 in shesha-reactjs/src/providers/signalR/index.tsx

View workflow job for this annotation

GitHub Actions / build-attempt

Unsafe member access .onreconnected on a type that cannot be resolved

Check failure on line 75 in shesha-reactjs/src/providers/signalR/index.tsx

View workflow job for this annotation

GitHub Actions / build-attempt

Unsafe call of a type that could not be resolved
onConnectedRef.current?.(connection);
});
}

connection.onclose((error) => {

Check failure on line 80 in shesha-reactjs/src/providers/signalR/index.tsx

View workflow job for this annotation

GitHub Actions / build-attempt

Unsafe call of a type that could not be resolved
console.error('SignalR connection closed', error);
onDisconnectedRef.current?.();
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

connection
.start()
.then(() => {
// Only expose the connection once it has actually started successfully,
// and only if this effect instance is still active.
if (!isActive) return;
setConnection(connection);
onConnectedRef.current?.(connection);
})
.catch((err) => console.error('SignalR start failed:', err));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return () => {
isActive = false;
// No need to call onDisconnected here — stop() triggers onclose, which already
// invokes onDisconnectedRef.current. If the connection never started, neither
// fires, mirroring the fact that onConnected was never called either.
connection
?.stop()
?.catch((err) => console.error('SignalRProvider connection error', err));

setConnection();
};
}, [baseUrl, backendUrl, hubUrl, enableReconnect, reconnectIntervalsKey]);

/* NEW_ACTION_DECLARATION_GOES_HERE */

return (
<SignalRStateContext.Provider value={state}>
<SignalRActionsContext.Provider
value={{
...getFlagSetters(dispatch),
/* NEW_ACTION_GOES_HERE */
}}
>
{children}
</SignalRActionsContext.Provider>
</SignalRStateContext.Provider>
);
}

function useSignalRState(require: boolean) {
const context = useContext(SignalRStateContext);

if (context === undefined && require) {
throw new Error('useSignalRState must be used within a SignalRProvider');
}

return context;
}

function useSignalRActions(require: boolean) {
const context = useContext(SignalRActionsContext);

if (context === undefined && require) {
throw new Error('useSignalRActions must be used within a SignalRProvider');
}

return context;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

function useSignalR(require: boolean = true) {
const actionsContext = useSignalRActions(require);
const stateContext = useSignalRState(require);

// useContext() returns initial state when provider is missing
// initial context state is useless especially when require == true
// so we must return value only when both context are available
return actionsContext !== undefined && stateContext !== undefined
? { ...actionsContext, ...stateContext }
: undefined;
}

export default SignalRProvider;

export { SignalRProvider, useSignalR, useSignalRActions, useSignalRState };
Loading