Lo admito, probablemente ni siquiera sea una pregunta específica de SignalR, sino más bien una pregunta de "hacerlo de la manera correcta". Por lo general, en C#, podríamos crear un singleton de algo compartido haciéndolo estático y luego usando un lock a su alrededor para que solo un subproceso pueda crear el objeto compartido. Usando el cliente Javascript SignalR, me gustaría hacer lo mismo, sin tener que preocuparme por si dos componentes quieren usar una conexión o no. Mi solución es esta, pero aún da como resultado una condición de carrera en la que dos componentes web obtienen cada uno su propia instancia de conexión:
export class MessagingService {
private static service: MessagingService;
static async GetService(): Promise<MessagingService> {
if (!this.service) {
let service = new MessagingService();
await service.start();
this.service = service;
}
return this.service;
}
connection;
async start() {
this.connection = new signalR.HubConnectionBuilder().withUrl("/MyHub").withAutomaticReconnect().build();
await this.connection.start();
}
}
Ingenuamente, no esperaría que dos componentes web en la página llamaran await MessagingService.GetService() desde sus métodos async connectedCallback() , para que cada uno termine con su propia instancia de MessagingService y, por lo tanto, conexión, pero eso es exactamente lo que sucede.
Innumerables otras preguntas en Internet sugieren que el bloqueo no es lo que necesito aquí, sino que lo estoy haciendo mal. ¿Cómo puedo asegurarme de que solo se crea una de estas conexiones?
Mi cerebro C# no estaba pensando en promesas. Estaba fingiendo un bloqueo en el miembro de la instancia estática, que no se asignó hasta que se creó e inició la conexión SignalR, por lo que no sorprende que diferentes componentes llegaran al mismo tiempo y no encontraran una instancia lista. Si bien pude reordenar el código para asignarlo de inmediato, los segundos en llegar a la llamada descubrieron que la conexión estaba allí, pero no había terminado de abrirse, por lo que falló la invoke de llamadas en su contra.
El truco consistía en verificar primero la promesa y luego esperar a que se completara. El código resultante fue este:
export class MessagingService {
private static service: MessagingService;
private static promise: Promise<void>;
static async GetService(): Promise<MessagingService> {
if (!this.promise) {
const service = new MessagingService();
this.promise = service.start();
this.service = service;
}
await Promise.all([this.promise]);
return this.service;
}
connection: any;
private async start() {
this.connection = new signalR.HubConnectionBuilder().withUrl("/MyHub").withAutomaticReconnect().build();
await this.connection.start();
}
}
La promesa estática se asignó al método start() , que es esencialmente la parte que inicia la conexión. Antes de devolver la instancia de servicio, await Promise.all([this.promise]) para asegurarme de que esté listo. Esto parece resolver la condición de carrera, pero no estoy convencido de que sea posible que dos personas que llaman aterricen en la primera línea del método GetService() y aún así encuentren que no hay promesa.