javascript - Cum pot crea în siguranță o conexiune comună pentru clientul SignalR?


Trebuie să recunosc, probabil că aceasta nu este nici măcar o întrebare specifică SignalR, ci mai mult o întrebare de „a face lucrurile corect”. De obicei, în C#, am putea crea un singleton al unui obiect partajat făcându-l static și apoi folosind un lock în jurul lui, astfel încât doar un singur thread să poată crea obiectul partajat. Folosind clientul SignalR pentru JavaScript, aș vrea să fac același lucru, fără să trebuiască să mă preocup dacă două componente vor să folosească aceeași conexiune. Soluția mea este aceasta, dar încă rezultă într-o condiție de cursă în care două componente web își obțin fiecare propria instanță de conexiune:

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();
    }
}

Naiv, nu m-aș aștepta ca două componente web de pe pagină, care apelează await MessagingService.GetService() din metodele lor async connectedCallback(), să ajungă fiecare să aibă propria instanță a MessagingService și, prin urmare, propria conexiune, dar exact asta se întâmplă.

Nenumărate alte întrebări pe Internet sugerează că blocarea nu este ceea ce am nevoie aici, ci mai degrabă că o fac greșit. Cum pot să mă asigur că doar una dintre aceste conexiuni este creată vreodată?

Răspuns #1

Creierul meu C# nu gândea în termeni de promises. Simulam un blocaj pe membrul static al instanței, care nu era atribuit până când conexiunea SignalR nu era creată și pornită, așa că nu e surprinzător că diferite componente ajungeau acolo în același timp și nu găseau o instanță pregătită. Deși aș fi putut reordona codul pentru a o atribui imediat, cei care apelau după al doilea constatau că conexiunea era acolo, dar nu se deschisese complet, așadar apelurile invoke împotriva ei eșuau.

Trucul a fost să verifici mai întâi promisiunea, apoi să aștepți să fie finalizată. Codul rezultat a fost acesta:

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();
    }
}

Promisiunea statică a fost atribuită metodei start(), care este, în esență, partea care inițiază conexiunea. Înainte de a returna instanța serviciului, eu await Promise.all([this.promise]) pentru a mă asigura că s-a terminat. Se pare că acest lucru rezolvă condiția de cursă, dar nu sunt convins că este posibil ca doi apelanți să ajungă pe prima linie a metodei GetService() și totuși să descopere că nu există nicio promisiune.