workerize-loader icon indicating copy to clipboard operation
workerize-loader copied to clipboard

Typescript types?

Open spion opened this issue 7 years ago • 16 comments

Its a bit difficult to write a definition file... any ideas how to handle that?

Would be easier if the export was the factory function that returns the exported functions:

export default function worker() {
    function expensive(time) {
        let start = Date.now(), count = 0
        while (Date.now() - start < time) count++
        return count
    }
    return {expensive}
}

then the types would be identical if the functions return promises.

spion avatar Jan 10 '18 16:01 spion

I'm not against that, but it would make statically analyzing the exports mostly impossible. One direction I'd thought of going for a 2.0 (that happens to unify workerize and workerize-loader) would be to switch both the worker module and wrapper to be factories:

export default async function worker() {
  await sleep()  // yay, optional async init
  // define methods here as you mentioned
  return { method, method2 }
}

// then in the consumer:
import worker from 'workerize!./worker'
worker().then( async inst => {
  await inst.method()
})

One really neat advantage here is that it would allow for the module exports to instead be collected at runtime and sent to the parent thread via the existing RPC ready event. It requires asynchronous initialization on the consuming side, but we could also take advantage of that to then allow async init within the worker. This also makes the worker.ready promise unnecessary, which is nice. The biggest benefit though would be that re-exports and * exports would work perfectly - right now they fail since we're doing static export analysis.

developit avatar Jan 10 '18 19:01 developit

I just dropped this library into my typescript project and it was really easy to use, but it's tough loosing the type safety.

are there any plans/issues/prs where I could see (and maybe help) the effort towards this feature?

Place1 avatar Feb 15 '18 04:02 Place1

My style like this:

// ./workers/md5.worker.ts
import md5 from './lib/md5'
export function md5(file: File) {
  return md5(file)
}
// then in the consumer:
import * as md5Worker from './workers/md5.worker'

const { md5 } = (md5Worker as any)() as typeof md5Worker
// md5 type in vscode is: (file: File) => Promise<{}>

cncolder avatar Mar 14 '18 14:03 cncolder

Well this loader was ridiculously easy to set up 🥇 @developit 🙏

I think it's even easier in TypeScript—if you use async

// worker.ts
export async function expensive(time: number) {
        const start = Date.now();
        let count: number = 0;
        while (Date.now() - start < time) {
            count++;
        }
        return count;
}
// app.ts
import ExampleWorker from './workers/example.worker';

const instance = ExampleWorker();

instance.expensive(1000).then(count => {
    console.log(`Ran ${count} loops`);
});

Type checking works well:

image

phil-lgr avatar Mar 20 '18 02:03 phil-lgr

Agreed - best practise I'd recommend for everyone using workerize-loader is to use async/await or Promises in your module. That way, there's no interface difference created by applying workerize, it just changes the runtime context to a worker.

developit avatar Apr 13 '18 02:04 developit

Could anyone here provide a small demo repository? At the moment I'm struggling a little bit with this and don't get it to work.

  • did you change your webpack.config.js?
  • do you have a custom.d.ts with for example declare module 'workerize-loader!*';?
  • why did @cncolder and @phil-lgr didn't prefix the import with workerize-loader! like import * as md5Worker from 'workerize-loader!./workers/md5.worker'?
  • how does your tsconfig.json look like?

screendriver avatar May 18 '18 20:05 screendriver

@screendriver

My webpack config is default and simple:

        {
            test: /\.worker\.js$/,
            use: [
                { loader: 'workerize-loader' },
            ]
        }

When you import * as md5Worker from './workers/md5.worker'. You got a function from workerize-loader like this:

function md5Worker() {
  return {
    md5(file) {}
  }
}

But typescript thinking the type is

type md5Worker = {
  md5: (file: File) => Promise<{}>
}

So we need fool typescript.

const { md5 } = (md5Worker as any /* I'm not object, i'm a function */)() as typeof md5Worker /* The result of function is md5Worker  */

Why not 'workerize-loader!*'?

Because there is no way to tell typescript what types in the *.worker.js file.

cncolder avatar May 22 '18 07:05 cncolder

Thank you for your feedback, but this doesn't work. I created a small demo repo. Just make a yarn install && yarn start and you can see the webpack error Uncaught TypeError: _workers_md5_worker__WEBPACK_IMPORTED_MODULE_0__ is not a function

By the way: is this on purpose that you wrote test: /\.worker\.js$/ and not test: /\.worker\.ts$/ in your webpack.config.js?

screendriver avatar May 22 '18 09:05 screendriver

oops! I'm sorry. I forget my ts files compiled by vscode task.

I think you need update your webpack.config.js like this:

  module: {
    rules: [
      {
        test: /\.worker\.ts$/,
        use: ["workerize-loader", "ts-loader"]
      },
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/
      }
    ]
  }

And your workers/md5.worker.ts like this:

export async function md5(): Promise<string> {
  return "12345";
}

In index.ts, md5() return a promise.

md5().then((hash: string) => alert(hash));

cncolder avatar May 28 '18 07:05 cncolder

There should be a easy way to expose the terminate function while using this with Typescript. I get that we can probably do (workerInstance as any).terminate(), but I feel like it would be more intuitive if it was just workerInstance.terminate().

Cammisuli avatar May 29 '18 18:05 Cammisuli

I actually found a way to wrap the import to get the terminate function when creating the worker.

I created a wrapper function:

// create-worker.ts
type WorkerType<T> = T & Pick<Worker, 'terminate'>;
export function createWorker<T>(workerPath: T): WorkerType<T>  {
    return (workerPath as any)();
}
export type createWorker<T> = WorkerType<T>;

Then import it into the module where you want the web worker:

// app.ts
import { createWorker } from './create-worker'
import * as ExampleWorker from './example-worker';

const instance = createWorker(ExampleWorker);
instance.expensive(1000);
instance.terminate();

I have an example here: https://stackblitz.com/edit/typescript-zlgfwe

Cammisuli avatar May 30 '18 13:05 Cammisuli

I have trouble http://take.ms/OVssET http://take.ms/giGuE http://take.ms/yDYX1 http://take.ms/lqGor http://take.ms/3kMUu http://take.ms/sEos5

leonidkuznetsov18 avatar Jan 27 '19 13:01 leonidkuznetsov18

I ended up with this, which is not perfect, but seems like a good alternative to casting to any:

// global.d.ts
declare module "workerize-loader!*" {
  type AnyFunction = (...args: any[]) => any;
  type Async<F extends AnyFunction> = (...args: Parameters<F>) => Promise<ReturnType<F>>;

  type Workerized<T> = Worker & { [K in keyof T]: T[K] extends AnyFunction ? Async<T[K]> : never };

  function createInstance<T>(): Workerized<T>;
  export = createInstance;
}

// foo.worker.ts
export function foo(a: number, b: number) {
  // here is where you expensive work happens
  return a + b;
}

export function bar(a: number, b: number) {
  // here is where you expensive work happens
  return a * b;
}
// main.ts
import createFooWorker from "workerize-loader!./foo.worker";
import * as FooWorker from "./foo.worker";

const fooWorker = createFooWorker<typeof FooWorker>();

async function main() {
  const result = await fooWorker.foo(1, 2);
  const result2 = await fooWorker.bar(2, 3);

  fooWorker.terminate(); // works as well
}

There are probably ways to optimize this further, but this works without too much hassle for my use-case.

bengry avatar Oct 06 '19 10:10 bengry

@phil-lgr You don't have export default in your worker, how do you use import X from Y without an error?

Menci avatar Jan 21 '20 03:01 Menci

@Menci i think workerize would create the default for you, and the module u will be importing is really imported to a workerize module as its functions but not directly imported into your other module.

aeroxy avatar May 15 '20 10:05 aeroxy

@developit if it's any help, here's how I dealt with the type safety issue in my own build process that doesn't use workerize

https://www.obvibase.com/dev-blog/how-obvibase-uses-web-workers

and it has worked out really well.

I wonder if the same approach (.ui and .worker file naming convention) would work for you as well, even if you don't use TS compiler...

ivan7237d avatar Jul 07 '20 03:07 ivan7237d