Search code examples
javascriptangularsignalrweb-worker

Is it possible to create signalR streaming connection in web worker


I am using Angular 8 and singalR for image streaming from the server.

I noticed that streaming slow down my UI rendering. I want whole streaming part to move to Web Worker and only valuable content send from Web Worker to UI tread.

I do not have code example since I am not sure if my idea even possible.
Can anyone give me some tips, or post, since I do not find anything about web workers and web socket (signalR) connection in them.


Solution

  • After some researching I finally finished this. This is possible, and there is couple tips for it:

    1. https://angular.io/guide/web-worker Create web worker, ng generate webWorker , is command for that and Angular will set up enviroment for you. If you do that without this command, there is additional work to do. Please check documentation for it.

    2. Install @microsoft/signalr library, not @aspnet/signalr. I had problem to establish connection in web worker with @aspnet/signalr.

    3. Then from my component I called worker to establish connection:

    public establishConnectionToStreamingHub() {
      if (!this.streamingWorker) {
        this.streamingWorker = new Worker('./streaming-data.worker', { type: 'module' });
    
        this.streamingWorker.postMessage(<StreamingWorkerRequest>{
          action: StreamingWorkerAction.EstablishConnection,
          baseHref: this.href
        });
    
        this.streamingWorker.onmessage = this.onWorkerMessage;
      }
    }
    
    // listen for streaming response from worker. 
    // I created interface here for message type. 
    // If message is success I notified listener about it, if message is error handling should be added here.
    
    private onWorkerMessage = (message) => {
      const response = message.data as StreamingWorkerResponse;
      if (response.type === ResponseMessageType.StreamingSuccessResult) {
        this.streamingDataNotify(response.data);
      }
    }
    
    

    This is my streaming worker file. The main point here is that I have one class for connection and streaming. First time I get message to the worker I check message type if message is 'StreamingWorkerAction.EstablishConnection'. If it is I create new class instance and establish connection to the hub in class constructor. I send worker instace to the class also. After it for all streaming request class object is in charge of.

    import { StreamingWorkerAction,
      StreamingWorkerRequest,
      StreamingWorkerResponse,
      ResponseMessageType
    } from './streaming-worker.model';
    
    export class StreamingClass {
      private hubConnection: HubConnection;
    
      private workerInstance;
      private href = '';
    
      private streamingSubscription: ISubscription<Data>;
    
    
      constructor(data: StreamingWorkerRequest) {
        this.href = data.baseHref;
        this.workerInstance = data.workerInstance;
    
        this.connectHub().subscribe(result => {
          console.log('Streaming connection established.')
        }, error => {
          this.workerInstance.postMessage(<StreamingWorkerResponse>{
            type: ResponseMessageType.ConnectingError,
            error: error
          });
        });
    
        this.workerInstance.onmessage = (event) => {
          const requestData = event.data as StreamingWorkerRequest;
          if (requestData.action === StreamingWorkerAction.StartStreaming) {
            this.clearCurrentSubscription();
            this.streamingSubscription = this.startStreaming(streamingId);
          }
        };
      }
    
      private clearCurrentSubscription() {
        if (this.streamingSubscription) {
          this.streamingSubscription.dispose();
        }
      }
    
      private startStreaming(streamingId: string): ISubscription<StreamingData> {
        return this.hubConnection.stream<StreamingData>('Stream', streamingId).subscribe({
          next: data => {
            this.workerInstance.postMessage(<StreamingWorkerResponse>{
              type: ResponseMessageType.StreamingSuccessResult,
              data: data
            });
          },
          complete: () => {
            console.log('complete');
          },
          error: error => {
            this.workerInstance.postMessage(<StreamingWorkerResponse>{
              type: ResponseMessageType.StreamingError,
              error: error
            });
          }
        });
      }
    
      private connectHub(): Observable<boolean> {
        this.hubConnection = new HubConnectionBuilder()
           .withUrl(this.href + '/api/hub/pathToTheHub')
           .withAutomaticReconnect()
           .build();
      }
    
    }
    
    // web worker event listener
    addEventListener('message', event => {
      const requestData = event.data as StreamingWorkerRequest;
    
      if (requestData.action === StreamingWorkerAction.EstablishConnection) {
        requestData.workerInstance = self;
        const streamingClass = new StreamingClass(requestData);
      }
    });
    

    Update: 02-09-2020

    On npm site for @microsoft/signalr library: https://www.npmjs.com/package/@microsoft/signalr

    I noticed this:

    To use the client in a webworker, copy *.js files from the dist/webworker > >folder to your script folder include on your webworker using the importScripts >function. Note that webworker SignalR hub connection supports only absolute path >to a SignalR hub.

    After it i found webworker folder in my node modules: \node_modules@microsoft\signalr\dist\webworker

    and copied file signalr.js to assets folder on path: assets/scripts/signalr.js.

    Then in my worker file I imported script like this:

    import * as signalR from '../../../../assets/scripts/signalr.js';
    

    And in worker file replaced all old signalR import to new one from webworker script. For example those lines:

    private hubConnection: HubConnection;  
    private streamingSubscription: ISubscription<MetaModule>;
    
    this.hubConnection = new HubConnectionBuilder()
           .withUrl(this.href + '/api/hub/pathToTheHub')
           .withAutomaticReconnect()
           .build();
    

    are changed to:

    private hubConnection: signalR.HubConnection;
    private streamingSubscription: signalR.ISubscription<MetaModule>;
    
    this.hubConnection = new signalR.HubConnectionBuilder()
          .withUrl(this.href + '/api/hub/pathToTheHub')
          .withAutomaticReconnect()
          .build();
    

    After those updates our streaming process works significantly faster.