Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ng generate webWorker for libraries #15059

Open
klausj opened this issue Jul 12, 2019 · 29 comments
Open

Support ng generate webWorker for libraries #15059

klausj opened this issue Jul 12, 2019 · 29 comments
Labels
area: @schematics/angular feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature
Milestone

Comments

@klausj
Copy link

klausj commented Jul 12, 2019

🚀 Feature request

Command (mark with an x)

- [ ] new
- [ ] build
- [ ] serve
- [ ] test
- [ ] e2e
- [x] generate
- [ ] add
- [ ] update
- [ ] lint
- [ ] xi18n
- [ ] run
- [ ] config
- [ ] help
- [ ] version
- [ ] doc

Description

I would like to use Web Workers in an Angular 8 library project.

In version 8.1.1 this seems to be supported for project type 'application' only:

ng generate webWorker my-module/myWorker --project my-lib
Web Worker requires a project type of "application".

Describe the solution you'd like

Generating a Web Worker should work for project type 'library' as well.

Describe alternatives you've considered

Rewrite TypeScript worker code to JavaScript file.
@emps
Copy link

emps commented Dec 17, 2019

hey do any have any workaround ?

@SansDK
Copy link

SansDK commented Jan 31, 2020

Hey @klausj,

It's been a while since you opened this issue, do you have any kind of solution / workaround on how to use a web worker in an Angular library?

Thanks

@klausj
Copy link
Author

klausj commented Feb 11, 2020

Hi @SansDK

As a workaround I create an object URL which contains the code of a TypeScript method for the worker. The worker code is wrapped by a self executing function to be available to the worker.

// Helper class to build Worker Object URL
export class WorkerHelper {

 static buildWorkerBlobURL(workerFct: Function): string {

      // Update 2020-07-16: Try to mitigate XSS attacks
      // Should ensure a type check at runtime and reject an injected XSS string.
      if(! (workerFct instanceof Function)) {
         throw new Error(
            'Parameter workerFct is not a function! (XSS attack?).'
         )
      }
      let woFctNm = workerFct.name;
      let woFctStr = workerFct.toString();

      // Make sure code starts with "function()"
      // Chrome, Firefox: "[wofctNm](){...}", Safari: "function [wofctNm](){...}"
      // we need an anonymous function: "function() {...}"
      let piWoFctStr = woFctStr.replace(/^function +/, '');

      // Convert to anonymous function
      let anonWoFctStr = piWoFctStr.replace(woFctNm + '()', 'function()')
     
      // Self executing
      let ws = '(' + anonWoFctStr + ')();'
   
      // Build the worker blob
      let wb = new Blob([ws], {type: 'text/javascript'});

      let workerBlobUrl=window.URL.createObjectURL(wb);
      return workerBlobUrl;
    }
}

A TypeScript class which requires a worker looks like this:

export class ClassWithWorker{
 
 private workerURL: string;
 private worker: Worker | null;
 
  constructor() {
        this.worker = null;
        this.workerURL = WorkerHelper.buildWorkerBlobURL(this.workerFunction)
    }

    /*
     *  Method used as worker code.
     */
    workerFunction() {
       self.onmessage = function (msg) {
        let someInputData = msg.data.someInput;
        // Work on someInput
        ...
        // Post result
        postMessage({someOutput:someData });
       }
    }

    start(){
       this.worker = new Worker(this.workerURL);
       this.worker.onmessage = (me) => {
           let result = me.data.someOutput;
           ...
       }
       this.worker.postmessage({someInput: someInputData});
     }

    stop() {
      this.worker.terminate();
     }
}

The approach works but has some drawbacks:

  • As dgp1130 has demonstrated the solution could potentially be vulnerable to XSS attacks. I try to prevent this by a type query on the type function, but I cannot guarantee that this works for all runtimes and transpilers.

  • The string returned by Function.toString() seems not be well defined. It differs on different browsers and depends on the transpiler version used ( for example I had to change the code migrating from Angular 7 to 8).
    So my approach might break in future versions of browsers or build environments!

  • If the worker code requires other classes they must be put inside the workerFunction. Duplicate code might be necessary.

@SansDK
Copy link

SansDK commented Feb 11, 2020

Thanks for the explanation and the example, very useful!

@TheKrisSodroski
Copy link

Just gonna drop my 2 cents, but allowing libraries to encapsulate web worker functionality away from consuming applications would be an absolute game changer.

With this, a future where we'll only be chaining awaits vs handling call backs and subscriptions isn't far off. I suspect performance would skyrocket as library maintainers would be able to offload computations completely from the UI thread.

Code quality would increase in the ecosystem as a whole.

@dgp1130 dgp1130 added area: @schematics/angular freq1: low Only reported by a handful of users who observe it rarely needs: discussion On the agenda for team meeting to determine next steps triage #1 feature Issue that requests a new feature labels May 27, 2020
@ngbot ngbot bot modified the milestone: Backlog May 27, 2020
@dgp1130
Copy link
Collaborator

dgp1130 commented May 28, 2020

Talked about this a bit in amongst the team. Workers have a lot of use cases for libraries. The challenge here is that workers must be loaded from a URL, and a library typically can't assume anything about the URL layout of a particular application.

Fortunately this is pretty easy to work around. Libraries can just export a function that does what they want and applications can easily import and call this function from their own worker. For example:

// my-lib.ts

export function work() {
  console.log('Does work');
}

Then the application can define a worker file which calls this library:

// my-worker.worker.ts

import { work } from 'my-lib';
work();

The worker can be started like any other by the application:

// app.component.ts

const worker = new Worker('./my-worker.worker');

The work() function can add event listeners to provide a proper service or support whatever API is useful for the library. This adds a little more boilerplate to the application, but keeps URL layout limited to the application and out of the library.

We're curious to hear if there are other use cases which might not be well-supported by this kind of design. The only one I can think of would be a library which wants to manage its workers directly (like a ThreadPool in Java), but that could be worked around and I'm not aware of any web worker use cases which fit that kind of model. You can't run arbitrary functions in a worker, so the comparison to ThreadPool isn't entirely accurate here. If there are other use cases that don't work well here, please post them so we can re-evaluate if necessary.

The workaround previously mentioned is quite clever but not usable for production.

  • This introduces potential XSS security vulnerabilities (what if a malicious user tricked application code into calling WorkerHelper.buildWorkerBlobUrl() with their own user-input string?)
  • Closures and global data are not packaged in the string format, so you really have no guarantee that this function will work.
    • WorkerHelper.buildWorkerBlobUrl(() => console.log(window.foo)); // window is not defined
    • WorkerHelper.buildWorkerBlobUrl(function() { console.log(this.foo); }.bind({ foo: 'bar' })); // this is undefined

@dgp1130 dgp1130 removed the needs: discussion On the agenda for team meeting to determine next steps label May 28, 2020
@TheKrisSodroski
Copy link

That sounds nice, but ideally, libraries would automatically wire up in a consuming application.

If there were some kind of standard that we developed for workers inside a library, the compiler would be able to automatically wire up service workers correctly, would they not?

This same issue somewhat exists for assets, styles, and scripts. In order to consume some libraries, you have to modify the consuming applications angular.json sections manually.

To circumnavigate this, I've seen libraries create a service that during App Initialization will essentially wire up scripts by appending to document body some CDN paths.

In our case, we end up deploying npm-install scripts in the packages that has to search, then modify, these sections of the angular application. While scaffolding exists, its extremely heavy duty for what is essentially a json merge of these sections from library and application.

@s4m0r4m4
Copy link

s4m0r4m4 commented Jun 9, 2020

Here's a use case to mention: I have an angular library for websocket comm. It contains an Angular Service, MyWebsocketService, which injects MyMessageHandlerService (from a different lib) to handle displaying/logging info and errors. MyWebsocketService is used across multiple applications and has deserialization code that creates MyMessage classes from the websocket buffer (rather than using JSON.stringify).

Sometimes, messages get very big (~300MB) and this causes MyWebsocketService to lock up the main thread while processing. I started looking to move all of the message handling (deserialization and MyMessage creation) to a webworker, which would then send the MyMessage classes to MyWebsocketService, which could broadcast the messages via an observable.

@dgp1130 thank you for posting such detailed instructions. I followed your approach:

  1. start with an application-specific service for handling the websocket communication, App1WebsocketService
  2. make a web worker for a specific application, App1Webworker
  3. import the general deserialization code from the library into App1Webworker
  4. when a new websocket communication comes in, App1Webworker deserializes and creates a MyMessage and uses postMessage to send it to App1WebsocketService. It can also throw an error.
  5. App1WebsocketService then injects MyMessageHandlerService (from a different lib) and has to respond to completed messages or errors.

Here are the drawbacks:
A) Each application then has to replicate the same App1Webworker code, which is unfortunate, but manageable since most of the heavy lifting can be done in the general deserialization code from the lib.
B) Each application also has to implement the way in which it responds to errors from the websocket, which I would ideally be able to enforce the same response across all apps.
C) I can't import code from barrels that includes angular components. I've noticed that if I import code from barrel that includes an angular component, ng serve throws the following error where it appears to try and parse the HTML template for the angular component:

ERROR in ..../app1.worker.ts (C:/.../node_modules/worker-plugin/dist/loader.js?name=0!./src/app/core/services/rh-ui-router.worker.ts)
Module build failed (from C:/.../node_modules/worker-plugin/dist/loader.js):
ModuleParseError: Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> <span *ngIf="this.m_cAsyncTask">
|     <strong>Task Status</strong> (Id: {{ this.m_cAsyncTask.Id }})
|
    at handleParseError (C:\AIMDev\CI-Web\node_modules\webpack\lib\NormalModule.js:469:19)
    at C:\...\node_modules\webpack\lib\NormalModule.js:503:5
    at C:\...\node_modules\webpack\lib\NormalModule.js:358:12
    at C:\...\node_modules\loader-runner\lib\LoaderRunner.js:373:3
    at iterateNormalLoaders (C:\...\node_modules\loader-runner\lib\LoaderRunner.js:214:10)
    at C:\AIMDev\CI-Web\node_modules\loader-runner\lib\LoaderRunner.js:205:4
    at VirtualFileSystemDecorator.readFile (C:\...\node_modules\@ngtools\webpack\src\virtual_file_system_decorator.js:42:13)
    at processResource (C:\...\node_modules\loader-runner\lib\LoaderRunner.js:202:11)
    at iteratePitchingLoaders (C:\...\node_modules\loader-runner\lib\LoaderRunner.js:158:10)
    at runLoaders (C:\...\node_modules\loader-runner\lib\LoaderRunner.js:365:2)
    at NormalModule.doBuild (C:\...\node_modules\webpack\lib\NormalModule.js:295:3)
    at NormalModule.build (C:\...b\node_modules\webpack\lib\NormalModule.js:446:15)
    at Compilation.buildModule (C:\...\node_modules\webpack\lib\Compilation.js:739:10)
    at C:\...\node_modules\webpack\lib\Compilation.js:981:14
    at C:\...\node_modules\webpack\lib\NormalModuleFactory.js:409:6
    at C:\...\node_modules\webpack\lib\NormalModuleFactory.js:155:13

If I simply import the code directly from its file (rather than the barrel), the error goes away.
D) I can't use postMessage in the shared library code (since it doesn't use the worker postMessage). This means that if I want to code something like "any time you receive a websocket message, send it to the client via postMessage()", I need to implement it in each specific worker rather than the shared code.
E) Another use case - an angular image component that renders the image on canvas using a web worker. I have multiple applications that would love to utilize this functionality, but I can't put same angular component in a library to share because the angular component utilizes a web worker, which can't go in a library.

TLDR - it does indeed seem like there are ways of work around this limitation, but it results in duplicate code across applications, and there are all sorts of nuiances that I'm still discovering. I somewhat understand the challenges you describe here, but it would certainly be nice if I could simply package my web worker into a library that any application could use.

@klausj
Copy link
Author

klausj commented Jun 9, 2020

The workaround previously mentioned is quite clever but not usable for production.

  • This introduces potential XSS security vulnerabilities (what if a malicious user tricked application code into calling WorkerHelper.buildWorkerBlobUrl() with their own user-input string?)

@dgp1130 thanks for your comment.
Could you please explain in more detail how an XSS attack could occur.
The argument to WorkerHelper.buildWorkerBlobURL() is the worker method/function of my own class. There is no user input involved.

@dgp1130
Copy link
Collaborator

dgp1130 commented Jun 9, 2020

@klausj, I don't immediately see anything exploitable in your ClassWithWorker example, however the general design relies on application code doing the right thing in a way that can't be guaranteed. I made a quick example of a well-meaning but flawed application which attempts to log all query parameters in a worker (and does so in a vulnerable way).

https://stackblitz.com/edit/typescript-b4g24s

// Vulnerable to XSS, do NOT use this example!
class VulnerableWorkerWrapper {
  static worker?: Worker;

  work() {
    self.onmessage = ({ data }) => {
      console.log(`${data.key}: ${data.value}`);
    };
  }

  start() {
    VulnerableWorkerWrapper.worker = new Worker(WorkerHelper.buildWorkerBlobURL(this.work));
    for (const [ key, value ] of Object.entries(this)) {
      VulnerableWorkerWrapper.worker.postMessage({ key, value });
    }
  }
};

const vulnerableWorker = new VulnerableWorkerWrapper();

const params = new URL(window.location.href).searchParams;
for (const [ key, value ] of Array.from(params.entries())) {
  vulnerableWorker[key] = value; // BAD!!! What happens if `key === 'work'`?
}

vulnerableWorker.start();

This example can be trivially exploited by setting JavaScript code in the work query parameter: https://typescript-b4g24s.stackblitz.io/?work=function(){console.log(%22Imma%20mine%20some%20bitcoin%20in%20here!%22)}.

Ultimately buildWorkerBlobURL() is converting a string into a function and relies on that input being sanitized and trusted. It is incredibly difficult to prove that a given piece of data can't be corrupted by a clever attacker. Maybe trusted types could help work around this (not sure of the current status), but there is always a fundamental XSS risk when you interpret a string like this.

@klausj
Copy link
Author

klausj commented Jul 16, 2020

Thanks for your interesting demo!
I program mainly with statically typed programming languages and sometimes forget that a function property can be assigned with any value in JavaScript.

And I updated my code snippet with a type check which should ensure that the builder method only accepts values of type 'Function'. Injected code strings should be rejected with an error at runtime.

@TheKrisSodroski
Copy link

TheKrisSodroski commented Aug 5, 2020

@dgp1130 Sanitizing input is always a must, even in in Angular today. If Angular allows interpolation without requiring the user to sanitize, I don't see why this case should be any different.

The power that could be unleashed by allowing libraries to encapsulate and provide worker support is just too tempting.

Like i mentioned before, we already hook into npm install in order to auto wire up asset, style, and dependent package support. I'm beginning to do the same with this just for investigation.

@TheKrisSodroski
Copy link

TheKrisSodroski commented Aug 6, 2020

Using some of @dgp1130 thoughts, I was able to kludge together a solution that worked. Not ideal. Won't be putting this into production in any way shape or form until there's less glue required to get this to work.

Getting the thing to compile required absolute paths in any of the webworker code. Got a bunch of typing errors when any library was references by relative or module name.

It seems when writing the code in a library, there can be no javascript imports or else it won't compile correctly. On top of that, in the parent application, I had to reference the js file directly in the web worker (after compiling the worker using tsc manually in the library and deploying it with the package).

import { SomeClassWithTrueImplementation } from '@mylib';

forget about doing that. You'll be met with typescript typings mismatches between the dom library and the worker library.

It also seems that any functionality in the lib must be restricted to exporting functions. I originally wrote my library code in a class. In the parent worker, using new MyClass(). Instantly would break.

The component then requires an input of type Worker that the parent passes to it (since calling new Worker inside the library doesn't work at all).

That being said, the performance improvement, even without using Transferable between the worker and parent, was astounding.

Better support/encapsulation of this technology in angular would be revolutionary.

@Ocean-Blue
Copy link

I am also trying to do the same thing. So far the only way I can make it work is by:

I have also tried to keep the logic for the web worker in the library as much as possible, by keeping the initialization of the worker object in the library. But to do this, I have to pass the hard-coded file path to the worker script. Passing the path by using Input or InjectionToken does not work.
Example here: https://github.com/Ocean-Blue/angular-web-worker/tree/master/web-worker-solution-1

Another workaround I have tried is to write the worker script as a JS script, then import it through angular.json. But not able to make this work. It gives me a mimeType (text/html) not matched.
Example here: https://github.com/Ocean-Blue/angular-web-worker/tree/master/web-worker-solution-js

Has anyone tried the 3rd workaround?

@Akxe
Copy link

Akxe commented Dec 14, 2020

I will leave a note here, I need this too!

I am clustering 62000 markers on a map. When I do it from the main thread the process takes up to several seconds on a normal PC... I would love to be able to split it. I have the splitting ready, I only need to be able to create a worker...

@maxime1992
Copy link
Contributor

While I hope that at some point we'll have proper support for this, it's not too painful to go around this limitation by using dependency injection.

The only thing that is blocking us to use a webworker in a library is the creation of the worker itself, which is pretty much one line: new Worker('./your/path/to/the/worker.ts', { type: 'module' });. So as long as this is declared in the main app, we're good 🙌.

Disclaimer: I'm only sharing a solution which @zakhenry introduced within our project a long time ago so most of the credits here go to him 😸!

Let see how to achieve this using DI.

I've created a small open source repro here: https://github.com/maxime1992/demo-webworker-library

The main idea being:

  • one library (fibonacci) gives us access to a fibonacci function (which is a slow implementation on purpose)
  • one library (fibonacci-webworker) gives us access to an InjectionToken and also holds the logic of the webworker (which is pretty dumb and will simply make a call to the fibonacci function from the other library within the worker context)
  • one library (demo-lib-consuming-webworker) which will use the web worker
  • one app which first declare how to use the web worker through DI and also call the library demo-lib-consuming-webworker which uses the web worker

Here's a schema representing the overall idea of what we want to simulate through DI:

image

First of all, I've picked a slow implementation for the fibonacci function (on purpose):

projects/fibonacci/src/lib/fibonacci.ts

// I've chosen a slow implementation of fibonacci on purpose
// so that it takes more time to run otherwise an optimised
// version could take less than 1s to compute fibonacci(3000)
// while this one will struggle with fibonacci(30)
// https://stackoverflow.com/questions/11287418/why-is-this-js-code-so-slow
export const fibonacci = (n) => {
  if (n < 2) {
    return n;
  } else {
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
};

Then we can build the library fibonacci-webworker which will just be a wrapper to call the fibonacci function from a webworker context:

**projects/fibonacci-webworker/src/lib/fibonacci.ts**

/// <reference lib="webworker" />

// this file is excluded from the compilation of the `fibonacci-webworker` library
// this is due to the fact that an app has to import it directly (instead of a path
// coming from a ts alias path)

import { fibonacci } from 'fibonacci';

addEventListener('message', ({ data }) => {
  postMessage(fibonacci(data));
});

Important notes:

  • we need to not include this file within the projects/fibonacci-webworker/src/public-api.ts as we don't want it to be "really" part of this library. We'll come back to that later
  • we also need to exclude this file from TS compilation for this library by going in projects/fibonacci-webworker/tsconfig.lib.json and adding "src/lib/fibonacci.ts" to the exclude array which then becomes: "exclude": ["src/lib/fibonacci.ts", "src/test.ts", "**/*.spec.ts"]

We can now create a token which we'll use later on to provide our web worker through DI:

projects/fibonacci-webworker/src/lib/fibonacci.token.ts

import { InjectionToken } from '@angular/core';

export const FIBONACCI_WEBWORKER_FACTORY = new InjectionToken<() => Worker>(
  'fibonacci'
);

As you can see it's been defined as a factory so that we can easily create new instances of the worker on demand 👍.

Then we expose this token by exporting it from projects/fibonacci-webworker/src/public-api.ts: export * from './lib/fibonacci.token';

Now, within our component in demo-lib-consuming-webworker library, even though we don't know how the worker will be provided... We don't care. We know we can use the web worker through DI:

projects/demo-lib-consuming-webworker/src/lib/demo-lib-consuming-webworker.component.ts

import { Component, Inject } from '@angular/core';
import { FIBONACCI_WEBWORKER_FACTORY } from 'fibonacci-webworker';
import { interval } from 'rxjs';

@Component({
  selector: 'lib-demo-lib-consuming-webworker',
  template: `
    <p>
      Timer just to prove that the page isn't frozen while we compute fibonacci
      (which it would if we were doing it on the main thread!):
      {{ timer$ | async }}
    </p>

    <div *ngIf="result; else computingTpl">
      <h1>Fibonacci 45</h1>
      <p>{{ result | json }}</p>
    </div>

    <ng-template #computingTpl>
      Computing fibonacci(45)... Be patient =)!
    </ng-template>
  `,
})
export class DemoLibConsumingWebworkerComponent {
  public result;

  public timer$ = interval(1000);

  constructor(
    @Inject(FIBONACCI_WEBWORKER_FACTORY) fibonacciWebworkerFactory: () => Worker
  ) {
    const fibonacciWebworker = fibonacciWebworkerFactory();

    fibonacciWebworker.onmessage = ({ data }) => {
      this.result = data;
    };

    fibonacciWebworker.postMessage(45);
  }
}

Last remaining bit to have everything working is to define the worker factory from the main app and use the demo-lib-consuming-webworker through the router by lazy loading it.

In order to include the web worker within the compilation of the app, we add a tsconfig.worker.json at the root of the project:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/worker",
    "lib": ["es2018", "webworker"],
    "types": []
  },
  "include": [
    "src/**/*.worker.ts",
    "projects/fibonacci-webworker/src/lib/fibonacci.ts"
  ]
}

Note that the most important line is within the include array: "projects/fibonacci-webworker/src/lib/fibonacci.ts"

In order to use it we've got to declare it into angular.json. Here's the JSON structure where we should declare it:

projects.demo-webworker-library.architect.build.options

And the line we want to add is:

"webWorkerTsConfig": "tsconfig.worker.json"

Finally, our app.module:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { FIBONACCI_WEBWORKER_FACTORY } from 'fibonacci-webworker';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {
        path: '',
        loadChildren: () =>
          import('demo-lib-consuming-webworker').then(
            (m) => m.DemoLibConsumingWebworkerModule
          ),
      },
    ]),
  ],
  providers: [
    {
      provide: FIBONACCI_WEBWORKER_FACTORY,
      useValue: function (): Worker {
        return new Worker('projects/fibonacci-webworker/src/lib/fibonacci', {
          name: 'fibonacci.worker',
          type: 'module',
        });
      },
    },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}
  • Using the RouterModule we tell angular to lazy load our library
  • Using the providers property we provide the FIBONACCI_WEBWORKER_FACTORY using useValue and passing a function which wraps the creation of the worker

And here's the result of the app:

While the webworker is running Once it's done
image image

Important note:

  • The timer representing the number of seconds elapsed since we started the app keeps increasing every seconds while the computation is happening in the web worker. If we were doing the computation on the main thread we wouldn't be able to see it increasing untill the fibonacci call would be done ~30s later...

image

  • We can see our web worker running in the developper panel (source tab)
    image

While I'm aware the above may seem a bit scarry and long, I've been giving quite a lot of details and the number of changes to actually build all of this (not including the creation of the 3 different libs because it's all done by the CLI and we don't have to care about it) is not that big:

image

I reckon following this would take the first time maybe... 5 to 10mn and reusing the worker within other bits of the same app once already created/declared in the app would take 10s 😺!

@s4m0r4m4
Copy link

@maxime1992 Thank you so much for that detailed writeup, it was incredibly helpful and seems like a good interim solution.
My only suggestion is to add an exported type for the function signature (of the factory) so that you can use it in the component @Inject statement, in the AppModule providers section, and in the declaration of the token itself. That way if, say you decide you actually want to pass in an id to put in the worker name, you can update the one signature and everything stays in sync.

Notable downsides that I'd look for a native angular feature to solve:

  1. Avoid having to hard-code the path and filename of the worker.ts file in the app and lib tsconfig files.
  2. Somehow put the tsconfig.worker.json in the lib, not the app (not sure how this would be accomplished, but would be nice)
  3. Avoid the injection token altogether and provide/construct the worker in the component directly. This would provide much cleaner encapsulation.

Again, thanks for your solution, I'll be using that for now and looking for a solution from Angular in the long-term.

@Akxe
Copy link

Akxe commented Apr 16, 2021

@dgp1130 Do you know if anything has changed since Angular 12 supports webpack 5 (d883ce5) and changed the way web workers loading has changed because of it?

@IlgamGabdullin
Copy link

are there any chances it will be implemented in the near future?

@JoFrMueller
Copy link

We tried to play around a bit with the different approaches mentioned here, but in the end it always comes down to the problem that web-worker TypeScript files aren't compiled towards JavaScript during ng build, if you don't reference the web-worker files directly via some Angular project of type 'application' and hardcode the affected file patterns as configuration parameters... In fact, the angular.json schema even prevents libraries from inserting web-worker related configuration properties.

The approach of @maxime1992 consisted of a lot of "indirections" to make web-workers actually work, but the approach is useless if you're not inside the sphere of a shared monorepo. I don't see it working as soon as library and application do live in separate repositories, because the following assumption wouldn't make sense anymore:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/worker",
    "lib": ["es2018", "webworker"],
    "types": []
  },
  "include": [
    "src/**/*.worker.ts",
    "projects/fibonacci-webworker/src/lib/fibonacci.ts"
  ]
}

The entry "projects/fibonacci-webworker/src/lib/fibonacci.ts" does only make sense in a monorepo. But if we want to provide web-worker based standalone libraries we're out of luck.

It seems there is still some kind of custom webpack magic required like described by worker-plugin...

Can anyone of the Angular team tell us if this will change anytime soon? Or do you consider the execution environment of an algorithm to be a developer decision? So a library only provides the algorithms and interfaces, but a web-worker would execute the algorithms against given parameters?

@alan-agius4
Copy link
Collaborator

alan-agius4 commented Jun 28, 2021

Or do you consider the execution environment of an algorithm to be a developer decision? So a library only provides the algorithms and interfaces, but a web-worker would execute the algorithms against given parameters?

IMHO, this would be the preferred approach, by using web-workers in a library you are making that library platform dependent as it cannot run on Node.JS since web-workers are only available in the browser. I'd definitly suggest the decision on how to run/consume a method to the application developer.

So far, I didn't see a completing enough use-case were web-workers in a library were the optimal solution. In some cases the best solution was not to use web-workers and shift the heavy data transformation and calculations to the backend.

@Akxe
Copy link

Akxe commented Jun 28, 2021

IMHO, this would be the preferred approach, by using web-workers in a library you are making that library platform dependent as it cannot run on Node.JS since web-workers are only available in the browser. I'd definitly suggest the decision on how to run/consume a method to the application developer.

Server-side rendering could potentially be impossible, but when someone creates computation with web workers, they do fallback too.

So far, I didn't a completing enough use-case were web-workers in a library were the optimal solution. In some cases the best solution was not to use web-workers and shift the heavy data transformation and calculations to the backend.

A server that is paid by process time. Dumb servers. Offline availability.

I don't see a reason why the decision should not be up to the developer. Forbidding the option in libraries does no sense.


Edit: 62 people gave this issue "like" which means that at least 62 people were bothered enough by not being able to do it to vote on it. At least 62 people found a compelling reason why we would like to have this option.

@Akxe
Copy link

Akxe commented Aug 11, 2021

Could we get the vote for the feature request done? @anuglar-robot

I feel like we would easily get 30 votes to pass this.

@alan-agius4 alan-agius4 removed the freq1: low Only reported by a handful of users who observe it rarely label Aug 11, 2021
@angular-robot angular-robot bot added the feature: under consideration Feature request for which voting has completed and the request is now under consideration label Feb 1, 2022
@ngbot ngbot bot modified the milestones: Backlog, needsTriage Feb 1, 2022
@muhamedkarajic
Copy link

Have also a usecase where I need this.

@maxime1992
Copy link
Contributor

@muhamedkarajic meanwhile, I explained how to do it fairly easily here: #15059 (comment)

@KrisSodroski
Copy link

I think your solution is great, but it would be ideal if library authors could encapsulate web workers within themselves while hiding that internal implementation from its consumers.

@Ketec
Copy link

Ketec commented Dec 14, 2022

Was just looking into the viability of web/shared workers In a data-processing-heavy enterprise solution.
It is not a mono repository - but rather libraries are used as micro frontends in separate repositories for different teams.

So those teams would not be able to build and configure workers of any kind in the wrapper "shell".
And even though the lazy loaded modules currently exist as npm packages - near future is full Module federation - even if you can hack-export the worker files from a library, they will not be physically available anymore in the "application" shell.

It's 2023 almost and we are still stuck with single-thread apps in the era of 16 thread+ computers/devices.

Perhaps libraries could expose assets/files - like when lazy loading modules with the router?
So new Worker('./lazy-route/test.worker') would resolve to <appdomain>/lazy-route/test.worker for the network request.

I feel like a way for libraries to expose assets/js files to be accessible from the main app is needed - not just what is directly bundled under the app dist.

@sancelot
Copy link

I made a template to dynamically launch a library webworker. This project uses a reactjs application, but this may help.
https://github.com/sancelot/nx-webworker-example

@timautin
Copy link

timautin commented Apr 11, 2024

I am developping a 3D mapping library, I also need workers in libs.

IMHO, this would be the preferred approach, by using web-workers in a library you are making that library platform dependent as it cannot run on Node.JS since web-workers are only available in the browser. I'd definitly suggest the decision on how to run/consume a method to the application developer.

So far, I didn't see a completing enough use-case were web-workers in a library were the optimal solution. In some cases the best solution was not to use web-workers and shift the heavy data transformation and calculations to the backend.

And since when every single library in the world has to support NodeJS? In my case it's a complete non-sense, it has to run in a browser to load a WebGL canvas. The fact you didn't see use-cases doesn't mean there are none, only that you didn't search very hard 😉

We're curious to hear if there are other use cases which might not be well-supported by this kind of design. The only one I can think of would be a library which wants to manage its workers directly (like a ThreadPool in Java), but that could be worked around and I'm not aware of any web worker use cases which fit that kind of model. You can't run arbitrary functions in a worker, so the comparison to ThreadPool isn't entirely accurate here. If there are other use cases that don't work well here, please post them so we can re-evaluate if necessary.

Indeed all libs that want to manage their workers directly would benefit that. There are workarounds but it would be much better to have a native support.

Anyway, thanks @maxime1992 for the suggested solution. There's another option described here: https://stackoverflow.com/questions/57072680/web-worker-in-angular-library that seems simpler and will try first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: @schematics/angular feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature
Projects
None yet
Development

No branches or pull requests