-
+
diff --git a/apps/demo/src/app/pages/speech/speech-page.component.ts b/apps/demo/src/app/pages/speech/speech-page.component.ts
index 71e9dbcf1..80bccfee0 100644
--- a/apps/demo/src/app/pages/speech/speech-page.component.ts
+++ b/apps/demo/src/app/pages/speech/speech-page.component.ts
@@ -1,4 +1,4 @@
-import {ChangeDetectionStrategy, Component, Inject} from '@angular/core';
+import {ChangeDetectionStrategy, Component, Inject, PLATFORM_ID} from '@angular/core';
import {
continuous,
isSaid,
@@ -11,6 +11,7 @@ import {
import {TuiContextWithImplicit, tuiPure} from '@taiga-ui/cdk';
import {merge, Observable, repeat, retry} from 'rxjs';
import {filter, mapTo, share} from 'rxjs/operators';
+import {isPlatformBrowser} from '@angular/common';
@Component({
selector: `speech-page`,
@@ -19,6 +20,7 @@ import {filter, mapTo, share} from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SpeechPageComponent {
+ readonly isBrowser = isPlatformBrowser(this.platformId);
paused = true;
voice = null;
@@ -30,6 +32,7 @@ export class SpeechPageComponent {
}: TuiContextWithImplicit
) => $implicit.name;
constructor(
+ @Inject(PLATFORM_ID) readonly platformId: Record,
@Inject(SPEECH_SYNTHESIS_VOICES)
readonly voices$: Observable,
@Inject(SpeechRecognitionService)
diff --git a/apps/demo/src/app/pages/workers/clock.component.ts b/apps/demo/src/app/pages/workers/clock.component.ts
new file mode 100644
index 000000000..10915a5f5
--- /dev/null
+++ b/apps/demo/src/app/pages/workers/clock.component.ts
@@ -0,0 +1,14 @@
+import {ChangeDetectionStrategy, Component} from '@angular/core';
+import {Observable, timer} from 'rxjs';
+import {map} from 'rxjs/operators';
+
+@Component({
+ selector: 'app-clock',
+ template: `
+ {{ date$ | async | date: 'mediumTime' }}
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ClockComponent {
+ readonly date$: Observable = timer(0, 1000).pipe(map(() => Date.now()));
+}
diff --git a/apps/demo/src/app/pages/workers/workers-page.component.html b/apps/demo/src/app/pages/workers/workers-page.component.html
new file mode 100644
index 000000000..2dd8054b2
--- /dev/null
+++ b/apps/demo/src/app/pages/workers/workers-page.component.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
Execution time: {{ workerData$ | async }}
+
+
+
+
Execution time: {{ result$ | async }}
+
+
diff --git a/apps/demo/src/app/pages/workers/workers-page.component.less b/apps/demo/src/app/pages/workers/workers-page.component.less
new file mode 100644
index 000000000..1e8a83cf7
--- /dev/null
+++ b/apps/demo/src/app/pages/workers/workers-page.component.less
@@ -0,0 +1,13 @@
+:host {
+ perspective: 150vw;
+ user-select: none;
+ flex-direction: column;
+ align-items: center;
+}
+
+.example {
+ min-width: 360px;
+ border-top: 1px solid gainsboro;
+ margin-top: 16px;
+ padding-top: 16px;
+}
diff --git a/apps/demo/src/app/pages/workers/workers-page.component.ts b/apps/demo/src/app/pages/workers/workers-page.component.ts
new file mode 100644
index 000000000..b79364b9d
--- /dev/null
+++ b/apps/demo/src/app/pages/workers/workers-page.component.ts
@@ -0,0 +1,31 @@
+import {ChangeDetectionStrategy, Component, Inject, PLATFORM_ID} from '@angular/core';
+import {toData, WebWorker} from '@ng-web-apis/workers';
+import {Subject} from 'rxjs';
+import {map} from 'rxjs/operators';
+import {isPlatformBrowser} from '@angular/common';
+
+function startCompute(): number {
+ const start = performance.now();
+
+ Array.from({length: 16000}).forEach((_, index) =>
+ Array.from({length: index}).reduce((sum: number) => sum + 1, 0),
+ );
+
+ return performance.now() - start;
+}
+
+@Component({
+ selector: `workers-page`,
+ templateUrl: `./workers-page.component.html`,
+ styleUrls: [`./workers-page.component.less`],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class WorkersPageComponent {
+ readonly isBrowser = isPlatformBrowser(this.platformId);
+ readonly workerThread = WebWorker.fromFunction(startCompute);
+ readonly workerData$ = this.workerThread.pipe(toData());
+ readonly emitter: Subject = new Subject();
+ readonly result$ = this.emitter.pipe(map(startCompute));
+
+ constructor(@Inject(PLATFORM_ID) readonly platformId: Record) {}
+}
diff --git a/apps/demo/src/app/pages/workers/workers-page.module.ts b/apps/demo/src/app/pages/workers/workers-page.module.ts
new file mode 100644
index 000000000..1af196d46
--- /dev/null
+++ b/apps/demo/src/app/pages/workers/workers-page.module.ts
@@ -0,0 +1,16 @@
+import {NgModule} from '@angular/core';
+import {WorkersPageComponent} from './workers-page.component';
+import {RouterModule} from '@angular/router';
+import {CommonModule} from '@angular/common';
+import {WorkerModule} from '@ng-web-apis/workers';
+import {ClockComponent} from './clock.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ WorkerModule,
+ RouterModule.forChild([{path: '', component: WorkersPageComponent}]),
+ ],
+ declarations: [WorkersPageComponent, ClockComponent],
+})
+export class WorkersPageModule {}
diff --git a/libs/universal/src/mocks.js b/libs/universal/src/mocks.js
index c30e2e72b..7551b50ce 100644
--- a/libs/universal/src/mocks.js
+++ b/libs/universal/src/mocks.js
@@ -39,6 +39,8 @@
global.DynamicsCompressorNode = class {};
global.GainNode = class {};
global.IIRFilterNode = class {};
+ global.speechSynthesis = class {};
+ global.SpeechSynthesisUtterance = class {};
global.PannerNode = class {};
global.ScriptProcessorNode = class {};
global.StereoPannerNode = class {};
diff --git a/libs/workers/LICENSE b/libs/workers/LICENSE
new file mode 100644
index 000000000..c62979cb5
--- /dev/null
+++ b/libs/workers/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Alexander Inkin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/libs/workers/README.md b/libs/workers/README.md
new file mode 100644
index 000000000..e6b5b8bac
--- /dev/null
+++ b/libs/workers/README.md
@@ -0,0 +1,116 @@
+# ![ng-web-apis logo](logo.svg) Web Workers API for Angular
+
+[![npm version](https://img.shields.io/npm/v/@ng-web-apis/workers.svg)](https://npmjs.com/package/@ng-web-apis/workers)
+[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@ng-web-apis/workers)](https://bundlephobia.com/result?p=@ng-web-apis/workers)
+[![Coveralls github](https://img.shields.io/coveralls/github/ng-web-apis/workers)](https://coveralls.io/github/ng-web-apis/workers?branch=master)
+
+This is a library for comfortable use of
+[Web Workers API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) with Angular.
+
+## Install
+
+If you do not have [@ng-web-apis/common](https://github.com/ng-web-apis/common):
+
+```
+npm i @ng-web-apis/common
+```
+
+Now install the package:
+
+```
+npm i @ng-web-apis/workers
+```
+
+## How it use
+
+Create a worker and use it in a template with `AsyncPipe`:
+
+```typescript
+import {WebWorker} from '@ng-web-apis/workers';
+
+function compute(data: number): number {
+ return data ** 2;
+}
+
+@Component({
+ template: `
+ Computed Result:
+ {{ event.data }}
+
+ `,
+})
+class SomeComponent {
+ readonly worker = WebWorker.fromFunction(compute);
+}
+```
+
+To get data from the worker event, use the toData operator
+
+```typescript
+import {toData, WebWorker} from '@ng-web-apis/workers';
+
+function compute(data: number): number {
+ return data ** 2;
+}
+
+@Component({
+ template: `
+ Computed Result: {{ workerData$ | async }}
+
+ `,
+})
+class SomeComponent {
+ readonly worker = WebWorker.fromFunction(compute);
+ readonly workerData$ = this.worker.pipe(toData());
+}
+```
+
+It's the same with `WorkerPipe` only:
+
+```typescript
+import {WorkerModule} from '@ng-web-apis/workers';
+import {NgModule} from '@angular/core';
+
+@NgModule({
+ imports: [WorkerModule],
+ declarations: [SomeComponent],
+})
+class SomeModule {}
+```
+
+```typescript
+import {WorkerExecutor, WebWorker} from '@ng-web-apis/workers';
+import {FormControl} from '@angular/forms';
+
+@Component({
+ template: `
+ Computed Result: {{ value | waWorker: changeData | async }}
+
+
+ `,
+})
+class SomeComponent {
+ value: string;
+
+ changeData(data: string): string {
+ return `${data} (changed)`;
+ }
+}
+```
+
+## See also
+
+Other [Web APIs for Angular](https://ng-web-apis.github.io/) by [@ng-web-apis](https://github.com/ng-web-apis)
+
+## Open-source
+
+Do you also want to open-source something, but hate the collateral work? Check out this
+[Angular Open-source Library Starter](https://github.com/TinkoffCreditSystems/angular-open-source-starter) we’ve created
+for our projects. It got you covered on continuous integration, pre-commit checks, linting, versioning + changelog, code
+coverage and all that jazz.
diff --git a/libs/workers/karma.conf.js b/libs/workers/karma.conf.js
new file mode 100644
index 000000000..47c96bef0
--- /dev/null
+++ b/libs/workers/karma.conf.js
@@ -0,0 +1,43 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: '',
+ frameworks: ['jasmine', '@angular-devkit/build-angular'],
+ plugins: [
+ require('karma-jasmine'),
+ require('karma-chrome-launcher'),
+ require('karma-jasmine-html-reporter'),
+ require('karma-coverage-istanbul-reporter'),
+ require('@angular-devkit/build-angular/plugins/karma'),
+ ],
+ client: {
+ clearContext: false, // leave Jasmine Spec Runner output visible in browser
+ },
+ coverageIstanbulReporter: {
+ dir: require('path').join(__dirname, '../../coverage/workers'),
+ reports: ['html', 'lcovonly'],
+ fixWebpackSourcePaths: true,
+ },
+ reporters: ['progress', 'kjhtml'],
+ port: 9876,
+ colors: true,
+ logLevel: config.LOG_INFO,
+ autoWatch: true,
+ browsers: ['ChromeHeadless'],
+ singleRun: true,
+ customLaunchers: {
+ ChromeHeadless: {
+ base: 'Chrome',
+ flags: [
+ '--no-sandbox',
+ '--headless',
+ '--disable-gpu',
+ '--disable-web-security',
+ '--remote-debugging-port=9222',
+ ],
+ },
+ },
+ });
+};
diff --git a/libs/workers/logo.svg b/libs/workers/logo.svg
new file mode 100644
index 000000000..9d0f2c7f3
--- /dev/null
+++ b/libs/workers/logo.svg
@@ -0,0 +1,76 @@
+
diff --git a/libs/workers/ng-package.json b/libs/workers/ng-package.json
new file mode 100644
index 000000000..b173a3470
--- /dev/null
+++ b/libs/workers/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
+ "dest": "../../dist/workers",
+ "lib": {
+ "entryFile": "src/index.ts"
+ }
+}
diff --git a/libs/workers/package.json b/libs/workers/package.json
new file mode 100644
index 000000000..7bea910b2
--- /dev/null
+++ b/libs/workers/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@ng-web-apis/workers",
+ "version": "0.0.0-development",
+ "description": "A library for comfortable use of Web Workers API in Angular",
+ "keywords": [
+ "angular",
+ "ng",
+ "worker",
+ "web",
+ "service",
+ "shared"
+ ],
+ "homepage": "https://github.com/ng-web-apis/workers#README",
+ "bugs": "https://github.com/ng-web-apis/workers/issues",
+ "repository": "https://github.com/ng-web-apis/workers",
+ "license": "MIT",
+ "author": {
+ "name": "Igor Katsuba",
+ "email": "katsuba.igor@gmail.com"
+ },
+ "contributors": [
+ "Alexander Inkin ",
+ "Roman Sedov <79601794011@ya.ru>"
+ ],
+ "peerDependencies": {
+ "@angular/core": ">=6.0.0",
+ "@ng-web-apis/common": ">=1.1.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/libs/workers/project.json b/libs/workers/project.json
new file mode 100644
index 000000000..488e9b901
--- /dev/null
+++ b/libs/workers/project.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "name": "workers",
+ "root": "libs/workers",
+ "sourceRoot": "libs/workers",
+ "projectType": "library",
+ "targets": {
+ "test": {
+ "executor": "@angular-devkit/build-angular:karma",
+ "outputs": ["coverage/workers"],
+ "options": {
+ "main": "libs/workers/test.ts",
+ "tsConfig": "tsconfig.spec.json",
+ "karmaConfig": "libs/workers/karma.conf.js",
+ "codeCoverage": true,
+ "browsers": "ChromeHeadless"
+ }
+ },
+ "build": {
+ "executor": "@angular-devkit/build-angular:ng-packagr",
+ "outputs": ["dist/workers"],
+ "options": {
+ "tsConfig": "tsconfig.build.json",
+ "project": "libs/workers/ng-package.json"
+ },
+ "dependsOn": [
+ {
+ "target": "build",
+ "projects": "dependencies",
+ "params": "forward"
+ }
+ ]
+ }
+ }
+}
diff --git a/libs/workers/src/index.ts b/libs/workers/src/index.ts
new file mode 100644
index 000000000..a7a39baee
--- /dev/null
+++ b/libs/workers/src/index.ts
@@ -0,0 +1,12 @@
+/**
+ * Public API Surface of @ng-web-apis/workers
+ */
+export * from './worker/classes/web-worker';
+export * from './worker/operators/to-data';
+
+export * from './worker/pipes/worker.pipe';
+
+export * from './worker/types/worker-function';
+export * from './worker/types/typed-message-event';
+
+export * from './worker/worker.module';
diff --git a/libs/workers/src/worker/classes/web-worker.ts b/libs/workers/src/worker/classes/web-worker.ts
new file mode 100644
index 000000000..0fc43740e
--- /dev/null
+++ b/libs/workers/src/worker/classes/web-worker.ts
@@ -0,0 +1,99 @@
+import {EMPTY, fromEvent, merge, Observable, Subject} from 'rxjs';
+import {take, takeUntil, tap} from 'rxjs/operators';
+import {WORKER_BLANK_FN} from '../consts/worker-fn-template';
+import {TypedMessageEvent} from '../types/typed-message-event';
+import {WorkerFunction} from '../types/worker-function';
+
+export class WebWorker extends Observable> {
+ private readonly worker: Worker | undefined;
+ private readonly url: string;
+ private readonly destroy$: Subject;
+
+ constructor(url: string, options?: WorkerOptions) {
+ let worker: Worker | undefined;
+ let error: any;
+
+ try {
+ worker = new Worker(url, options);
+ } catch (e) {
+ error = e;
+ }
+
+ super(subscriber => {
+ let eventStream$: Observable | ErrorEvent> = EMPTY;
+
+ if (error) {
+ subscriber.error(error);
+ } else if (this.destroy$.isStopped) {
+ subscriber.complete();
+ } else if (worker) {
+ eventStream$ = merge(
+ fromEvent>(worker, 'message').pipe(
+ tap(event => subscriber.next(event)),
+ ),
+ fromEvent(worker, 'error').pipe(
+ tap(event => subscriber.error(event)),
+ ),
+ ).pipe(takeUntil(this.destroy$));
+ }
+
+ eventStream$.subscribe().add(subscriber);
+ });
+
+ this.worker = worker;
+ this.url = url;
+ this.destroy$ = new Subject();
+ }
+
+ static fromFunction(
+ fn: WorkerFunction,
+ options?: WorkerOptions,
+ ): WebWorker {
+ return new WebWorker(WebWorker.createFnUrl(fn), options);
+ }
+
+ static execute(
+ fn: WorkerFunction,
+ data: T,
+ ): Promise> {
+ const worker = WebWorker.fromFunction(fn);
+ const promise = worker.pipe(take(1)).toPromise();
+
+ worker.postMessage(data);
+
+ return promise.then(result => {
+ worker.terminate();
+
+ return result as unknown as TypedMessageEvent;
+ });
+ }
+
+ private static createFnUrl(fn: WorkerFunction): string {
+ const script = `(${WORKER_BLANK_FN})(${fn});`;
+
+ const blob = new Blob([script], {type: 'text/javascript'});
+
+ return URL.createObjectURL(blob);
+ }
+
+ terminate() {
+ if (this.destroy$.isStopped) {
+ return;
+ }
+
+ if (this.worker) {
+ this.worker.terminate();
+ }
+
+ URL.revokeObjectURL(this.url);
+
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ postMessage(value: T) {
+ if (this.worker) {
+ this.worker.postMessage(value);
+ }
+ }
+}
diff --git a/libs/workers/src/worker/consts/worker-fn-template.ts b/libs/workers/src/worker/consts/worker-fn-template.ts
new file mode 100644
index 000000000..0d01e13e1
--- /dev/null
+++ b/libs/workers/src/worker/consts/worker-fn-template.ts
@@ -0,0 +1,22 @@
+// throw an error using the `setTimeout` function
+// because web worker doesn't emit ErrorEvent from promises
+export const WORKER_BLANK_FN = `
+function(fn){
+ function isFunction(type){
+ return type === 'function';
+ }
+
+ self.addEventListener('message', function(e) {
+ var result = fn.call(null, e.data);
+ if (result && [typeof result.then, typeof result.catch].every(isFunction)){
+ result
+ .then(postMessage)
+ .catch(function(error) {
+ setTimeout(function(){throw error}, 0)
+ })
+ } else {
+ postMessage(result);
+ }
+ })
+}
+`;
diff --git a/libs/workers/src/worker/operators/to-data.ts b/libs/workers/src/worker/operators/to-data.ts
new file mode 100644
index 000000000..0db7e1a23
--- /dev/null
+++ b/libs/workers/src/worker/operators/to-data.ts
@@ -0,0 +1,7 @@
+import {OperatorFunction} from 'rxjs';
+import {map} from 'rxjs/operators';
+import {TypedMessageEvent} from '../types/typed-message-event';
+
+export function toData(): OperatorFunction, T> {
+ return map, T>(({data}) => data);
+}
diff --git a/libs/workers/src/worker/pipes/worker.pipe.ts b/libs/workers/src/worker/pipes/worker.pipe.ts
new file mode 100644
index 000000000..36a9fd720
--- /dev/null
+++ b/libs/workers/src/worker/pipes/worker.pipe.ts
@@ -0,0 +1,41 @@
+import {OnDestroy, Pipe, PipeTransform} from '@angular/core';
+import {Observable} from 'rxjs';
+import {WebWorker} from '../classes/web-worker';
+import {toData} from '../operators/to-data';
+import {WorkerFunction} from '../types/worker-function';
+
+@Pipe({
+ name: 'waWorker',
+})
+export class WorkerPipe implements PipeTransform, OnDestroy {
+ private fn!: WorkerFunction;
+ private worker!: WebWorker;
+ private observer!: Observable;
+
+ transform(value: T, fn: WorkerFunction): Observable {
+ if (this.fn !== fn) {
+ this.terminateWorker();
+ this.initNewWorker(fn);
+ }
+
+ this.worker.postMessage(value);
+
+ return this.observer;
+ }
+
+ ngOnDestroy(): void {
+ this.terminateWorker();
+ }
+
+ private terminateWorker() {
+ if (this.worker) {
+ this.worker.terminate();
+ }
+ }
+
+ private initNewWorker(fn: WorkerFunction) {
+ this.fn = fn;
+ this.worker = WebWorker.fromFunction(fn);
+ this.observer = this.worker.pipe(toData());
+ }
+}
diff --git a/libs/workers/src/worker/types/typed-message-event.ts b/libs/workers/src/worker/types/typed-message-event.ts
new file mode 100644
index 000000000..492e03b8d
--- /dev/null
+++ b/libs/workers/src/worker/types/typed-message-event.ts
@@ -0,0 +1,3 @@
+export interface TypedMessageEvent extends MessageEvent {
+ data: T;
+}
diff --git a/libs/workers/src/worker/types/worker-function.ts b/libs/workers/src/worker/types/worker-function.ts
new file mode 100644
index 000000000..854e5557d
--- /dev/null
+++ b/libs/workers/src/worker/types/worker-function.ts
@@ -0,0 +1 @@
+export type WorkerFunction = (data: T) => R | Promise;
diff --git a/libs/workers/src/worker/worker.module.ts b/libs/workers/src/worker/worker.module.ts
new file mode 100644
index 000000000..e188f5197
--- /dev/null
+++ b/libs/workers/src/worker/worker.module.ts
@@ -0,0 +1,8 @@
+import {NgModule} from '@angular/core';
+import {WorkerPipe} from './pipes/worker.pipe';
+
+@NgModule({
+ declarations: [WorkerPipe],
+ exports: [WorkerPipe],
+})
+export class WorkerModule {}
diff --git a/libs/workers/test.ts b/libs/workers/test.ts
new file mode 100644
index 000000000..a9cc62b9e
--- /dev/null
+++ b/libs/workers/test.ts
@@ -0,0 +1,23 @@
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+import 'zone.js/dist/zone';
+import 'zone.js/dist/zone-testing';
+
+import {getTestBed} from '@angular/core/testing';
+import {
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting,
+} from '@angular/platform-browser-dynamic/testing';
+
+declare const require: any;
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting(),
+);
+
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+
+// And load the modules.
+context.keys().map(context);
diff --git a/libs/workers/tests/web-worker.spec.ts b/libs/workers/tests/web-worker.spec.ts
new file mode 100644
index 000000000..822b7c225
--- /dev/null
+++ b/libs/workers/tests/web-worker.spec.ts
@@ -0,0 +1,113 @@
+import {Observable} from 'rxjs';
+import {take} from 'rxjs/operators';
+import {TypedMessageEvent} from '../src/worker/types/typed-message-event';
+import {WebWorker} from '../src/worker/classes/web-worker';
+
+// it is needed to ignore web worker errors
+window.onerror = () => {};
+
+describe('WebWorker', () => {
+ it('should fail if a worker is not available', async () => {
+ const OriginalWorker = Worker;
+
+ delete (window as any).Worker;
+
+ const worker = WebWorker.fromFunction(d => d);
+
+ expect(() => worker.terminate()).not.toThrow();
+ expect(() => worker.postMessage()).not.toThrow();
+
+ await expectAsync(worker.toPromise()).toBeRejected();
+
+ (window as any).Worker = OriginalWorker;
+ });
+
+ it('should create worker from a function', () => {
+ const worker = WebWorker.fromFunction(d => d);
+
+ expect(worker instanceof WebWorker).toEqual(true);
+ expect((worker as any).worker instanceof Worker).toEqual(true);
+ });
+
+ it('should trigger an error if URL was not found', async () => {
+ const worker = new WebWorker('some/wrong/url');
+
+ await expectAsync(worker.toPromise()).toBeRejected();
+ });
+
+ it('should resolve the last value before completing', async () => {
+ const worker = WebWorker.fromFunction((data: string) => Promise.resolve(data));
+
+ const promise = worker
+ .pipe(source => {
+ return new Observable(subscriber => {
+ source.subscribe({
+ next({data}: TypedMessageEvent) {
+ (source as WebWorker).terminate();
+ subscriber.next(data);
+ subscriber.complete();
+ },
+ });
+ });
+ })
+ .toPromise();
+
+ worker.postMessage('a');
+ worker.postMessage('b');
+ expect(await promise).toEqual('a');
+ });
+
+ it('should run a worker and return a correct data', async () => {
+ const workerPromise: Promise> = WebWorker.execute<
+ string,
+ string
+ >(data => Promise.resolve().then(() => data), 'some data');
+
+ expect((await workerPromise).data).toEqual('some data');
+ }, 10000);
+
+ it('should create worker', async () => {
+ const thread = WebWorker.fromFunction(data =>
+ Promise.resolve(data),
+ );
+
+ const workerPromise = thread.pipe(take(1)).toPromise();
+
+ thread.postMessage('some data');
+
+ expect((await workerPromise)?.data).toEqual('some data');
+ }, 10000);
+
+ it('should fail if an inner promise is rejected', async () => {
+ const worker = WebWorker.fromFunction(() =>
+ Promise.reject('reason'),
+ );
+
+ worker.postMessage();
+
+ expect(await worker.toPromise().catch(err => err.message)).toEqual(
+ 'Uncaught reason',
+ );
+ });
+
+ it('should close all subscriptions, if the worker was terminated', async () => {
+ const worker = WebWorker.fromFunction(() => 'some data');
+
+ const subscriptions = [
+ worker.subscribe(),
+ worker.subscribe(),
+ worker.subscribe(),
+ ];
+
+ worker.terminate();
+ expect(subscriptions.map(s => s.closed)).toEqual([true, true, true]);
+ });
+
+ it("shouldn't throw any errors, if the worker was terminated twice", async () => {
+ const worker = WebWorker.fromFunction(() => 'some data');
+
+ worker.terminate();
+ worker.terminate();
+ expect(await worker.toPromise()).toBeUndefined();
+ });
+});
diff --git a/libs/workers/tests/worker.pipe.spec.ts b/libs/workers/tests/worker.pipe.spec.ts
new file mode 100644
index 000000000..4372932d3
--- /dev/null
+++ b/libs/workers/tests/worker.pipe.spec.ts
@@ -0,0 +1,49 @@
+import {take} from 'rxjs/operators';
+import {WorkerPipe} from '../src/worker/pipes/worker.pipe';
+
+describe('WorkerPipe', () => {
+ let pipe: WorkerPipe;
+
+ beforeEach(() => {
+ pipe = new WorkerPipe();
+ });
+
+ it('should emit the first value', async () => {
+ const result = await pipe
+ .transform('a', data => data)
+ .pipe(take(1))
+ .toPromise();
+
+ expect(await result).toEqual('a');
+ });
+
+ it('should return the same worker for the same function', async () => {
+ const workerFn = (data: unknown) => data;
+
+ const worker = await pipe.transform('a', workerFn);
+ const theSameWorker = await pipe.transform('a', workerFn);
+
+ expect(worker).toEqual(theSameWorker);
+ });
+
+ it('should return a different worker for a different function', async () => {
+ const worker = await pipe.transform('a', (data: unknown) => data);
+ const differentWorker = await pipe.transform('a', (data: unknown) => data);
+
+ expect(worker).not.toEqual(differentWorker);
+ });
+
+ it('should terminate a previous worker', async () => {
+ const worker = await pipe.transform('a', (data: unknown) => data);
+
+ await pipe.transform('a', (data: unknown) => data);
+ await expectAsync(worker.toPromise()).toBeResolved();
+ });
+
+ it('should terminate a worker then a pipe is destroyed', async () => {
+ const worker = await pipe.transform('a', (data: unknown) => data);
+
+ pipe.ngOnDestroy();
+ await expectAsync(worker.toPromise()).toBeResolved();
+ });
+});
diff --git a/libs/workers/tsconfig.spec.json b/libs/workers/tsconfig.spec.json
new file mode 100644
index 000000000..8e7067ed2
--- /dev/null
+++ b/libs/workers/tsconfig.spec.json
@@ -0,0 +1,5 @@
+{
+ "extends": "../../tsconfig.spec.json",
+ "include": ["**/*.spec.ts", "./test.ts", "**/*.d.ts"],
+ "files": ["./test.ts"]
+}
diff --git a/tsconfig.build.json b/tsconfig.build.json
index 2ccc5ff8c..db547a771 100644
--- a/tsconfig.build.json
+++ b/tsconfig.build.json
@@ -22,6 +22,7 @@
"@ng-web-apis/permissions": ["./dist/permissions"],
"@ng-web-apis/intersection-observer": ["./dist/intersection-observer"],
"@ng-web-apis/midi": ["./dist/midi"],
+ "@ng-web-apis/workers": ["./dist/workers"],
"@ng-web-apis/resize-observer": ["./dist/resize-observer"]
}
}
diff --git a/tsconfig.json b/tsconfig.json
index 155456cb5..f8f98feda 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -74,6 +74,7 @@
"@ng-web-apis/intersection-observer": ["./libs/intersection-observer/src/index.ts"],
"@ng-web-apis/midi": ["./libs/midi/src/index.ts"],
"@ng-web-apis/storage": ["./libs/storage/src/index.ts"],
+ "@ng-web-apis/workers": ["./libs/workers/src/index.ts"],
"@ng-web-apis/resize-observer": ["./libs/resize-observer/src/index.ts"]
}
},