├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── blocking.gif
├── non-blocking.gif
├── blocking-react.gif
├── non-blocking-react.gif
├── src
├── setupTests.ts
├── App.test.tsx
├── my-first-worker
│ ├── index.ts
│ └── tsconfig.json
├── index.css
├── index.tsx
├── App.css
├── takeALongTimeToDoSomething.ts
├── App.tsx
├── react-app-env.d.ts
├── App.hooks.ts
├── logo.svg
└── serviceWorker.ts
├── config
├── jest
│ ├── cssTransform.js
│ └── fileTransform.js
├── pnpTs.js
├── paths.js
├── env.js
├── modules.js
├── webpackDevServer.config.js
└── webpack.config.js
├── .gitignore
├── tsconfig.json
├── LICENSE
├── scripts
├── test.js
├── start.js
└── build.js
├── README.md
├── package.json
└── BLOG.md
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/blocking.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnnyreilly/webworkers-comlink-typescript-react/HEAD/blocking.gif
--------------------------------------------------------------------------------
/non-blocking.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnnyreilly/webworkers-comlink-typescript-react/HEAD/non-blocking.gif
--------------------------------------------------------------------------------
/blocking-react.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnnyreilly/webworkers-comlink-typescript-react/HEAD/blocking-react.gif
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnnyreilly/webworkers-comlink-typescript-react/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnnyreilly/webworkers-comlink-typescript-react/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnnyreilly/webworkers-comlink-typescript-react/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/non-blocking-react.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnnyreilly/webworkers-comlink-typescript-react/HEAD/non-blocking-react.gif
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render();
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/my-first-worker/index.ts:
--------------------------------------------------------------------------------
1 | import { expose } from "comlink";
2 | import {
3 | takeALongTimeToDoSomething,
4 | takeALongTimeToAddTwoNumbers
5 | } from "../takeALongTimeToDoSomething";
6 |
7 | const exports = {
8 | takeALongTimeToDoSomething,
9 | takeALongTimeToAddTwoNumbers
10 | };
11 | export type MyFirstWorker = typeof exports;
12 |
13 | expose(exports);
14 |
--------------------------------------------------------------------------------
/config/jest/cssTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // This is a custom Jest transformer turning style imports into empty objects.
4 | // http://facebook.github.io/jest/docs/en/webpack.html
5 |
6 | module.exports = {
7 | process() {
8 | return 'module.exports = {};';
9 | },
10 | getCacheKey() {
11 | // The output is always the same.
12 | return 'cssTransform';
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/my-first-worker/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "strict": true,
5 | "target": "esnext",
6 | "module": "esnext",
7 | "lib": [
8 | "webworker",
9 | "esnext"
10 | ],
11 | "moduleResolution": "node",
12 | "noUnusedLocals": true,
13 | "sourceMap": true,
14 | "allowJs": false,
15 | "baseUrl": "."
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/config/pnpTs.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { resolveModuleName } = require('ts-pnp');
4 |
5 | exports.resolveModuleName = (
6 | typescript,
7 | moduleName,
8 | containingFile,
9 | compilerOptions,
10 | resolutionHost
11 | ) => {
12 | return resolveModuleName(
13 | moduleName,
14 | containingFile,
15 | compilerOptions,
16 | resolutionHost,
17 | typescript.resolveModuleName
18 | );
19 | };
20 |
21 | exports.resolveTypeReferenceDirective = (
22 | typescript,
23 | moduleName,
24 | containingFile,
25 | compilerOptions,
26 | resolutionHost
27 | ) => {
28 | return resolveModuleName(
29 | moduleName,
30 | containingFile,
31 | compilerOptions,
32 | resolutionHost,
33 | typescript.resolveTypeReferenceDirective
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/src/takeALongTimeToDoSomething.ts:
--------------------------------------------------------------------------------
1 | export function takeALongTimeToDoSomething() {
2 | console.log('Start our long running job...');
3 | const seconds = 5;
4 | const start = new Date().getTime();
5 | const delay = seconds * 1000;
6 | while (true) {
7 | if ((new Date().getTime() - start) > delay) {
8 | break;
9 | }
10 | }
11 | console.log('Finished our long running job');
12 | }
13 |
14 | export function takeALongTimeToAddTwoNumbers(number1: number, number2: number) {
15 | console.log('Start to add...');
16 | const seconds = 5;
17 | const start = new Date().getTime();
18 | const delay = seconds * 1000;
19 | while (true) {
20 | if ((new Date().getTime() - start) > delay) {
21 | break;
22 | }
23 | }
24 | const total = number1 + number2;
25 | console.log('Finished adding');
26 | return total;
27 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 John Reilly
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import "./App.css";
3 | import { useTakeALongTimeToAddTwoNumbers } from "./App.hooks";
4 |
5 | const App: React.FC = () => {
6 | const [number1, setNumber1] = useState(1);
7 | const [number2, setNumber2] = useState(2);
8 |
9 | const total = useTakeALongTimeToAddTwoNumbers(number1, number2);
10 |
11 | return (
12 |
13 |
Web Workers in action!
14 |
15 |
16 |
17 | setNumber1(parseInt(e.target.value))}
20 | value={number1}
21 | />
22 |
23 |
24 |
25 | setNumber2(parseInt(e.target.value))}
28 | value={number2}
29 | />
30 |
31 |
32 | Total:{" "}
33 | {total.isCalculating ? (
34 | Calculating...
35 | ) : (
36 | {total.total}
37 | )}
38 |
39 |
40 | );
41 | };
42 |
43 | export default App;
44 |
--------------------------------------------------------------------------------
/config/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const camelcase = require('camelcase');
5 |
6 | // This is a custom Jest transformer turning file imports into filenames.
7 | // http://facebook.github.io/jest/docs/en/webpack.html
8 |
9 | module.exports = {
10 | process(src, filename) {
11 | const assetFilename = JSON.stringify(path.basename(filename));
12 |
13 | if (filename.match(/\.svg$/)) {
14 | // Based on how SVGR generates a component name:
15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
16 | const pascalCaseFilename = camelcase(path.parse(filename).name, {
17 | pascalCase: true,
18 | });
19 | const componentName = `Svg${pascalCaseFilename}`;
20 | return `const React = require('react');
21 | module.exports = {
22 | __esModule: true,
23 | default: ${assetFilename},
24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
25 | return {
26 | $$typeof: Symbol.for('react.element'),
27 | type: 'svg',
28 | ref: ref,
29 | key: null,
30 | props: Object.assign({}, props, {
31 | children: ${assetFilename}
32 | })
33 | };
34 | }),
35 | };`;
36 | }
37 |
38 | return `module.exports = ${assetFilename};`;
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | declare namespace NodeJS {
6 | interface ProcessEnv {
7 | readonly NODE_ENV: 'development' | 'production' | 'test';
8 | readonly PUBLIC_URL: string;
9 | }
10 | }
11 |
12 | declare module '*.bmp' {
13 | const src: string;
14 | export default src;
15 | }
16 |
17 | declare module '*.gif' {
18 | const src: string;
19 | export default src;
20 | }
21 |
22 | declare module '*.jpg' {
23 | const src: string;
24 | export default src;
25 | }
26 |
27 | declare module '*.jpeg' {
28 | const src: string;
29 | export default src;
30 | }
31 |
32 | declare module '*.png' {
33 | const src: string;
34 | export default src;
35 | }
36 |
37 | declare module '*.webp' {
38 | const src: string;
39 | export default src;
40 | }
41 |
42 | declare module '*.svg' {
43 | import * as React from 'react';
44 |
45 | export const ReactComponent: React.FunctionComponent>;
46 |
47 | const src: string;
48 | export default src;
49 | }
50 |
51 | declare module '*.module.css' {
52 | const classes: { readonly [key: string]: string };
53 | export default classes;
54 | }
55 |
56 | declare module '*.module.scss' {
57 | const classes: { readonly [key: string]: string };
58 | export default classes;
59 | }
60 |
61 | declare module '*.module.sass' {
62 | const classes: { readonly [key: string]: string };
63 | export default classes;
64 | }
65 |
--------------------------------------------------------------------------------
/scripts/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Do this as the first thing so that any code reading it knows the right env.
4 | process.env.BABEL_ENV = 'test';
5 | process.env.NODE_ENV = 'test';
6 | process.env.PUBLIC_URL = '';
7 |
8 | // Makes the script crash on unhandled rejections instead of silently
9 | // ignoring them. In the future, promise rejections that are not handled will
10 | // terminate the Node.js process with a non-zero exit code.
11 | process.on('unhandledRejection', err => {
12 | throw err;
13 | });
14 |
15 | // Ensure environment variables are read.
16 | require('../config/env');
17 |
18 |
19 | const jest = require('jest');
20 | const execSync = require('child_process').execSync;
21 | let argv = process.argv.slice(2);
22 |
23 | function isInGitRepository() {
24 | try {
25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
26 | return true;
27 | } catch (e) {
28 | return false;
29 | }
30 | }
31 |
32 | function isInMercurialRepository() {
33 | try {
34 | execSync('hg --cwd . root', { stdio: 'ignore' });
35 | return true;
36 | } catch (e) {
37 | return false;
38 | }
39 | }
40 |
41 | // Watch unless on CI or explicitly running all tests
42 | if (
43 | !process.env.CI &&
44 | argv.indexOf('--watchAll') === -1 &&
45 | argv.indexOf('--watchAll=false') === -1
46 | ) {
47 | // https://github.com/facebook/create-react-app/issues/5210
48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository();
49 | argv.push(hasSourceControl ? '--watch' : '--watchAll');
50 | }
51 |
52 |
53 | jest.run(argv);
54 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `yarn test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `yarn build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `yarn eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
--------------------------------------------------------------------------------
/src/App.hooks.ts:
--------------------------------------------------------------------------------
1 | import { wrap, releaseProxy } from "comlink";
2 | import { useEffect, useState, useMemo } from "react";
3 |
4 | /**
5 | * Our hook that performs the calculation on the worker
6 | */
7 | export function useTakeALongTimeToAddTwoNumbers(
8 | number1: number,
9 | number2: number
10 | ) {
11 | // We'll want to expose a wrapping object so we know when a calculation is in progress
12 | const [data, setData] = useState({
13 | isCalculating: false,
14 | total: undefined as number | undefined
15 | });
16 |
17 | // acquire our worker
18 | const { workerApi } = useWorker();
19 |
20 | useEffect(() => {
21 | // We're starting the calculation here
22 | setData({ isCalculating: true, total: undefined });
23 |
24 | workerApi
25 | .takeALongTimeToAddTwoNumbers(number1, number2)
26 | .then(total => setData({ isCalculating: false, total })); // We receive the result here
27 | }, [workerApi, setData, number1, number2]);
28 |
29 | return data;
30 | }
31 |
32 | function useWorker() {
33 | // memoise a worker so it can be reused; create one worker up front
34 | // and then reuse it subsequently; no creating new workers each time
35 | const workerApiAndCleanup = useMemo(() => makeWorkerApiAndCleanup(), []);
36 |
37 | useEffect(() => {
38 | const { cleanup } = workerApiAndCleanup;
39 |
40 | // cleanup our worker when we're done with it
41 | return () => {
42 | cleanup();
43 | };
44 | }, [workerApiAndCleanup]);
45 |
46 | return workerApiAndCleanup;
47 | }
48 |
49 | /**
50 | * Creates a worker, a cleanup function and returns it
51 | */
52 | function makeWorkerApiAndCleanup() {
53 | // Here we create our worker and wrap it with comlink so we can interact with it
54 | const worker = new Worker("./my-first-worker", {
55 | name: "my-first-worker",
56 | type: "module"
57 | });
58 | const workerApi = wrap(worker);
59 |
60 | // A cleanup function that releases the comlink proxy and terminates the worker
61 | const cleanup = () => {
62 | workerApi[releaseProxy]();
63 | worker.terminate();
64 | };
65 |
66 | const workerApiAndCleanup = { workerApi, cleanup };
67 |
68 | return workerApiAndCleanup;
69 | }
70 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const url = require('url');
6 |
7 | // Make sure any symlinks in the project folder are resolved:
8 | // https://github.com/facebook/create-react-app/issues/637
9 | const appDirectory = fs.realpathSync(process.cwd());
10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
11 |
12 | const envPublicUrl = process.env.PUBLIC_URL;
13 |
14 | function ensureSlash(inputPath, needsSlash) {
15 | const hasSlash = inputPath.endsWith('/');
16 | if (hasSlash && !needsSlash) {
17 | return inputPath.substr(0, inputPath.length - 1);
18 | } else if (!hasSlash && needsSlash) {
19 | return `${inputPath}/`;
20 | } else {
21 | return inputPath;
22 | }
23 | }
24 |
25 | const getPublicUrl = appPackageJson =>
26 | envPublicUrl || require(appPackageJson).homepage;
27 |
28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
29 | // "public path" at which the app is served.
30 | // Webpack needs to know it to put the right