javascript - Hur skapar jag en delad SignalR-klientanslutning på ett säkert sätt?


Jag måste erkänna, det här är antagligen inte ens en SignalR-specifik fråga, utan mer en ”göra det på rätt sätt”-fråga. Vanligtvis i C# kan vi skapa en singleton av någon delad sak genom att göra den statisk och sedan använda en lock runt den så att endast en tråd kan skapa det delade objektet. Med Javascript SignalR-klienten vill jag göra samma sak, utan att behöva oroa mig för om två komponenter vill använda en anslutning. Min lösning är detta, men det resulterar fortfarande i en race condition där två webbkomponenter var och en får sin egen anslutningsinstans:

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

Naivt nog skulle jag inte förvänta mig att två webbkomponenter på sidan, som anropar await MessagingService.GetService() från sina async connectedCallback()-metoder, skulle få varsin instans av MessagingService och därmed varsin anslutning, men det är precis vad som händer.

Otroligt många andra frågor på internet tyder på att låsning inte är vad jag behöver här, utan snarare att jag gör det fel. Hur kan jag vara säker på att bara en av dessa anslutningar någonsin skapas?

Svar #1

Min C#-hjärna tänkte inte i löften. Jag fejkar ett lås på den statiska instansmedlemmen, som inte tilldelades förrän SignalR-anslutningen skapades och startades, så det är ingen överraskning att olika komponenter kom dit samtidigt och inte hittade en färdig instans. Även om jag kunde omordna koden för att tilldela den omedelbart, upptäckte de som kom efter till anropet att anslutningen fanns där, men att den inte var färdig att öppnas, så invoke-anrop mot den misslyckades.

Tricket var att först kontrollera löftet, och sedan vänta på att det skulle bli klart. Den resulterande koden blev denna:

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

Det statiska löftet tilldelades start()-metoden, som i princip är den del som startar anslutningen. Innan jag returnerar tjänsteinstansen, await Promise.all([this.promise]) för att säkerställa att det är klart. Detta verkar ta hand om tävlingsförhållandet, men jag är inte övertygad om att det är möjligt för två anropare att hamna på den första raden i GetService()-metoden och fortfarande upptäcka att det inte finns något löfte.