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

Typescript types? #3

Open
spion opened this issue Jan 10, 2018 · 16 comments
Open

Typescript types? #3

spion opened this issue Jan 10, 2018 · 16 comments
Labels
question Further information is requested

Comments

@spion
Copy link

spion commented Jan 10, 2018

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.

@developit
Copy link
Owner

developit commented Jan 10, 2018

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.

@Place1
Copy link

Place1 commented Feb 15, 2018

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?

@cncolder
Copy link

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<{}>

@phil-lgr
Copy link

phil-lgr commented Mar 20, 2018

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

@developit
Copy link
Owner

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 developit added the question Further information is requested label Apr 13, 2018
@screendriver
Copy link

screendriver commented May 18, 2018

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?

@cncolder
Copy link

cncolder commented May 22, 2018

@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.

@screendriver
Copy link

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?

@cncolder
Copy link

cncolder commented May 28, 2018

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

@Cammisuli
Copy link

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
Copy link

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

@bengry
Copy link

bengry commented Oct 6, 2019

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.

@Menci
Copy link

Menci commented Jan 21, 2020

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

@aeroxy
Copy link

aeroxy commented May 15, 2020

@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.

@ivan7237d
Copy link

@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...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests